# Distribution + Auto-Update

## Architecture

```mermaid
flowchart TD
    subgraph DIST["GoClaw Distribution"]
        BIN["<b>Single Binary</b><br/>cgo: SQLite + tree-sitter"]
        DOC["<b>Docker</b><br/>Multi-stage Alpine"]
        SVC["<b>OS Service</b><br/>systemd + launchd"]
        BUILD["<b>CGO_ENABLED=1 go build -o goclaw ./cmd/gateway</b><br/>C compiler per target · Linux builds: glibc 2.34+"]

        BIN --> BUILD
        DOC --> BUILD
        SVC --> BUILD
    end
```

GoClaw needs cgo: `mattn/go-sqlite3` (the database) and `smacker/go-tree-sitter` (the code
analysis tools) are C code compiled into the binary, so a build with `CGO_ENABLED=0` does not
compile. The Linux release binaries link glibc dynamically and need glibc 2.34 or newer
(Ubuntu 22.04, Debian 12, RHEL 9 or later); they do not run on musl (Alpine). On an older or
musl system, build from source there or use the Docker image, which builds its own binary.

## Distribution / Installation (Section 17)

| # | Method | OpenClaw | GoClaw | Status |
|---|--------|----------|---------|--------|
| 17.1 | **Single binary** | — | One binary per platform with SQLite and tree-sitter compiled in (cgo); Linux builds need glibc 2.34+ | Done |
| 17.2 | **From source** | `pnpm install && pnpm build` | `go build -o goclaw ./cmd/gateway` with a C compiler; add `CGO_CFLAGS="-DSQLITE_ENABLE_FTS5"` as the release does, or the knowledge graph falls back from full-text search to `LIKE` | Done |
| 17.3 | **Docker** | Dockerfile + docker-compose | ✅ Multi-stage Alpine build (cgo on musl, non-root user) + docker-compose.yaml | Done |
| 17.4 | **OS service install** | launchd/systemd/schtasks | `goclaw service install` for systemd (Linux) and launchd (macOS); `--system [--run-as <user>]` for a system service run by an ordinary user | Done |
| 17.5 | **OS service uninstall** | Platform-specific | `goclaw service uninstall` (stop + disable + remove) | Done |
| 17.6 | **Cross-compilation** | — | Release matrix: darwin arm64/amd64 built on macOS; linux amd64 (`gcc`), arm64 (`aarch64-linux-gnu-gcc`), armv7 (`arm-linux-gnueabihf-gcc`) and windows amd64 (`x86_64-w64-mingw32-gcc`) cross-compiled on Ubuntu 24.04 | Done |
| 17.7 | npm install | `npm i -g openclaw` (primary) | — | **TODO** |
| 17.8 | Shell script (macOS/Linux) | `curl openclaw.ai/install.sh \| bash` | `curl -fsSL https://goclaw.ulab.mn/install.sh \| sh` | Done |
| 17.9 | PowerShell (Windows) | `irm openclaw.ai/install.ps1 \| iex` | — (the Windows binary is a release asset) | **TODO** |
| 17.10 | macOS .app / .dmg | Signed app bundle with Sparkle | — | **TODO** |
| 17.11 | Homebrew | — | — | **TODO** |
| 17.12 | Go install | — | — The module is `ulab.mn/agents/goclaw` and its main package `./cmd/gateway`, so `go install` would name the binary `gateway`; build from source instead | **TODO** |
| 17.13 | GitHub Releases | Release assets (binaries) | Every tag publishes its assets to a GitHub Release, to `https://goclaw.ulab.mn/releases/<version>/` and to the release bucket (`https://storage.googleapis.com/goclaw-releases/releases/<version>/`) | Done |

### install.sh

`curl -fsSL https://goclaw.ulab.mn/install.sh | sh` installs the latest release on macOS
(arm64, amd64) and Linux (amd64, arm64, armv7):

1. Reads the version from `https://goclaw.ulab.mn/releases/latest.json`, falling back to
   `https://goclaw-hosting.web.app` and then to the release bucket
   (`https://storage.googleapis.com/goclaw-releases`) when the one before does not answer,
   and stops when none does. It downloads from the mirror that answered.
2. Downloads `goclaw-<os>-<arch>` and that release's `checksums.json`, and stops unless the
   binary's SHA-256 matches (it also stops when no SHA-256 tool is found).
3. Installs it as `/usr/local/bin/goclaw` through a staged file and a rename, so it replaces a
   running binary; it asks for `sudo` when the directory is not writable.
4. Prints the next steps: `goclaw setup`, `goclaw`, `goclaw service install`, `goclaw doctor`.

