Deployment — agent.spboucher.ai on MacLustr
Agent OS is deployed like every other MacLustr app: a manifest on the gateway (M1M32), mld picks /
syncs / builds / starts with PM2, and the MacLustr Tunnel (Caddy on BHS64) publishes the domain.
Nothing here is specific to a node except the pin (default M4M64c).
| Item | Value |
|---|---|
| mld app | agent-os |
| Domain / port | agent.spboucher.ai → <node>:8640 (health GET /api/v1/health) |
| App dir on the node | ~/apps/agent-os (a git clone once adopted — remote-first) |
| Data dir | ~/agent-os-data (setup token, artifacts under artifacts/, per-run temp dirs) |
| Workspace root | ~/agent-workspace (one directory per project) |
| PostgreSQL 17 (local) | role and database agent_os |
| PM2 | agent-os-server (apps/server/dist/main.js), agent-os-worker (apps/worker/dist/main.js, kill_timeout 20000), agent-os-backup (cron 04:05, deploy/backup.sh) |
| Secrets | ~/apps/agent-os/.env (0600): DATABASE_URL, AOS_SECRET_KEY (from the manifest) + provider keys appended from ~/.config/agent-os/keys.env on the node |
Where each secret lives
- Database password,
AOS_SECRET_KEY: generated bydeploy/make-manifest.pyon the gateway, reused from the existing manifest on later runs, written to.env(0600) on the node by thepost_synchook. They are in the manifest (M1M32:~/dispatch/apps/agent-os.json, keep it 0600) and nowhere else. The PM2 environment holds no secret. - Provider keys (
ANTHROPIC_API_KEY,OPENAI_API_KEY): never pass through the gateway or the manifest. The operator creates~/.config/agent-os/keys.env(0600) on the node;post_syncappends it to.env. Changing a key = edit that file, redeploy (or append to.envby hand and restart both processes). - How the processes get them: PM2 sets
AOS_ENV_FILE=~/apps/agent-os/.env;apps/server/src/main.tsandapps/worker/src/main.tsload that file at start (env-file.ts:KEY=VALUE,#comments, optional quotes; a variable already set in the environment wins; no other file is ever read). - The sandboxed shell cannot read
.env:~/appsis inAOS_SANDBOX_EXTRA_DENY_READ, and~/.configis denied by the built-in profile.
Environment (names actually read by the code)
| Variable | Read by | Value in production |
|---|---|---|
NODE_ENV |
server | production (enforces https origin, DATABASE_URL, 64-hex AOS_SECRET_KEY) |
DATABASE_URL |
server, worker, kernel, migrate-cli |
.env |
AOS_SECRET_KEY |
server | .env (64 hex; AES-256-GCM key of TOTP secrets) |
AOS_DATA_DIR |
server, worker (must be absolute), kernel | /Users/…/agent-os-data |
AOS_WORKSPACE_ROOT |
server, worker, engine | /Users/…/agent-workspace |
AOS_PORT, AOS_HOST |
server | 8640, 0.0.0.0 (reached by the tunnel over wg1) |
AOS_PUBLIC_ORIGIN |
server | https://agent.spboucher.ai |
AOS_TRUSTED_PROXIES |
server | 127.0.0.1,::1,10.67.0.1 |
AOS_WEB_DIR |
server | ~/apps/agent-os/apps/web/out |
AOS_MAX_CONCURRENT_RUNS |
worker | 4 |
AOS_WORKER_ID |
worker | unset (hostname-pid) |
AOS_SANDBOX_EXTRA_DENY_READ |
engine (worker) | ~/apps,~/backups,~/Desktop,~/Documents,~/Downloads,~/dispatch,~/srv as absolute paths |
AOS_ENV_FILE |
server, worker, backup.sh |
~/apps/agent-os/.env |
AOS_LOG_LEVEL |
all | info |
ANTHROPIC_API_KEY, OPENAI_API_KEY |
providers (worker); server records presence only | .env, from keys.env |
AOS_MIGRATIONS_DIR |
kernel migrate |
unset (found by walking up to db/migrations) |
Audit result (2026-11-10): every name listed in docs/CONTRACT.md §1 is read under that name; no renamed
or dead name. Read but not listed there: AOS_SANDBOX_EXTRA_DENY_READ, AOS_MIGRATIONS_DIR,
AOS_ENV_FILE, NODE_ENV. Defaults differ between processes when a variable is unset (AOS_DATA_DIR:
server ~/agent-os-data, kernel $TMPDIR/aos-data, worker refuses to start; AOS_WORKSPACE_ROOT:
server ~/workspace) — the manifest sets both explicitly for both processes.
First deployment
# 0. DNS (GoDaddy): A agent.spboucher.ai → 51.161.112.61
# 1. On the node (M4M64c): provider keys, never through the gateway
ssh M4M64c 'mkdir -p ~/.config/agent-os && chmod 700 ~/.config/agent-os && umask 077 && cat > ~/.config/agent-os/keys.env' <<'EOF'
ANTHROPIC_API_KEY=…
OPENAI_API_KEY=…
EOF
# 2. Manifest, generated ON the gateway (secrets are created / reused there)
scp deploy/make-manifest.py M1M32:/tmp/agent-os-make-manifest.py
ssh M1M32 'python3 /tmp/agent-os-make-manifest.py --node M4M64c > ~/dispatch/apps/agent-os.json.new \
&& mv ~/dispatch/apps/agent-os.json.new ~/dispatch/apps/agent-os.json && chmod 600 ~/dispatch/apps/agent-os.json'
# (write to .new first: a direct "> agent-os.json" would truncate the manifest before its secrets are read)
# 3. Stage and deploy
~/Desktop/cluster-skill/mld stage ~/Desktop/Cluster/projets/agent-spboucher-ai agent-os
~/Desktop/cluster-skill/mld deploy agent-os --node M4M64cpost_sync on the node: creates role / database agent_os (only when a new database password was
generated), writes .env, appends keys.env, runs
pnpm install --frozen-lockfile && pnpm -r --workspace-concurrency=1 build, checks the four build
outputs, then node packages/kernel/dist/migrate-cli.js (a failed migration fails the deploy). mld
then starts the PM2 processes, checks health locally, routes https://agent.spboucher.ai to the node and
checks health publicly.
Then follow docs/operations.md → "First run".
Later deployments
The node's ~/apps/agent-os is the source of truth (remote-first): change, build and commit there, or
push to spbgit and git pull on the node, then pnpm -r --workspace-concurrency=1 build,
node packages/kernel/dist/migrate-cli.js, pm2 restart agent-os-worker agent-os-server.
mld deploy agent-os from a staging copy is refused when the node's clone has diverged
(mld fetch agent-os --from <node> first).
Restarting the worker is safe at any time: it stops claiming, in-flight runs stay running with a
lease that the new process takes over (≤ 90 s). Restarting the server only drops WebSockets; the
console reconnects and resumes from its cursor.
Not verified by a real deployment
The manifest generator was run locally only (fresh and against a stubbed existing manifest: valid JSON,
secrets reused, role/db creation only with a new password, no provider key anywhere). mld deploy, the
build on the node, PM2 kill_timeout, the tunnel route and backup.sh / restore.sh against a real
database have not been exercised yet.