feat: ship agent onboarding runtime
This commit is contained in:
60
deploy/vds/Caddyfile
Normal file
60
deploy/vds/Caddyfile
Normal 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
121
deploy/vds/README.md
Normal 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
|
||||
```
|
||||
55
deploy/vds/agents.env.example
Normal file
55
deploy/vds/agents.env.example
Normal 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
|
||||
76
deploy/vds/zopu-agents.service
Normal file
76
deploy/vds/zopu-agents.service
Normal 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
|
||||
Reference in New Issue
Block a user