### Release assets

A tag `vX.Y.Z` runs `.github/workflows/release.yml`, which publishes to
`https://goclaw.ulab.mn/releases/<version>/` (Firebase Hosting), to a GitHub Release of the
same name and, from that Release, to the release bucket `gs://goclaw-releases`, public at
`https://storage.googleapis.com/goclaw-releases/releases/<version>/`:

| Asset | What it is |
|-------|------------|
| `goclaw-darwin-arm64`, `goclaw-darwin-amd64` | macOS binaries, codesigned when the signing certificate is configured |
| `goclaw-linux-amd64`, `goclaw-linux-arm64`, `goclaw-linux-armv7` | Linux binaries (glibc 2.34+) |
| `goclaw-windows-amd64.exe` | Windows binary |
| `goclaw-fleet-<version>.tar.gz` | The [fleet toolkit](../user-guide/fleet.md) (`ansible/`) |
| `goclaw-<version>-linux-amd64-sbom.cdx.json`, `goclaw-<version>-linux-amd64-sbom.spdx.json` | CycloneDX 1.5 and SPDX 2.3 SBOMs read from the linux-amd64 binary ([SBOM](sbom.md)) |
| `goclaw-<version>-vex.json` | OpenVEX statements from `security/vex.json`, stamped with the release |
| `goclaw-<version>-govulncheck.json`, `goclaw-<version>-govulncheck.txt` | govulncheck report |
| `checksums.json` | SHA-256 of every asset above |

`releases/latest.json` (`{"version": …, "published_at": …}`) and
`releases/latest/checksums.json` name the newest release; `install.sh` and `goclaw update`
read them. Firebase Hosting replaces the whole site on every deploy, so a docs-only deploy
(`.github/workflows/deploy-site.yml`) first rebuilds `dist/releases/` with
`scripts/restore_releases.sh`, from the GitHub Release (else the bucket, else the live site),
checking every asset against `checksums.json`.

The site keeps only the newest release; the bucket keeps every version from v0.2.2 on, under
the same paths (`releases/<version>/…`, `releases/latest.json`, `releases/latest/checksums.json`).
A versioned file is never replaced once uploaded and is served with
`Cache-Control: public, max-age=31536000, immutable`; the two `latest` files are `no-cache`
and move only forward, so republishing an older version leaves them on the newer one.

### Docker (Multi-Stage Alpine Build)

The `Dockerfile`, without its comments:

```dockerfile
# Stage 1: Build
FROM golang:1.27-alpine AS builder
WORKDIR /app
RUN apk add --no-cache git build-base sqlite-dev
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=1 GOOS=linux \
    CGO_CFLAGS="-DSQLITE_ENABLE_FTS5 -DSQLITE_ENABLE_JSON1" \
    go build -tags="sqlite_fts5 sqlite_json" -o goclaw ./cmd/gateway

# Stage 2: Runtime
FROM alpine:3.21
RUN apk add --no-cache ca-certificates tzdata bash sqlite-libs
RUN adduser -D -u 10001 goclaw
WORKDIR /app
COPY --from=builder /app/goclaw .
COPY --from=builder /app/web ./web
RUN touch config.yaml && chown -R goclaw:goclaw /app \
    && mkdir -p /home/goclaw/.goclaw && chown goclaw:goclaw /home/goclaw/.goclaw
USER goclaw
EXPOSE 8080
CMD ["./goclaw"]
```

The gateway runs as the unprivileged user `goclaw` (uid 10001); its state directory is
`/home/goclaw/.goclaw`.

**Docker Compose:** `docker-compose.yaml` builds the image and runs it as `goclaw-gateway` on
port 8080 with `restart: unless-stopped`. It mounts:

- `./config.yaml` at `/app/config.yaml`. It must set `gateway.bind: lan`, or the gateway
  listens on the container's loopback and the port mapping reaches nothing. The gateway reads
  it when the state directory has no `config.yaml` of its own.
- the named volume `goclaw-state` at `/home/goclaw/.goclaw`, the state directory: the database
  (`gc_state.sqlite`), the secrets vault, the workspace, memory and logs.

Set `GOCLAW_SECRET_KEY` under `environment` (the file has the line commented out). Without
it the vault key is derived from the user name and home directory, which are the same in every
container built from this image, so anyone with the image can derive it.

### OS Service Templates

