feat: ship agent onboarding runtime

This commit is contained in:
-Puter
2026-08-04 01:12:17 +05:30
parent 48205a5e19
commit b02200b3dc
68 changed files with 10522 additions and 4156 deletions

60
deploy/vds/Caddyfile Normal file
View File

@@ -0,0 +1,60 @@
# Caddy reverse proxy for the Zopu VDS staging agent stack.
#
# This is the ONLY public ingress to the single agents Node process (see
# docs/DEPLOYMENT_PLAN.md §"Public surface"). It terminates TLS for
# agents.zopu.puter.wtf — the Flue callback hostname Convex reaches — then
# forwards ONLY the documented worker paths to the internal `agents` service at
# 127.0.0.1:3000.
#
# Hard rules this file enforces:
# - No generic /api/* proxy and no browser-to-Flue traffic (DEPLOYMENT_PLAN.md
# line 86). Browsers talk only to Convex.
# - Only the paths the Convex backend actually calls are routed:
# /health liveness probe
# /internal/project-setup single-call project setup coordinator
# /internal/agents/<agentId>/events agent event dispatch callback
# - The RivetKit serverless registry at /internal/rivet/* is NOT proxied. It
# is a private loopback callback the Engine reaches directly at
# http://127.0.0.1:3000/internal/rivet; exposing it publicly would widen the
# private worker protocol.
# - Former routes (/agents/zopu/*, /internal/work-attempts/*, /workflows/*,
# /api/rivet/*) are intentionally absent: they are stale and not part of the
# current Convex→agents contract.
# - Everything else returns 404. The private worker protocol stays narrow.
#
# TLS certs persist in Caddy's data directory. Caddy serves its own ACME
# HTTP-01 challenge responses on :80 automatically, so the :80 block only
# redirects everything else to HTTPS — worker traffic is never served over
# plain HTTP.
#
# Install/update: see the "Caddy" section of deploy/vds/README.md. In short,
# copy this file to /etc/caddy/Caddyfile, then:
# sudo caddy validate --config /etc/caddy/Caddyfile
# sudo systemctl reload caddy
agents.zopu.puter.wtf {
encode zstd gzip
# Explicit route block pins directive order: path-matched proxies run
# first, then the terminal 404 fallback. No directive is left to the
# default sort.
route {
# Liveness probe (CI healthcheck). Routed to the real worker so a failed
# process is not masked by a synthetic success.
reverse_proxy /health 127.0.0.1:3000
# Single-call project setup coordinator. Requires the internalRoute bearer.
reverse_proxy /internal/project-setup 127.0.0.1:3000
# Agent event dispatch callback. Requires the internalRoute bearer.
reverse_proxy /internal/agents/* 127.0.0.1:3000
# Everything else is not part of the private worker protocol.
respond 404
}
}
# HTTP → HTTPS redirect (Caddy still answers ACME challenges on :80).
:80 {
redir https://{host}{uri} permanent
}

121
deploy/vds/README.md Normal file
View File

@@ -0,0 +1,121 @@
# Zopu VDS agents deployment
This directory holds the systemd unit, environment template, and Caddy reverse-proxy config that run the single Zopu agents Node process on the VDS. It supersedes the former container deployment for the agents process.
## Runtime topology
One Node process (`zopu-agents.service`) runs the whole intelligence runtime:
```
zopu-agents.service
└─ node /srv/zopu/current/packages/agents/dist/server.mjs
├─ Hono server 127.0.0.1:3000
├─ Flue 2.0 agents SQLite at /srv/zopu/data/flue/flue.db
└─ AgentOS registry in-process, serverless, at /internal/rivet/*
```
The **Rivet Engine is a separate private service** on `127.0.0.1:6420`, managed by its own unit/compose. This unit does not start it. There is **no standalone AgentOS runner**: the registry handler runs inside the agents process.
### The two Rivet endpoints (not interchangeable)
| Variable | Role | Value on this host |
| --- | --- | --- |
| `RIVET_ENDPOINT` | Engine **control plane** this process connects _to_ for actor metadata | `http://127.0.0.1:6420` |
| `RIVET_SERVERLESS_ENDPOINT` | Address the engine calls **back into** this process's `/internal/rivet` handler | `http://127.0.0.1:3000/internal/rivet` |
Pointing both at the engine, or dropping the `/internal/rivet` path, breaks the registry. See `agents.env.example` for the authoritative comments.
## Files
- `zopu-agents.service` — systemd unit. Runs `/srv/zopu/current/.../server.mjs` as user `zopu`, waits for engine health before binding, hardens the service, and restarts on failure.
- `agents.env.example` — template for `/etc/zopu/agents.env`. Contains the full validated env schema with no secrets.
- `../../scripts/release-agents.sh` — local build, upload, and atomic release promotion command. Each release includes this directory so the unit's `Documentation=` target remains valid.
- `Caddyfile` — source-controlled public ingress for `agents.zopu.puter.wtf`. Terminates TLS and forwards only `/health`, `/internal/project-setup`, and `/internal/agents/*` to `127.0.0.1:3000`; the private `/internal/rivet/*` registry handler is never proxied.
## Prerequisites on the VDS
1. **Node 24** installed at `/opt/zopu/node/bin/node` (the verified VDS Node; the unit's `ExecStart` and `PATH` are anchored on this path). The release bundle ships its own `node_modules`; only the Node binary is expected from the host.
2. **`curl`** and **`bash`** on `PATH` (used by the `ExecStartPre` engine health probe).
3. A dedicated service user and group:
```sh
sudo useradd --system --no-create-home --shell /usr/sbin/nologin zopu
```
4. The two writable state trees, owned by `zopu`:
```sh
sudo mkdir -p /srv/zopu/data/flue /srv/zopu/workspaces
sudo chown -R zopu:zopu /srv/zopu/data /srv/zopu/workspaces
```
5. The **Rivet Engine** running and answering `http://127.0.0.1:6420/health`.
6. A release promoted by `scripts/release-agents.sh` to `/srv/zopu/releases/<release-id>`, with `/srv/zopu/current` symlinked to it. The release must contain `packages/agents/dist/server.mjs` and `deploy/vds/`.
## Install
```sh
# 1. Secrets
sudo install -d -m 0750 -o root -g zopu /etc/zopu
sudo cp agents.env.example /etc/zopu/agents.env
sudo chown root:zopu /etc/zopu/agents.env
sudo chmod 0600 /etc/zopu/agents.env
sudo "$EDITOR" /etc/zopu/agents.env # fill in every REPLACE-WITH-...
# 2. Unit
sudo install -m 0644 zopu-agents.service /etc/systemd/system/zopu-agents.service
sudo systemctl daemon-reload
sudo systemctl enable --now zopu-agents.service
```
## Verify
```sh
systemctl status zopu-agents.service
curl -fsS http://127.0.0.1:3000/health # {"service":"zopu-agents","status":"ok"}
```
### Public ingress (Caddy)
Caddy terminates TLS for `agents.zopu.puter.wtf` and forwards only the three paths the Convex backend calls to the internal agents worker (`127.0.0.1:3000`). The in-process serverless registry at `/internal/rivet/*` is reachable only over loopback (the Engine's `RIVET_SERVERLESS_ENDPOINT` callback), never through Caddy. All other paths return `404`.
Install or update from this checked-in artifact:
```sh
# 1. Materialize the config
sudo install -m 0644 Caddyfile /etc/caddy/Caddyfile
# 2. Validate before applying
sudo caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile
# 3. Reload the running Caddy (no restart needed)
sudo systemctl reload caddy
```
Verify the narrow public surface:
```sh
curl -fsS https://agents.zopu.puter.wtf/health # {"service":"zopu-agents","status":"ok"}
curl -i https://agents.zopu.puter.wtf/internal/rivet/anything # 404 — registry stays private
```
## Release switch
From the repository root on the build host, create and promote a release:
```sh
scripts/release-agents.sh <vds-host> <release-id>
```
The script atomically swaps `/srv/zopu/current`; it deliberately does **not** restart the service. After the upload succeeds, restart the single agents unit:
```sh
sudo systemctl restart zopu-agents.service
```
Roll back by atomically replacing `current` with a symlink to a prior release, then restarting the same service.
## Optional: explicit engine ordering
The unit already gates startup on the engine health endpoint via `ExecStartPre`. If the engine is itself a named systemd unit on this host, you may add a hard ordering by uncommenting/adding in the `[Unit]` section:
```ini
After=rivet-engine.service
Wants=rivet-engine.service
```

View File

@@ -0,0 +1,55 @@
# /etc/zopu/agents.env — runtime environment for zopu-agents.service
#
# Install at /etc/zopu/agents.env, owner root:zopu, mode 0600. Fill in every
# REPLACE-WITH-... value; the process validates the full schema at start and
# will refuse to boot if a required key is missing or malformed. This file is
# the single source of secrets for the VDS agents process — never commit a
# filled-in copy.
# --- Process listener --------------------------------------------------------
# Binds the private loopback only. Caddy (or an equivalent reverse proxy)
# terminates TLS upstream and forwards to 127.0.0.1:3000.
HOST=127.0.0.1
PORT=3000
NODE_ENV=production
# --- Rivet Engine + in-process registry (two distinct endpoints) -------------
#
# RIVET_ENDPOINT is the Engine CONTROL PLANE this process connects TO for actor
# metadata/management. The engine is a separate private service on this host.
RIVET_ENDPOINT=http://127.0.0.1:6420
#
# RIVET_SERVERLESS_ENDPOINT is the address the Engine calls BACK INTO this same
# process to reach the in-process serverless AgentOS registry handler, which is
# mounted at /internal/rivet/*. It must resolve to this process's own listener
# plus that base path. Do NOT point this at the engine; do NOT drop the path.
RIVET_SERVERLESS_ENDPOINT=http://127.0.0.1:3000/internal/rivet
#
# Shared secret authenticating actor connections between the engine and this
# registry's onBeforeConnect. Must match the value configured on the engine.
RIVET_WORKSPACE_TOKEN=REPLACE-WITH-LONG-RANDOM-WORKSPACE-TOKEN
# --- Flue SQLite persistence -------------------------------------------------
# Canonical conversation state. Parent dir must be one of the unit's
# ReadWritePaths (/srv/zopu/data).
FLUE_DB_PATH=/srv/zopu/data/flue/flue.db
# Bearer token shared with Convex for /internal/* service calls.
FLUE_DB_TOKEN=REPLACE-WITH-LONG-RANDOM-SERVICE-TOKEN
# --- Project workspaces ------------------------------------------------------
# Root for project-bound AgentOS VMs and generated artifacts. Must be one of
# the unit's ReadWritePaths (/srv/zopu/workspaces).
AGENT_WORKSPACE_ROOT=/srv/zopu/workspaces
# --- Convex control plane ----------------------------------------------------
CONVEX_URL=REPLACE-WITH-DEPLOYMENT.convex.cloud
CONVEX_SITE_URL=REPLACE-WITH-DEPLOYMENT.convex.site
# --- Agent model provider (OpenAI-compatible) --------------------------------
AGENT_MODEL_PROVIDER=cheaptricks
AGENT_MODEL_API=openai-completions
AGENT_MODEL_NAME=mimo-v2.5
AGENT_MODEL_BASE_URL=https://ai.example.com/v1
AGENT_MODEL_API_KEY=REPLACE-WITH-PROVIDER-API-KEY
AGENT_MODEL_CONTEXT_WINDOW=1048576
AGENT_MODEL_MAX_TOKENS=131072

View File

@@ -0,0 +1,76 @@
# zopu-agents.service — single Node intelligence runtime for the Zopu VDS.
#
# Runtime shape (one process, no separate runner):
#
# zopu-agents (this unit)
# └─ node /srv/zopu/current/packages/agents/dist/server.mjs
# ├─ Hono HTTP server on 127.0.0.1:3000
# ├─ Flue 2.0 conversation agents + SQLite at /srv/zopu/data/flue
# └─ AgentOS registry in serverless mode, served in-process at
# /internal/rivet/* (registry.handler drives the per-request
# runtime; there is no standalone AgentOS runner process).
#
# The Rivet Engine is a SEPARATE private service on 127.0.0.1:6420, managed
# by its own unit/compose. This unit does not start or own it; it only waits
# for the engine health endpoint before binding (ExecStartPre below).
#
# Two distinct Rivet endpoints (see agents.env):
# RIVET_ENDPOINT — engine control plane the registry connects to
# (http://127.0.0.1:6420). Used for actor metadata.
# RIVET_SERVERLESS_ENDPOINT — address the engine calls BACK into this same
# process's /internal/rivet handler
# (http://127.0.0.1:3000/internal/rivet).
# These are NOT interchangeable; do not point both at the engine.
#
# The release is a self-contained bundle under /srv/zopu/releases/<id> with
# /srv/zopu/current as the live symlink. This unit always runs `current`; the
# release script swaps it atomically, then the operator restarts this service.
[Unit]
Description=Zopu agents intelligence runtime (Flue + in-process AgentOS registry)
# Soft network ordering. The real gate on the separate Engine is the health
# probe in ExecStartPre. If the engine is itself a named systemd unit on this
# host, add an explicit ordering here, e.g.:
# After=rivet-engine.service
# Wants=rivet-engine.service
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=zopu
Group=zopu
WorkingDirectory=/srv/zopu/current
# Secrets and runtime config live in /etc/zopu/agents.env (chmod 600, owner
# root:zopu). See agents.env.example for the full schema; never commit real
# values. NODE_ENV is pinned here so a release can never accidentally start in
# development mode.
Environment=NODE_ENV=production
EnvironmentFile=/etc/zopu/agents.env
# Wait for the separate Rivet Engine control plane to report healthy before
# binding. The engine is NOT managed by this unit; this probe is the startup
# dependency on its health. Bounded to ~60s.
ExecStartPre=/usr/bin/env bash -c 'i=0; until curl -fsS -o /dev/null http://127.0.0.1:6420/health; do i=$$((i+1)); [ "$$i" -ge 60 ] && { echo "rivet engine not healthy at http://127.0.0.1:6420/health" >&2; exit 1; }; sleep 1; done'
# Built release artifact. Node is the verified VDS install at
# /opt/zopu/node/bin/node (Node 24). PATH is anchored on that directory so the
# Node binary and its bundled toolchain (corepack/pnpm) resolve deterministically.
Environment=PATH=/opt/zopu/node/bin:/usr/local/bin:/usr/bin:/bin
ExecStart=/opt/zopu/node/bin/node /srv/zopu/current/packages/agents/dist/server.mjs
Restart=always
RestartSec=5
# --- Hardening ---------------------------------------------------------------
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
# Writable state: Flue SQLite + project workspaces. The release tree under
# /srv/zopu/current is treated read-only at runtime.
ReadWritePaths=/srv/zopu/workspaces /srv/zopu/data
[Install]
WantedBy=multi-user.target