SPB Git

Repository Public

deployment.md 6.6 KB

# 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 by deploy/make-manifest.py on the gateway, reused from the existing manifest on later runs, written to .env (0600) on the node by the post_sync hook. 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_sync appends it to .env. Changing a key = edit that file, redeploy (or append to .env by hand and restart both processes).
  • How the processes get them: PM2 sets AOS_ENV_FILE=~/apps/agent-os/.env; apps/server/src/main.ts and apps/worker/src/main.ts load 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: ~/apps is in AOS_SANDBOX_EXTRA_DENY_READ, and ~/.config is 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

bash
# 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 M4M64c

post_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.

Files main
Menu

Theme