| Platform | Service | Features |
|----------|---------|----------|
| **Linux** | systemd (`goclaw.service`) | `Restart=always`, `RestartSec=5`, journal logging; `WantedBy=default.target` for a per-user unit (`~/.config/systemd/user/`, with lingering so it starts at boot), `multi-user.target` for `--system` (`/etc/systemd/system/`) |
| **macOS** | launchd (`goclaw.plist` in `~/Library/LaunchAgents/`, or `/Library/LaunchDaemons/` for `--system`) | `RunAtLoad=true`, `KeepAlive=true`, stdout/stderr to `~/Library/Logs/goclaw.stdout.log` and `goclaw.stderr.log` |

Service install auto-detects binary path via `os.Executable()` fallback to `$PATH` lookup.
The systemd unit comes from goclaw's own template (`systemdUnitTemplate` in
`internal/cli/service.go`): kardianos/service's writes `RestartSec=120` and
`WantedBy=multi-user.target` for every unit, and a per-user unit enabled for that target never
starts at boot. `goclaw doctor` and the gateway's start-up log report an installed unit that
will not start at boot, with the command that fixes it.

---

## Auto-Update (Section 21)

| # | Feature | OpenClaw | GoClaw | Status |
|---|---------|----------|---------|--------|
| 21.1 | CLI update command | `openclaw update` | `goclaw update [--version vX.Y.Z] [--force] [--backup]` | Done |
| 21.2 | macOS Sparkle | `appcast.xml`, signed updates | — | **TODO** |
| 21.3 | Auto-update config | `update.auto.*` (stableDelayHours, jitter) | — | **TODO** |
| 21.4 | Control UI update | "Update & Restart" button | — | **TODO** |
| 21.5 | Version check | npm registry / GitHub | — (`goclaw update` reads `releases/latest.json` when it is run; nothing checks on its own) | **TODO** |

How `goclaw update` works (`internal/cli/cmd_update.go`, `internal/cli/update_backup.go`):

1. Takes the version from `--version`, or from `https://goclaw.ulab.mn/releases/latest.json`
   (the release bucket's when the site does not answer), and stops when that version is
   already running, unless `--force`.
2. Downloads `releases/<version>/checksums.json` and the asset for this OS and architecture to
   a temporary file, and stops unless its SHA-256 matches. Both come from the site first and,
   when it does not have that version (it keeps only the newest) or its copy does not
   match, from the release bucket; so `--version` can go back to any release from v0.2.2 on.
3. On macOS, verifies the code signature (`codesign --verify --strict`) when the binary is
   signed; an unsigned binary is installed with a warning.
4. When `--backup` is passed, snapshots `gc_state.sqlite` and `kg.sqlite` (using `VACUUM INTO`)
   together with `config.yaml`, `credentials/`, `workspace/`, `agents/`,
   `memory/`, `skills/`, `extensions/` and `plugins/` into
   `<state dir>/backups/goclaw-backup_<version>_<new-version>_<timestamp>.tar.gz`, keeping the 3
   most recent update archives.
5. Renames the running binary to `<binary>.prev` (one step of rollback) and the new one into
   its place, asking for `sudo` when the directory (such as `/usr/local/bin`) is not writable.
6. Stops and starts the installed service: the system service when
   `/etc/systemd/system/goclaw.service` or `/Library/LaunchDaemons/goclaw.plist` exists, else
   the per-user one (running as `$SUDO_USER` when `goclaw update` was invoked through `sudo`).
   When that fails it says to run `goclaw service restart` (or `sudo goclaw service restart`
   for a system service).

The fleet toolkit's `make upgrade` runs it on each host
([Upgrade and restart](../user-guide/fleet.md#upgrade-and-restart)).

---

## OpenClaw vs GoClaw Distribution Comparison

| Aspect | OpenClaw | GoClaw |
|--------|----------|---------|
| **Primary install** | npm (requires Node.js) | **Single binary** (`install.sh` or a release asset) |
| **Runtime deps** | Node.js 22+ | **glibc 2.34+ on Linux; nothing else** (SQLite and tree-sitter compiled in with cgo) |
| **Docker** | Dockerfile | ✅ Multi-stage Alpine (non-root; state in a named volume) |
| **OS service** | launchd/systemd/schtasks | systemd + launchd (auto-detect binary) |
| **Cross-compile** | Platform packages | **darwin arm64/amd64, linux amd64/arm64/armv7, windows amd64**, each with its C compiler |
| **Service features** | — | auto-restart, journal/file logging, KeepAlive |
| **macOS app** | Signed .app with Sparkle | — (TODO) |
| **Auto-update** | Sparkle + npm | `goclaw update` (run by hand or by the fleet toolkit) |
| **Install methods** | 6+ | **install.sh, release binaries, Docker, source** |

Not implemented: npm, a PowerShell installer, a macOS app, Homebrew, `go install`, and
automatic update checks.
