# zopu-agents.service — single Zopu intelligence runtime for the VDS. # # One process hosts both the Hono/Flue server and the AgentOS registry running # in native envoy mode (Rivet Mode A): # # 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 envoy/native mode (registry.startAndWait) # # The AgentOS actor is hosted in-process: the Hono/Flue process is also the # envoy that connects outbound to the Rivet Engine. There is no separate # runner process and no Rivet HTTP handler route. # # 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). # RIVET_ENDPOINT is the engine control plane (http://127.0.0.1:6420). # # The release is a self-contained bundle under /srv/zopu/releases/ 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 (Hono + Flue + in-process envoy) # 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