415 lines
13 KiB
Markdown
415 lines
13 KiB
Markdown
|
|
# CACT server deploy playbook
|
|||
|
|
|
|||
|
|
Repeatable runbook for standing up the **CloudAxe Coder Team (CACT)** on a new Linux server: headless OpenCode, the `cact-*` agent pack, provider credentials, systemd supervision, and a CloudAxe desktop connection check.
|
|||
|
|
|
|||
|
|
Use placeholders only. Do not commit live IPs, passwords, or API keys.
|
|||
|
|
|
|||
|
|
## What you are deploying
|
|||
|
|
|
|||
|
|
| Piece | Role |
|
|||
|
|
| --- | --- |
|
|||
|
|
| OpenCode headless server | `opencode serve` HTTP API on port `4096` |
|
|||
|
|
| CACT agents | `~/.config/opencode/agents/cact-*.md` |
|
|||
|
|
| Global rules | `~/.config/opencode/AGENTS.md` |
|
|||
|
|
| Provider auth | `~/.local/share/opencode/auth.json` (never in git) |
|
|||
|
|
| Clients | CloudAxe / OpenCode Desktop (Settings → Add Server), or `curl` / SDK |
|
|||
|
|
|
|||
|
|
`cact-` means **CloudAxe Coder Team**. The normal entry agent is `cact-orchestrator-tech-lead`.
|
|||
|
|
|
|||
|
|
Paths follow XDG defaults (`xdg-basedir`):
|
|||
|
|
|
|||
|
|
| Kind | Default path (Linux) |
|
|||
|
|
| --- | --- |
|
|||
|
|
| Config | `~/.config/opencode` |
|
|||
|
|
| Data (auth, logs) | `~/.local/share/opencode` |
|
|||
|
|
| State | `~/.local/state/opencode` |
|
|||
|
|
| Cache | `~/.cache/opencode` |
|
|||
|
|
|
|||
|
|
Override the config directory with `OPENCODE_CONFIG_DIR` only when you intentionally isolate installs.
|
|||
|
|
|
|||
|
|
## Assumptions
|
|||
|
|
|
|||
|
|
- Target OS: Ubuntu 22.04/24.04 or Debian 12 (amd64 or arm64).
|
|||
|
|
- You have SSH access and can use `sudo`.
|
|||
|
|
- You will use a **dedicated service user** (example: `opencode`).
|
|||
|
|
- The CloudAxe desktop expects the **same OpenCode build** as this fork (v2 API). Do not deploy an unrelated older package and assume compatibility.
|
|||
|
|
- Provider policy: **direct vendor APIs** (Moonshot, DeepSeek, Z.ai). Avoid OpenRouter for CACT agents.
|
|||
|
|
|
|||
|
|
## Pre-flight
|
|||
|
|
|
|||
|
|
Before changing anything on the host:
|
|||
|
|
|
|||
|
|
1. Confirm access scope (sudo / root).
|
|||
|
|
2. Name the environment: `dev` / `staging` / `production`.
|
|||
|
|
3. Note existing listeners on `4096` (`ss -tlnp | grep 4096`).
|
|||
|
|
4. Decide whether port `4096` is exposed raw or only via Nginx TLS on `443`.
|
|||
|
|
5. Prepare a rollback posture: previous binary path, config tarball location, and how to stop the unit.
|
|||
|
|
|
|||
|
|
Destructive classification for a first install: **risky** (new systemd unit, firewall rules, public listener). Treat password/key rotation and unit replacement on a live host as **destructive** unless a verified backup and rollback path exist.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 1. Target shape
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
Internet / operator LAN
|
|||
|
|
│
|
|||
|
|
▼
|
|||
|
|
[optional Nginx TLS :443]
|
|||
|
|
│
|
|||
|
|
▼
|
|||
|
|
opencode serve
|
|||
|
|
--hostname 0.0.0.0 # or 127.0.0.1 behind Nginx
|
|||
|
|
--port 4096
|
|||
|
|
+ OPENCODE_SERVER_PASSWORD
|
|||
|
|
+ OPENCODE_SERVER_USERNAME (optional; default opencode)
|
|||
|
|
│
|
|||
|
|
▼
|
|||
|
|
~/.config/opencode/agents/cact-*.md
|
|||
|
|
~/.local/share/opencode/auth.json
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
CLI flags (see `packages/opencode/src/cli/cmd/serve.ts` and `packages/opencode/src/cli/network.ts`):
|
|||
|
|
|
|||
|
|
| Flag | Purpose | Typical CACT value |
|
|||
|
|
| --- | --- | --- |
|
|||
|
|
| `--port` | Listen port | `4096` |
|
|||
|
|
| `--hostname` | Bind address | `0.0.0.0` (direct) or `127.0.0.1` (behind proxy) |
|
|||
|
|
| `--cors` | Extra browser origins | Desktop / app origins as needed |
|
|||
|
|
| `--mdns` | LAN discovery | **off** on a VPS |
|
|||
|
|
|
|||
|
|
If `OPENCODE_SERVER_PASSWORD` is unset, the server starts **unsecured** and prints a warning. Always set it for any network-reachable host.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 2. Host bootstrap
|
|||
|
|
|
|||
|
|
Run as a sudo-capable operator.
|
|||
|
|
|
|||
|
|
### 2.1 Packages
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
sudo apt-get update
|
|||
|
|
sudo apt-get install -y git curl ca-certificates ripgrep build-essential unzip
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Install Bun (required to build/run from this repo):
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
curl -fsSL https://bun.sh/install | bash
|
|||
|
|
# ensure ~/.bun/bin is on PATH for the service user as well
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2.2 Service user
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
sudo useradd --system --create-home --shell /bin/bash opencode
|
|||
|
|
sudo -u opencode -H bash -lc 'mkdir -p ~/.config/opencode/agents ~/.local/share/opencode'
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 2.3 Clone and install this fork
|
|||
|
|
|
|||
|
|
Replace `YOUR_FORK_URL` and pin a known-good ref (tag or commit SHA).
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
sudo -u opencode -H bash <<'EOF'
|
|||
|
|
set -euo pipefail
|
|||
|
|
cd ~
|
|||
|
|
git clone YOUR_FORK_URL opencode
|
|||
|
|
cd opencode
|
|||
|
|
git checkout YOUR_PINNED_REF
|
|||
|
|
bun install
|
|||
|
|
cd packages/opencode
|
|||
|
|
bun run build # or the package's documented production build script
|
|||
|
|
EOF
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Install a durable binary on `PATH` for the service user (example: copy the built CLI into `~/bin`):
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
sudo -u opencode -H bash <<'EOF'
|
|||
|
|
set -euo pipefail
|
|||
|
|
mkdir -p "$HOME/bin"
|
|||
|
|
# Adjust source path to match the build output for your platform
|
|||
|
|
cp -f "$HOME/opencode/packages/opencode/dist/opencode-linux-"*/bin/opencode "$HOME/bin/opencode"
|
|||
|
|
chmod 755 "$HOME/bin/opencode"
|
|||
|
|
"$HOME/bin/opencode" --version
|
|||
|
|
EOF
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Record the version string in your ops notes. The CloudAxe desktop must speak the same API generation as this server.
|
|||
|
|
|
|||
|
|
Alternative: install a prebuilt musl binary that matches this fork’s release artifacts (see `packages/opencode/Dockerfile`), then still deploy CACT config as below.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 3. Deploy the CACT agent pack
|
|||
|
|
|
|||
|
|
Copy from a trusted workstation or private backup into the service user’s config. Source of truth on the operator machine is typically:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
~/.config/opencode/AGENTS.md
|
|||
|
|
~/.config/opencode/agents/cact-*.md
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
On the server:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
# From an operator machine (example)
|
|||
|
|
scp AGENTS.md opencode@SERVER:~/.config/opencode/
|
|||
|
|
scp cact-*.md opencode@SERVER:~/.config/opencode/agents/
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Expected agents:
|
|||
|
|
|
|||
|
|
| Agent file | Model (frontmatter) | Provider |
|
|||
|
|
| --- | --- | --- |
|
|||
|
|
| `cact-orchestrator-tech-lead.md` | `moonshotai/kimi-k3` | Moonshot |
|
|||
|
|
| `cact-frontend-agent.md` | `moonshotai/kimi-k3` | Moonshot |
|
|||
|
|
| `cact-backend-agent.md` | `deepseek/deepseek-v4-pro` | DeepSeek |
|
|||
|
|
| `cact-database-agent.md` | `deepseek/deepseek-v4-pro` | DeepSeek |
|
|||
|
|
| `cact-devops-server-engineer.md` | `deepseek/deepseek-v4-pro` | DeepSeek |
|
|||
|
|
| `cact-security-agent.md` | `deepseek/deepseek-v4-pro` | DeepSeek |
|
|||
|
|
| `cact-qa-agent.md` | `deepseek/deepseek-v4-pro` | DeepSeek |
|
|||
|
|
| `cact-gatekeeper-agent.md` | `deepseek/deepseek-v4-pro` | DeepSeek |
|
|||
|
|
| `cact-requirements-agent.md` | `zai/glm-5.2` | Z.ai |
|
|||
|
|
| `cact-requirements-agent-glm-divergent.md` | `zai/glm-5.2` | Z.ai |
|
|||
|
|
| `cact-research-agent.md` | `zai/glm-5.2` | Z.ai |
|
|||
|
|
| `cact-research-agent-glm-divergent.md` | `zai/glm-5.2` | Z.ai |
|
|||
|
|
| `cact-requirements-agent-deepseek.md` | `deepseek/deepseek-v4-pro` | DeepSeek |
|
|||
|
|
| `cact-research-agent-deepseek.md` | `deepseek/deepseek-v4-pro` | DeepSeek |
|
|||
|
|
| `cact-documentation-agent.md` | `openrouter/moonshotai/kimi-k2.6` | **Needs fix** |
|
|||
|
|
|
|||
|
|
**OpenRouter callout:** `cact-documentation-agent` still references OpenRouter. Before relying on that agent, change its frontmatter `model` to a direct Moonshot (or other approved direct) model ID and verify with `opencode models --refresh` on the server.
|
|||
|
|
|
|||
|
|
Optional global config (`~/.config/opencode/opencode.jsonc`) may enable MCP servers. Keep secrets out of that file.
|
|||
|
|
|
|||
|
|
Do **not** copy `auth.json`, `.env`, or systemd env files into the git repository.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 4. Secrets and provider auth
|
|||
|
|
|
|||
|
|
### 4.1 Server basic auth env file
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
sudo install -d -m 0750 -o root -g opencode /etc/opencode
|
|||
|
|
sudo tee /etc/opencode/server.env >/dev/null <<'EOF'
|
|||
|
|
OPENCODE_SERVER_USERNAME=opencode
|
|||
|
|
OPENCODE_SERVER_PASSWORD=REPLACE_WITH_LONG_RANDOM_SECRET
|
|||
|
|
# Optional: pin HOME for the service if the unit does not set User=
|
|||
|
|
# HOME=/home/opencode
|
|||
|
|
EOF
|
|||
|
|
sudo chmod 0640 /etc/opencode/server.env
|
|||
|
|
sudo chown root:opencode /etc/opencode/server.env
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Generate a strong password (`openssl rand -base64 32`). Store it in your password manager; do not paste it into tickets or git.
|
|||
|
|
|
|||
|
|
### 4.2 Provider credentials
|
|||
|
|
|
|||
|
|
As the service user, log in to each vendor API you need:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
sudo -u opencode -H bash -lc 'export PATH="$HOME/bin:$HOME/.bun/bin:$PATH"; opencode auth login'
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Credentials land in `~/.local/share/opencode/auth.json` under that user’s home. Restrict permissions:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
sudo -u opencode -H bash -lc 'chmod 600 ~/.local/share/opencode/auth.json'
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Minimum providers for the current CACT pack: Moonshot, DeepSeek, Z.ai. Refresh model lists after login:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
sudo -u opencode -H bash -lc 'opencode models --refresh'
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 5. systemd unit
|
|||
|
|
|
|||
|
|
Create `/etc/systemd/system/opencode-cact.service`:
|
|||
|
|
|
|||
|
|
```ini
|
|||
|
|
[Unit]
|
|||
|
|
Description=OpenCode CACT headless server
|
|||
|
|
After=network-online.target
|
|||
|
|
Wants=network-online.target
|
|||
|
|
|
|||
|
|
[Service]
|
|||
|
|
Type=simple
|
|||
|
|
User=opencode
|
|||
|
|
Group=opencode
|
|||
|
|
WorkingDirectory=/home/opencode
|
|||
|
|
EnvironmentFile=/etc/opencode/server.env
|
|||
|
|
Environment=HOME=/home/opencode
|
|||
|
|
Environment=PATH=/home/opencode/bin:/home/opencode/.bun/bin:/usr/local/bin:/usr/bin
|
|||
|
|
ExecStart=/home/opencode/bin/opencode serve --hostname 0.0.0.0 --port 4096
|
|||
|
|
Restart=on-failure
|
|||
|
|
RestartSec=5
|
|||
|
|
# Harden lightly; expand after validating tool/workspace needs
|
|||
|
|
NoNewPrivileges=true
|
|||
|
|
PrivateTmp=true
|
|||
|
|
|
|||
|
|
[Install]
|
|||
|
|
WantedBy=multi-user.target
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
If you terminate TLS at Nginx, bind OpenCode to loopback instead:
|
|||
|
|
|
|||
|
|
```ini
|
|||
|
|
ExecStart=/home/opencode/bin/opencode serve --hostname 127.0.0.1 --port 4096
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Enable and start:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
sudo systemctl daemon-reload
|
|||
|
|
sudo systemctl enable --now opencode-cact.service
|
|||
|
|
sudo systemctl status opencode-cact.service --no-pager
|
|||
|
|
sudo journalctl -u opencode-cact.service -n 50 --no-pager
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Rollout order
|
|||
|
|
|
|||
|
|
1. Validate binary: `opencode --version`
|
|||
|
|
2. Validate env file permissions and that `OPENCODE_SERVER_PASSWORD` is set
|
|||
|
|
3. `daemon-reload` → `restart` (or `start` on first install)
|
|||
|
|
4. Run health probes (section 7)
|
|||
|
|
5. Connect desktop
|
|||
|
|
|
|||
|
|
### Rollback
|
|||
|
|
|
|||
|
|
1. `sudo systemctl stop opencode-cact.service`
|
|||
|
|
2. Restore previous binary into `/home/opencode/bin/opencode`
|
|||
|
|
3. Restore config/auth from the timestamped snapshot (section 8)
|
|||
|
|
4. `sudo systemctl start opencode-cact.service`
|
|||
|
|
5. Re-run health probes
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 6. Network and hardening
|
|||
|
|
|
|||
|
|
### 6.1 Firewall
|
|||
|
|
|
|||
|
|
Allow `4096/tcp` only from operator addresses (example with UFW):
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
sudo ufw allow OpenSSH
|
|||
|
|
sudo ufw allow from OPERATOR_CIDR to any port 4096 proto tcp
|
|||
|
|
sudo ufw enable
|
|||
|
|
sudo ufw status verbose
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Cloud security groups / nftables should match the same allowlist.
|
|||
|
|
|
|||
|
|
### 6.2 Optional Nginx TLS
|
|||
|
|
|
|||
|
|
When the host must not expose raw HTTP:
|
|||
|
|
|
|||
|
|
1. Bind OpenCode to `127.0.0.1:4096`.
|
|||
|
|
2. Proxy `https://cact.example.com` → `http://127.0.0.1:4096` with WebSocket upgrade headers if clients use SSE/WS.
|
|||
|
|
3. Obtain certificates (Let’s Encrypt / ACME).
|
|||
|
|
4. Point the desktop at `https://cact.example.com` with the same basic-auth username/password.
|
|||
|
|
|
|||
|
|
Keep mDNS disabled on public VPS hosts (`--mdns` unset / false).
|
|||
|
|
|
|||
|
|
### 6.3 Workspace directories
|
|||
|
|
|
|||
|
|
Clients send a project directory via request headers. Ensure the service user can read/write the repos you intend to work in (for example under `/home/opencode/workspaces`). Do not run the server as root.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 7. Prove it
|
|||
|
|
|
|||
|
|
Replace placeholders. Username defaults to `opencode` unless you set `OPENCODE_SERVER_USERNAME`.
|
|||
|
|
|
|||
|
|
### 7.1 Authenticated HTTP
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
export CACT_URL='http://SERVER_IP:4096'
|
|||
|
|
export CACT_USER='opencode'
|
|||
|
|
export CACT_PASS='YOUR_SERVER_PASSWORD'
|
|||
|
|
|
|||
|
|
# Global health (many builds)
|
|||
|
|
curl -fsS -u "$CACT_USER:$CACT_PASS" "$CACT_URL/global/health"
|
|||
|
|
|
|||
|
|
# v2 location probe (desktop also uses this when health routes are odd)
|
|||
|
|
curl -fsS -u "$CACT_USER:$CACT_PASS" "$CACT_URL/api/location"
|
|||
|
|
|
|||
|
|
# OpenAPI explorer (optional)
|
|||
|
|
# open "$CACT_URL/doc"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Desktop health checking (`packages/app/src/utils/server-health.ts`) tries, in order:
|
|||
|
|
|
|||
|
|
1. `/api/health`
|
|||
|
|
2. `/global/health`
|
|||
|
|
3. Successful `/api/location` (treat as healthy when health routes fail for a non-network reason)
|
|||
|
|
|
|||
|
|
A 404 on one health path with a working `/api/location` can still show **online** in CloudAxe Desktop.
|
|||
|
|
|
|||
|
|
### 7.2 Agent inventory on the host
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
sudo -u opencode -H bash -lc 'opencode agent' # or the install's list-agents command
|
|||
|
|
# Confirm cact-orchestrator-tech-lead and other cact-* names appear
|
|||
|
|
ls -1 /home/opencode/.config/opencode/agents/cact-*.md
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 7.3 CloudAxe Desktop
|
|||
|
|
|
|||
|
|
1. Open **Settings → Servers** (or Add Server).
|
|||
|
|
2. URL: `http://SERVER_IP:4096` or your HTTPS proxy URL.
|
|||
|
|
3. Username / password: same as `OPENCODE_SERVER_*`.
|
|||
|
|
4. Save when health shows online.
|
|||
|
|
5. Start a session with agent **`cact-orchestrator-tech-lead`**.
|
|||
|
|
6. Confirm a trivial prompt returns (proves provider auth + model routing).
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 8. Upgrade
|
|||
|
|
|
|||
|
|
1. Snapshot:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
TS=$(date +%Y%m%d-%H%M%S)
|
|||
|
|
sudo -u opencode -H bash -lc "tar -C \$HOME -czf \$HOME/cact-backup-\$TS.tgz .config/opencode .local/share/opencode"
|
|||
|
|
sudo cp -a /home/opencode/bin/opencode "/home/opencode/bin/opencode.prev-$TS"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
2. Pull / rebuild / replace the binary at the pinned ref.
|
|||
|
|
3. `sudo systemctl restart opencode-cact.service`
|
|||
|
|
4. Re-run section 7 probes and a desktop session.
|
|||
|
|
5. Keep the previous binary and tarball until the new build is confirmed.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 9. Ops checklist (copy into the ticket)
|
|||
|
|
|
|||
|
|
- [ ] Service user created; OpenCode binary on PATH for that user
|
|||
|
|
- [ ] `opencode --version` recorded
|
|||
|
|
- [ ] `AGENTS.md` and all `cact-*.md` installed under `~/.config/opencode/`
|
|||
|
|
- [ ] `cact-documentation-agent` model switched off OpenRouter (or agent unused)
|
|||
|
|
- [ ] Provider logins present; `auth.json` mode `600`
|
|||
|
|
- [ ] `/etc/opencode/server.env` mode `640`, password set
|
|||
|
|
- [ ] `opencode-cact.service` enabled and active
|
|||
|
|
- [ ] Firewall / security group allowlists `4096` (or only `443` via Nginx)
|
|||
|
|
- [ ] `curl` health + `/api/location` succeed with basic auth
|
|||
|
|
- [ ] Desktop Add Server shows online
|
|||
|
|
- [ ] Session as `cact-orchestrator-tech-lead` succeeds
|
|||
|
|
- [ ] Backup tarball + previous binary path documented for rollback
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## References in this repo
|
|||
|
|
|
|||
|
|
- Serve command: `packages/opencode/src/cli/cmd/serve.ts`
|
|||
|
|
- Network flags: `packages/opencode/src/cli/network.ts`
|
|||
|
|
- Global path layout: `packages/core/src/global.ts`
|
|||
|
|
- Desktop health probes: `packages/app/src/utils/server-health.ts`
|
|||
|
|
- Upstream-style server docs: `packages/web/src/content/docs/server.mdx`
|
|||
|
|
- Container packaging of the CLI binary: `packages/opencode/Dockerfile`
|