feat: deploy zopu single-node production

This commit is contained in:
sai karthik
2026-07-26 11:39:21 +05:30
parent 2a0487aa6e
commit 39b27a229f
15 changed files with 320 additions and 121 deletions

View File

@@ -140,6 +140,11 @@ export const MobileFlowPage = ({ screen }: MobileFlowPageProps) => {
onOpenUnit={(selectedWorkUnitId) => onOpenUnit={(selectedWorkUnitId) =>
navigate(`/chat/${selectedWorkUnitId}`) navigate(`/chat/${selectedWorkUnitId}`)
} }
onProjectSelect={(projectId) =>
projectWorkspace.projectWorkspace.setSelectedProjectId(
projectId as Id<"projects">
)
}
onRestoreChecked={handleRestoreChecked} onRestoreChecked={handleRestoreChecked}
onSettingsClose={() => workspace.setSettingsOpen(false)} onSettingsClose={() => workspace.setSettingsOpen(false)}
onSettingsOpen={() => workspace.setSettingsOpen(true)} onSettingsOpen={() => workspace.setSettingsOpen(true)}

View File

@@ -8,11 +8,20 @@
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 1. Convex (control plane) — REQUIRED # 1. Convex (control plane) — REQUIRED
# Convex is already deployed; provide the production deployment URL. # Public URLs for the self-hosted Convex deployment.
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
CONVEX_URL=https://your-deployment.convex.cloud CONVEX_URL=https://your-deployment.convex.cloud
CONVEX_SITE_URL=https://your-deployment.convex.site CONVEX_SITE_URL=https://your-deployment.convex.site
SITE_URL=http://localhost:5173 SITE_URL=http://localhost:5173
VITE_CONVEX_URL=https://your-deployment.convex.cloud
VITE_CONVEX_SITE_URL=https://your-deployment.convex.site
VITE_FLUE_URL=http://localhost:3583
# Self-hosted Convex origins used by convex/docker-compose.yml
CONVEX_CLOUD_ORIGIN=https://your-deployment.convex.cloud
CONVEX_SITE_ORIGIN=https://your-deployment.convex.site
CONVEX_INSTANCE_NAME=zopu-production
CONVEX_INSTANCE_SECRET=
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 2. Self-hosted Git / Gitea — REQUIRED for issue lifecycle # 2. Self-hosted Git / Gitea — REQUIRED for issue lifecycle
@@ -46,16 +55,15 @@ AGENT_MODEL_MAX_TOKENS=131072
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 5. Zopu agent service (Flue) # 5. Zopu agent service (Flue)
# FLUE_DB_TOKEN authenticates the Flue persistence adapter. # FLUE_DB_TOKEN authenticates the Flue persistence adapter.
# PORT is the Flue Node HTTP server listen port. # zopu-agent.service pins the Flue Node server to port 3583.
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
FLUE_DB_TOKEN=replace-with-long-random-token FLUE_DB_TOKEN=replace-with-long-random-token
PORT=3583
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 6. Daemon identity # 6. Daemon identity
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
DAEMON_ID=zopu-dedicated DAEMON_ID=zopu-dedicated
DAEMON_NAME=Zopu Dedicated Server DAEMON_NAME=Zopu-Dedicated-Server
DAEMON_VERSION=0.0.0 DAEMON_VERSION=0.0.0
DAEMON_HEARTBEAT_MS=15000 DAEMON_HEARTBEAT_MS=15000
DAEMON_COMMAND_LEASE_MS=60000 DAEMON_COMMAND_LEASE_MS=60000

View File

@@ -1,9 +1,6 @@
# Zopu Single-Node Runtime Deployment # Zopu Single-Node Runtime Deployment
Deployment artifacts for the Zopu execution plane on a single Debian dedicated Deployment artifacts for the complete Zopu stack on a single Debian dedicated server: the React Router web app, a persistent self-hosted Convex control plane, the daemon (Bun/Effect + AgentOS/RivetKit), the Flue agent service, Docker Engine, and supporting infrastructure.
server. Convex is already deployed as the control plane; this lane deploys only
the execution plane: the daemon (Bun/Effect + AgentOS/RivetKit), the Flue agent
service, Docker Engine for future Orb sandboxes, and supporting infrastructure.
## Architecture ## Architecture
@@ -13,23 +10,17 @@ service, Docker Engine for future Orb sandboxes, and supporting infrastructure.
│ ~12 CPU cores · ~40 GB RAM · single-node │ │ ~12 CPU cores · ~40 GB RAM · single-node │
│ │ │ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ zopu-daemon │ │ zopu-agent │ │ Docker │ │ │ │ zopu-web │ │ zopu-agent │ │ Docker │ │
│ │ (systemd) │ │ (systemd) │ │ Engine │ │ │ │ (systemd) │ │ (systemd) │ │ Engine │ │
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │
│ │ Effect daemon│ │ Flue Node │ │ Orb sandboxes│ │ │ │ React Router │ │ Flue Node 22 │ │ Convex │ │
│ │ + RivetKit │ │ server.mjs │ │ (future) │ │ │ │ :13100 │ │ server.mjs │ │ backend │ │
│ │ in-process │ │ :3583 │ │ │ │ │ │ │ │ :3583 │ │ :3210/:3211 │ │
│ │ engine │ │ │ │ │ │
│ │ + native │ │ │ │ │ │
│ │ sidecar │ │ │ │ │ │
│ │ :6420 │ │ │ │ │ │
│ └──────┬───────┘ └──────┬───────┘ └──────────────┘ │ │ └──────┬───────┘ └──────┬───────┘ └──────────────┘ │
│ │ │ │ │ │ │ │
└─────────────────── ─────────────
│ zopu-daemon │ Bun + Effect + RivetKit :6420
────────▼───────── ──────────────
│ │ Convex (cloud) │ ← control plane (pre-deployed)│
│ └──────────────────┘ │
│ │ │ │
│ systemd timers: health-check (60s), docker-cleanup (daily) │ │ systemd timers: health-check (60s), docker-cleanup (daily) │
│ cron: disk-monitor (daily 06:00) │ │ cron: disk-monitor (daily 06:00) │
@@ -40,31 +31,15 @@ service, Docker Engine for future Orb sandboxes, and supporting infrastructure.
### Single-node RivetKit topology ### Single-node RivetKit topology
The daemon calls `registry.start()` from `rivetkit`, which boots an The daemon calls `registry.start()` from `rivetkit`, which boots an **in-process RivetKit engine** (envoy mode) backed by a **native Rust sidecar** binary (`@rivet-dev/agentos-sidecar`, platform-resolved). The engine listens on `RIVET_ENDPOINT` (default `http://localhost:6420`). The daemon then calls `createClient(RIVET_ENDPOINT)` to connect back to its own in-process engine for actor dispatch.
**in-process RivetKit engine** (envoy mode) backed by a **native Rust
sidecar** binary (`@rivet-dev/agentos-sidecar`, platform-resolved). The engine
listens on `RIVET_ENDPOINT` (default `http://localhost:6420`). The daemon then
calls `createClient(RIVET_ENDPOINT)` to connect back to its own in-process
engine for actor dispatch.
Evidence: RivetKit source `chunk-YDUQHING.js` line 4751 — Evidence: RivetKit source `chunk-YDUQHING.js` line 4751 — `DEFAULT_ENDPOINT = "http://localhost:6420"`. The `Registry.start()` method calls `#startEnvoy()``runtime.serveRegistry()` for serverful mode (Mode A). The `createClient()` function reads `RIVET_ENDPOINT` env or defaults to the same `http://localhost:6420`.
`DEFAULT_ENDPOINT = "http://localhost:6420"`. The `Registry.start()` method
calls `#startEnvoy()``runtime.serveRegistry()` for serverful mode (Mode A).
The `createClient()` function reads `RIVET_ENDPOINT` env or defaults to the
same `http://localhost:6420`.
**No separate Rivet Engine process is required.** The engine, actor envoy, and **No separate Rivet Engine process is required.** The engine, actor envoy, and sidecar all run inside the daemon process. A future multi-node deployment would externalize the engine, but that is out of scope.
sidecar all run inside the daemon process. A future multi-node deployment
would externalize the engine, but that is out of scope.
### Docker / Orb boundary ### Docker / Orb boundary
Docker Engine is installed and the `zopu` service user is in the `docker` Docker Engine is installed and the `zopu` service user is in the `docker` group. The daemon's systemd unit includes `SupplementaryGroups=docker`. However, **no Docker-backed sandbox code is currently wired**. The Orb sandbox lane contract has not landed; Docker access is provisioned now so the boundary is ready. The current agent uses the in-process AgentOS VM (Wasm/V8) sandbox, not Docker.
group. The daemon's systemd unit includes `SupplementaryGroups=docker`.
However, **no Docker-backed sandbox code is currently wired**. The Orb
sandbox lane contract has not landed; Docker access is provisioned now so the
boundary is ready. The current agent uses the in-process AgentOS VM (Wasm/V8)
sandbox, not Docker.
## Files ## Files
@@ -76,6 +51,7 @@ deploy/zopu-runtime/
├── systemd/ ├── systemd/
│ ├── zopu-daemon.service # Daemon systemd unit │ ├── zopu-daemon.service # Daemon systemd unit
│ ├── zopu-agent.service # Agent systemd unit │ ├── zopu-agent.service # Agent systemd unit
│ ├── zopu-web.service # React Router web systemd unit
│ ├── zopu-health.service # Health check oneshot │ ├── zopu-health.service # Health check oneshot
│ ├── zopu-health.timer # Health check every 60s │ ├── zopu-health.timer # Health check every 60s
│ ├── zopu-docker-cleanup.service │ ├── zopu-docker-cleanup.service
@@ -108,7 +84,7 @@ bash bootstrap.sh
nano /opt/zopu/.env nano /opt/zopu/.env
# 5. Start services: # 5. Start services:
systemctl start zopu-daemon systemctl start zopu-web zopu-daemon
sleep 3 sleep 3
systemctl start zopu-agent systemctl start zopu-agent
@@ -122,20 +98,20 @@ systemctl enable --now zopu-health.timer zopu-docker-cleanup.timer
## Start / stop / restart ## Start / stop / restart
```bash ```bash
# Start both services # Start all services
systemctl start zopu-daemon zopu-agent systemctl start zopu-web zopu-daemon zopu-agent
# Stop both services # Stop all services
systemctl stop zopu-agent zopu-daemon systemctl stop zopu-agent zopu-daemon zopu-web
# Restart (daemon first — it owns the RivetKit engine) # Restart (daemon first — it owns the RivetKit engine)
systemctl restart zopu-daemon && sleep 3 && systemctl restart zopu-agent systemctl restart zopu-web zopu-daemon && sleep 3 && systemctl restart zopu-agent
# Enable on boot # Enable on boot
systemctl enable zopu-daemon zopu-agent systemctl enable zopu-web zopu-daemon zopu-agent
# Disable on boot # Disable on boot
systemctl disable zopu-daemon zopu-agent systemctl disable zopu-web zopu-daemon zopu-agent
``` ```
## Log inspection ## Log inspection
@@ -183,16 +159,14 @@ systemctl list-timers zopu-health.timer
``` ```
The health check probes: The health check probes:
1. `zopu-daemon` systemd unit is active 1. `zopu-daemon` systemd unit is active
2. `zopu-agent` systemd unit is active 2. `zopu-agent` systemd unit is active
3. RivetKit engine port (default 6420) accepts TCP connections 3. RivetKit engine port (default 6420) accepts TCP connections
4. Flue agent port (default 3583) accepts TCP connections 4. Flue agent port (default 3583) accepts TCP connections
5. Docker daemon responds to `docker info` 5. Docker daemon responds to `docker info`
No HTTP health endpoints are assumed. Flue does not expose one by design No HTTP health endpoints are assumed. Flue does not expose one by design (per Flue docs: "Flue does not add a health endpoint"). RivetKit's health route is internal to the registry runtime and not documented as publicly addressable on the engine endpoint.
(per Flue docs: "Flue does not add a health endpoint"). RivetKit's health
route is internal to the registry runtime and not documented as
publicly addressable on the engine endpoint.
## Update to commit ## Update to commit
@@ -208,6 +182,7 @@ publicly addressable on the engine endpoint.
``` ```
The update script: The update script:
1. Records current HEAD to `.last-deployed-sha` 1. Records current HEAD to `.last-deployed-sha`
2. Fetches, resolves branch-or-commit, checks out 2. Fetches, resolves branch-or-commit, checks out
3. `bun install`, builds daemon and agent 3. `bun install`, builds daemon and agent
@@ -224,9 +199,7 @@ The update script:
/opt/zopu/deploy/zopu-runtime/scripts/rollback.sh abc123def456 /opt/zopu/deploy/zopu-runtime/scripts/rollback.sh abc123def456
``` ```
Rollback reads `.last-deployed-sha` (written by `update.sh`), checks out that Rollback reads `.last-deployed-sha` (written by `update.sh`), checks out that commit, rebuilds, and restarts services. The pre-rollback SHA is saved to `.pre-rollback-sha` for re-rollback if needed.
commit, rebuilds, and restarts services. The pre-rollback SHA is saved to
`.pre-rollback-sha` for re-rollback if needed.
## Docker cleanup ## Docker cleanup
@@ -242,11 +215,11 @@ systemctl list-timers zopu-docker-cleanup.timer
``` ```
Cleanup prunes: Cleanup prunes:
- Stopped containers older than 24 hours - Stopped containers older than 24 hours
- Dangling (untagged) images - Dangling (untagged) images
- Unused networks - Unused networks
Named volumes and running containers are never removed. Named volumes and running containers are never removed.
## Disk-space monitoring ## Disk-space monitoring
@@ -259,69 +232,71 @@ Named volumes and running containers are never removed.
/opt/zopu/deploy/zopu-runtime/scripts/disk-monitor.sh --warn-percent 90 /opt/zopu/deploy/zopu-runtime/scripts/disk-monitor.sh --warn-percent 90
``` ```
A cron job runs at 06:00 daily and writes to `/var/log/zopu/disk-monitor.log`. A cron job runs at 06:00 daily and writes to `/var/log/zopu/disk-monitor.log`. Default alert threshold is 80%.
Default alert threshold is 80%.
## Firewall and private networking ## Firewall and private networking
The firewall (`ufw`) is deny-by-default: The firewall (`ufw`) is deny-by-default:
- SSH (port 22) is allowed on all interfaces - SSH (port 22) is allowed on all interfaces
- All traffic on `tailscale0` is allowed (Tailscale private overlay) - All traffic on `tailscale0` is allowed (Tailscale private overlay)
- All other incoming traffic is denied - All other incoming traffic is denied
The RivetKit engine (`:6420`) and Flue agent (`:3583`) ports are **not** The RivetKit engine (`:6420`) and Flue agent (`:3583`) ports are **not** exposed on public interfaces. Reachability options:
exposed on public interfaces. Reachability options:
1. **Tailscale** (recommended): bootstrap runs `ufw allow in on tailscale0` 1. **Tailscale** (recommended): bootstrap runs `ufw allow in on tailscale0` so all ports are reachable over the private overlay. Set `TAILSCALE_AUTHKEY` before running bootstrap to configure automatically. Other Tailscale-connected machines can reach the agent at `http://zopu-runtime:3583` and the engine at `http://zopu-runtime:6420`.
so all ports are reachable over the private overlay. Set 2. **Custom private interface**: if you have a non-Tailscale private network (e.g. a VLAN or wireguard interface), add an explicit rule:
`TAILSCALE_AUTHKEY` before running bootstrap to configure automatically.
Other Tailscale-connected machines can reach the agent at
`http://zopu-runtime:3583` and the engine at `http://zopu-runtime:6420`.
2. **Custom private interface**: if you have a non-Tailscale private network
(e.g. a VLAN or wireguard interface), add an explicit rule:
```bash ```bash
ufw allow in on eth1 # or your private interface name ufw allow in on eth1 # or your private interface name
``` ```
Do NOT assume direct private IP access works by default — the deny-incoming Do NOT assume direct private IP access works by default — the deny-incoming policy blocks it until an interface-specific rule is added.
policy blocks it until an interface-specific rule is added. 3. **Caddy** (optional): install Caddy and use the annotated Caddyfile in `caddy/` if you need TLS termination or a public ingress point.
3. **Caddy** (optional): install Caddy and use the annotated Caddyfile in
`caddy/` if you need TLS termination or a public ingress point.
## Environment groups ## Environment groups
See [`.env.template`](./.env.template) for the full annotated template. The See [`.env.template`](./.env.template) for the full annotated template. The eight required groups:
eight required groups:
| Group | Variables | | Group | Variables |
|-------|-----------| | --- | --- |
| Convex | `CONVEX_URL`, `CONVEX_SITE_URL`, `SITE_URL` | | Convex | `CONVEX_URL`, `CONVEX_SITE_URL`, `SITE_URL` |
| Gitea | `GITEA_URL`, `GITEA_TOKEN` | | Gitea | `GITEA_URL`, `GITEA_TOKEN` |
| Model gateway | `AGENT_MODEL_*` | | Model gateway | `AGENT_MODEL_*` |
| AgentOS/RivetKit | `RIVET_ENDPOINT` (optional) | | AgentOS/RivetKit | `RIVET_ENDPOINT` (optional) |
| Zopu agent | `FLUE_DB_TOKEN`, `PORT` | | Zopu agent | `FLUE_DB_TOKEN` (`zopu-agent.service` sets port 3583) |
| Daemon | `DAEMON_ID`, `DAEMON_NAME`, `DAEMON_VERSION`, `DAEMON_HEARTBEAT_MS`, `DAEMON_COMMAND_LEASE_MS` | | Daemon | `DAEMON_ID`, `DAEMON_NAME`, `DAEMON_VERSION`, `DAEMON_HEARTBEAT_MS`, `DAEMON_COMMAND_LEASE_MS` |
| Docker sandbox | group membership (no env vars) | | Docker sandbox | group membership (no env vars) |
| Service auth | `AUTH_SECRET` (if needed) | | Service auth | `AUTH_SECRET` (if needed) |
## Public single-node routes
The checked-in Caddy example assumes Cloudflare Tunnel terminates TLS and sends the four Zopu hosts to Caddy on loopback:
- `zopu.sai-onchain.me` → React Router web app on `127.0.0.1:13100`
- `zopu-api.sai-onchain.me` → Convex API on `127.0.0.1:3210`
- `zopu-site.sai-onchain.me` → Convex HTTP actions on `127.0.0.1:3211`
- `zopu-agent.sai-onchain.me` → Flue on `127.0.0.1:3583`
Self-hosted Convex state lives in the `zopu-convex-data` Docker volume. Generate the CLI admin key after the backend is healthy:
```bash
docker compose \
--env-file /opt/zopu/.env \
-f /opt/zopu/deploy/zopu-runtime/convex/docker-compose.yml \
exec backend ./generate_admin_key.sh
```
## What is NOT deployed ## What is NOT deployed
- **Web app**: the web frontend is not deployed in this lane.
- **Kubernetes**: no container orchestration. - **Kubernetes**: no container orchestration.
- **PostgreSQL for Rivet**: the in-process RivetKit engine uses its own - **PostgreSQL for Rivet**: the in-process RivetKit engine uses its own storage; no external PostgreSQL is required.
storage; no external PostgreSQL is required.
- **Multi-node coordination**: single-node only. - **Multi-node coordination**: single-node only.
- **Public administration endpoints**: no admin HTTP surface. - **Public administration endpoints**: no admin HTTP surface.
- **Secrets in source**: `.env` is never committed; `.env.template` contains - **Secrets in source**: `.env` is never committed; `.env.template` contains only placeholder values.
only placeholder values. - **Docker-backed Orb sandboxes**: Docker is installed and access is provisioned, but no Orb sandbox code is wired. This is a boundary prepared for the Orb lane, not a working feature.
- **Production generated-app hosting**: only the execution plane runs here.
- **Docker-backed Orb sandboxes**: Docker is installed and access is
provisioned, but no Orb sandbox code is wired. This is a boundary
prepared for the Orb lane, not a working feature.
## Reproducibility ## Reproducibility
The deployment does not require the developer's MacBook to remain online. The deployment does not require the developer's MacBook to remain online. Once bootstrap completes and `.env` is filled in:
Once bootstrap completes and `.env` is filled in:
1. Services run under systemd with `Restart=always`. 1. Services run under systemd with `Restart=always`.
2. Logs persist in journald. 2. Logs persist in journald.

View File

@@ -14,9 +14,9 @@
# TAILSCALE_AUTHKEY — if set, configure Tailscale # TAILSCALE_AUTHKEY — if set, configure Tailscale
# TAILSCALE_HOSTNAME — Tailscale hostname (default: zopu-runtime) # TAILSCALE_HOSTNAME — Tailscale hostname (default: zopu-runtime)
# #
# Installs: Docker Engine, Bun, clones the repo, runs bun install, builds the # Installs: Docker Engine, Node.js 22, Bun, clones the repo, runs bun install, builds the
# daemon and agent, creates a non-root service user, installs systemd units, # web app, daemon, and agent, creates a non-root service user, installs systemd
# and configures firewall/Tailscale defaults. # units, and configures firewall/Tailscale defaults.
set -euo pipefail set -euo pipefail
@@ -105,18 +105,31 @@ fi
systemctl enable --now docker systemctl enable --now docker
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 3. Bun # 3. Node.js 22 (required by the Flue Node target)
# ---------------------------------------------------------------------------
NODE_MAJOR=$(node --version 2>/dev/null | sed -n 's/^v\([0-9][0-9]*\).*/\1/p')
if [[ -z "$NODE_MAJOR" || "$NODE_MAJOR" -lt 22 ]]; then
log "Installing Node.js 22..."
curl -fsSL https://deb.nodesource.com/setup_22.x -o /tmp/nodesource_setup.sh
bash /tmp/nodesource_setup.sh
apt-get install -y nodejs
else
log "Node.js already installed: $(node --version)"
fi
# ---------------------------------------------------------------------------
# 4. Bun
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
if ! command -v bun &>/dev/null; then if ! command -v bun &>/dev/null; then
log "Installing Bun..." log "Installing Bun..."
curl -fsSL https://bun.sh/install | bash curl -fsSL https://bun.sh/install | bash
ln -sf /root/.bun/bin/bun /usr/local/bin/bun install -m 0755 /root/.bun/bin/bun /usr/local/bin/bun
else else
log "Bun already installed: $(bun --version)" log "Bun already installed: $(bun --version)"
fi fi
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 4. Service user # 5. Service user
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
if ! id "$SERVICE_USER" &>/dev/null; then if ! id "$SERVICE_USER" &>/dev/null; then
log "Creating service user: $SERVICE_USER" log "Creating service user: $SERVICE_USER"
@@ -129,7 +142,7 @@ if ! id -nG "$SERVICE_USER" | grep -qw docker; then
fi fi
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 5. Clone or update repository # 6. Clone or update repository
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
if [[ -d "$INSTALL_DIR/.git" ]]; then if [[ -d "$INSTALL_DIR/.git" ]]; then
log "Repository exists at $INSTALL_DIR, fetching latest..." log "Repository exists at $INSTALL_DIR, fetching latest..."
@@ -144,25 +157,28 @@ else
fi fi
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 5b. Hand ownership of the checkout to the service user # 6b. Hand ownership of the checkout to the service user
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
log "Setting ownership of $INSTALL_DIR to $SERVICE_USER..." log "Setting ownership of $INSTALL_DIR to $SERVICE_USER..."
chown -R "$SERVICE_USER":"$SERVICE_USER" "$INSTALL_DIR" chown -R "$SERVICE_USER":"$SERVICE_USER" "$INSTALL_DIR"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 6. Install dependencies and build # 7. Install dependencies and build
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
log "Running bun install..." log "Running bun install..."
run_as_service bun install run_as_service bun install
log "Building daemon binary..." log "Building web app..."
run_as_service bun run --cwd apps/web build
log "Validating daemon production build..."
run_as_service bun run build:daemon run_as_service bun run build:daemon
log "Building agent service..." log "Building agent service..."
run_as_service bun run build:agents run_as_service bun run build:agents
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 7. Environment file # 8. Environment file
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
ENV_FILE="$INSTALL_DIR/.env" ENV_FILE="$INSTALL_DIR/.env"
if [[ ! -f "$ENV_FILE" ]]; then if [[ ! -f "$ENV_FILE" ]]; then
@@ -179,17 +195,17 @@ else
fi fi
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 8. Persistent log directory # 9. Persistent log directory
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
LOG_DIR="/var/log/zopu" LOG_DIR="/var/log/zopu"
mkdir -p "$LOG_DIR" mkdir -p "$LOG_DIR"
chown "$SERVICE_USER":"$SERVICE_USER" "$LOG_DIR" chown "$SERVICE_USER":"$SERVICE_USER" "$LOG_DIR"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 9. Install systemd units (substitute placeholders) # 10. Install systemd units (substitute placeholders)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
log "Installing systemd units..." log "Installing systemd units..."
for unit in zopu-daemon.service zopu-agent.service \ for unit in zopu-web.service zopu-daemon.service zopu-agent.service \
zopu-health.timer zopu-health.service \ zopu-health.timer zopu-health.service \
zopu-docker-cleanup.timer zopu-docker-cleanup.service; do zopu-docker-cleanup.timer zopu-docker-cleanup.service; do
SRC="$DEPLOY_DIR/systemd/$unit" SRC="$DEPLOY_DIR/systemd/$unit"
@@ -205,7 +221,7 @@ done
systemctl daemon-reload systemctl daemon-reload
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 10. Firewall (deny-by-default, explicit allow for Tailscale) # 11. Firewall (deny-by-default, explicit allow for Tailscale)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
log "Configuring firewall..." log "Configuring firewall..."
if ! ufw status 2>/dev/null | grep -q "Status: active"; then if ! ufw status 2>/dev/null | grep -q "Status: active"; then
@@ -227,7 +243,7 @@ else
fi fi
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 11. Tailscale (optional) # 12. Tailscale (optional)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
if [[ -n "${TAILSCALE_AUTHKEY:-}" ]]; then if [[ -n "${TAILSCALE_AUTHKEY:-}" ]]; then
log "Installing and configuring Tailscale..." log "Installing and configuring Tailscale..."
@@ -249,7 +265,7 @@ else
fi fi
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 12. Disk-space monitoring cron # 13. Disk-space monitoring cron
# /etc/cron.d format REQUIRES a username field. # /etc/cron.d format REQUIRES a username field.
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
log "Installing disk-space monitor (daily at 06:00)..." log "Installing disk-space monitor (daily at 06:00)..."
@@ -265,7 +281,7 @@ echo ""
echo "Next steps:" echo "Next steps:"
echo " 1. Edit $ENV_FILE with real values" echo " 1. Edit $ENV_FILE with real values"
echo " 2. Start services:" echo " 2. Start services:"
echo " systemctl start zopu-daemon zopu-agent" echo " systemctl start zopu-web zopu-daemon zopu-agent"
echo " 3. Enable health monitoring:" echo " 3. Enable health monitoring:"
echo " systemctl enable --now zopu-health.timer zopu-docker-cleanup.timer" echo " systemctl enable --now zopu-health.timer zopu-docker-cleanup.timer"
echo " 4. Verify health:" echo " 4. Verify health:"

View File

@@ -17,10 +17,28 @@
# reverse_proxy localhost:3583 # reverse_proxy localhost:3583
# } # }
# #
# --- Public deployment with automatic TLS (only if needed) --- # --- Public deployment through Cloudflare Tunnel ---
# #
# agent.example.invalid { # Cloudflare terminates TLS and forwards these hosts to Caddy over loopback.
# reverse_proxy localhost:3583 #
# http://zopu.sai-onchain.me {
# bind 127.0.0.1
# reverse_proxy 127.0.0.1:13100
# }
#
# http://zopu-api.sai-onchain.me {
# bind 127.0.0.1
# reverse_proxy 127.0.0.1:3210
# }
#
# http://zopu-site.sai-onchain.me {
# bind 127.0.0.1
# reverse_proxy 127.0.0.1:3211
# }
#
# http://zopu-agent.sai-onchain.me {
# bind 127.0.0.1
# reverse_proxy 127.0.0.1:3583
# } # }
# Default: do not ship an active Caddyfile. The file is documentation. # Default: do not ship an active Caddyfile. The file is documentation.

View File

@@ -0,0 +1,25 @@
{
admin 127.0.0.1:2019
auto_https off
http_port 8080
}
http://zopu.sai-onchain.me {
bind 127.0.0.1
reverse_proxy 127.0.0.1:13100
}
http://zopu-api.sai-onchain.me {
bind 127.0.0.1
reverse_proxy 127.0.0.1:3210
}
http://zopu-site.sai-onchain.me {
bind 127.0.0.1
reverse_proxy 127.0.0.1:3211
}
http://zopu-agent.sai-onchain.me {
bind 127.0.0.1
reverse_proxy 127.0.0.1:3583
}

View File

@@ -0,0 +1,13 @@
tunnel: 9e54ac9c-c1a1-4583-be08-3b5f1acc7b65
credentials-file: /root/.cloudflared/9e54ac9c-c1a1-4583-be08-3b5f1acc7b65.json
ingress:
- hostname: zopu.sai-onchain.me
service: http://127.0.0.1:8080
- hostname: zopu-api.sai-onchain.me
service: http://127.0.0.1:8080
- hostname: zopu-site.sai-onchain.me
service: http://127.0.0.1:8080
- hostname: zopu-agent.sai-onchain.me
service: http://127.0.0.1:8080
- service: http_status:404

View File

@@ -0,0 +1,32 @@
services:
backend:
image: ghcr.io/get-convex/convex-backend:latest
container_name: zopu-convex
restart: unless-stopped
stop_grace_period: 10s
stop_signal: SIGINT
ports:
- "127.0.0.1:3210:3210"
- "127.0.0.1:3211:3211"
volumes:
- zopu-convex-data:/convex/data
environment:
CONVEX_CLOUD_ORIGIN: ${CONVEX_CLOUD_ORIGIN}
CONVEX_SITE_ORIGIN: ${CONVEX_SITE_ORIGIN}
DISABLE_BEACON: "true"
DISABLE_METRICS_ENDPOINT: "true"
DOCUMENT_RETENTION_DELAY: "172800"
INSTANCE_NAME: ${CONVEX_INSTANCE_NAME:-zopu-production}
INSTANCE_SECRET: ${CONVEX_INSTANCE_SECRET:-}
REDACT_LOGS_TO_CLIENT: "true"
RUST_LOG: ${CONVEX_RUST_LOG:-info}
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3210/version"]
interval: 5s
timeout: 3s
retries: 12
start_period: 10s
volumes:
zopu-convex-data:
name: zopu-convex-data

View File

@@ -72,7 +72,10 @@ fi
log "Running bun install..." log "Running bun install..."
run_as_service bun install run_as_service bun install
log "Building daemon..." log "Building web app..."
run_as_service bun run --cwd apps/web build
log "Validating daemon production build..."
run_as_service bun run build:daemon run_as_service bun run build:daemon
log "Building agent service..." log "Building agent service..."
@@ -82,6 +85,7 @@ log "Restarting services..."
systemctl restart zopu-daemon systemctl restart zopu-daemon
sleep 3 sleep 3
systemctl restart zopu-agent systemctl restart zopu-agent
systemctl restart zopu-web
sleep 5 sleep 5
# Health check # Health check
@@ -94,6 +98,7 @@ if [[ -x "$HEALTH_SCRIPT" ]]; then
err "Health check failed after rollback!" err "Health check failed after rollback!"
err " journalctl -u zopu-daemon -n 50" err " journalctl -u zopu-daemon -n 50"
err " journalctl -u zopu-agent -n 50" err " journalctl -u zopu-agent -n 50"
err " journalctl -u zopu-web -n 50"
exit 1 exit 1
fi fi
fi fi

View File

@@ -77,17 +77,21 @@ fi
log "Running bun install..." log "Running bun install..."
run_as_service bun install run_as_service bun install
log "Building daemon binary..." log "Building web app..."
run_as_service bun run --cwd apps/web build
log "Validating daemon production build..."
run_as_service bun run build:daemon run_as_service bun run build:daemon
log "Building agent service..." log "Building agent service..."
run_as_service bun run build:agents run_as_service bun run build:agents
# Graceful restart: daemon first (owns RivetKit engine), then agent # Graceful restart: daemon first (owns RivetKit engine), then agent and web
log "Restarting services..." log "Restarting services..."
systemctl restart zopu-daemon systemctl restart zopu-daemon
sleep 3 sleep 3
systemctl restart zopu-agent systemctl restart zopu-agent
systemctl restart zopu-web
sleep 5 sleep 5
# Health check # Health check
@@ -100,6 +104,7 @@ if [[ -x "$HEALTH_SCRIPT" ]]; then
err "Health check failed after update!" err "Health check failed after update!"
err " journalctl -u zopu-daemon -n 50" err " journalctl -u zopu-daemon -n 50"
err " journalctl -u zopu-agent -n 50" err " journalctl -u zopu-agent -n 50"
err " journalctl -u zopu-web -n 50"
err "To rollback: ${INSTALL_DIR}/deploy/zopu-runtime/scripts/rollback.sh" err "To rollback: ${INSTALL_DIR}/deploy/zopu-runtime/scripts/rollback.sh"
exit 1 exit 1
fi fi

View File

@@ -10,10 +10,11 @@ Group=__SERVICE_USER__
WorkingDirectory=__INSTALL_DIR__/packages/agents WorkingDirectory=__INSTALL_DIR__/packages/agents
EnvironmentFile=__INSTALL_DIR__/.env EnvironmentFile=__INSTALL_DIR__/.env
Environment=PORT=3583
# Flue build output: packages/agents/dist/server.mjs # Flue's Node target requires Node built-ins such as node:sqlite.
# Listens on PORT (default 3583 per .env.template). # Listens on the service-pinned port 3583.
ExecStart=/usr/local/bin/bun dist/server.mjs ExecStart=/usr/bin/node dist/server.mjs
Restart=always Restart=always
RestartSec=10 RestartSec=10

View File

@@ -10,11 +10,13 @@ Group=__SERVICE_USER__
WorkingDirectory=__INSTALL_DIR__ WorkingDirectory=__INSTALL_DIR__
EnvironmentFile=__INSTALL_DIR__/.env EnvironmentFile=__INSTALL_DIR__/.env
Environment=HOME=/var/lib/zopu
StateDirectory=zopu
# RivetKit in-process engine: envoy mode (serverful). # RivetKit in-process engine: envoy mode (serverful).
# registry.start() boots the native Rust sidecar and actor envoy. # Run the Bun source entrypoint so AgentOS software assets retain their real
# RIVET_ENDPOINT defaults to http://localhost:6420 inside the daemon process. # node_modules paths; Bun compiled executables do not embed .aospkg files.
ExecStart=__INSTALL_DIR__/apps/daemon/dist/code-daemon ExecStart=/usr/local/bin/bun --env-file=__INSTALL_DIR__/.env apps/daemon/src/index.ts
Restart=always Restart=always
RestartSec=10 RestartSec=10

View File

@@ -0,0 +1,26 @@
[Unit]
Description=Zopu Web
After=network-online.target docker.service
Wants=network-online.target
[Service]
Type=simple
User=__SERVICE_USER__
Group=__SERVICE_USER__
WorkingDirectory=__INSTALL_DIR__/apps/web
EnvironmentFile=__INSTALL_DIR__/.env
Environment=HOST=127.0.0.1
Environment=PORT=13100
ExecStart=/usr/local/bin/bun run start
Restart=always
RestartSec=5
StandardOutput=journal
StandardError=journal
SyslogIdentifier=zopu-web
NoNewPrivileges=true
ProtectSystem=full
ProtectHome=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,53 @@
import { cn } from "@code/ui/lib/utils";
import { ChevronDown } from "lucide-react";
import type { MobileProjectView } from "./models";
export interface MobileProjectSelectProps {
readonly activeCount: number;
readonly className?: string;
readonly onProjectSelect: (projectId: string) => void;
readonly projects: readonly MobileProjectView[];
readonly selectedProjectId?: string;
}
export const MobileProjectSelect = ({
activeCount,
className,
onProjectSelect,
projects,
selectedProjectId,
}: MobileProjectSelectProps) => (
<div
className={cn(
"mt-0.5 flex min-w-0 items-center gap-1.5 text-[12px] leading-4 text-[#7c7772]",
className
)}
>
<span className="size-1.5 shrink-0 rounded-full bg-[#64ad1f]" />
<label className="relative min-w-0">
<span className="sr-only">Connected project</span>
<select
aria-label="Connected project"
className="max-w-[132px] cursor-pointer appearance-none truncate bg-transparent pr-4 font-medium text-[#5d5955] outline-none focus-visible:ring-2 focus-visible:ring-[#315dc0]"
onChange={(event) => onProjectSelect(event.currentTarget.value)}
value={selectedProjectId ?? ""}
>
{projects.length === 0 ? (
<option value="">No connected project</option>
) : null}
{projects.map((project) => (
<option key={project.id} value={project.id}>
{project.name}
</option>
))}
</select>
<ChevronDown
aria-hidden="true"
className="pointer-events-none absolute top-1/2 right-0 size-3 -translate-y-1/2"
/>
</label>
<span aria-hidden="true">·</span>
<span className="shrink-0 tabular-nums">{activeCount} active</span>
</div>
);

View File

@@ -10,8 +10,10 @@ import {
} from "lucide-react"; } from "lucide-react";
import type { FormEvent, MouseEvent } from "react"; import type { FormEvent, MouseEvent } from "react";
import { MobileProjectSelect } from "./mobile-project-select";
import { MobileSettingsPanel } from "./mobile-settings-panel"; import { MobileSettingsPanel } from "./mobile-settings-panel";
import type { import type {
MobileProjectView,
MobileWorkspaceView, MobileWorkspaceView,
MobileWorkUnitTone, MobileWorkUnitTone,
MobileWorkUnitView, MobileWorkUnitView,
@@ -41,12 +43,18 @@ const dateLabel = new Intl.DateTimeFormat("en", {
interface WorkStackHeaderProps { interface WorkStackHeaderProps {
readonly activeCount: number; readonly activeCount: number;
readonly onOpenSettings?: () => void; readonly onOpenSettings?: () => void;
readonly onProjectSelect: (projectId: string) => void;
readonly projects: readonly MobileProjectView[];
readonly selectedProjectId?: string;
readonly userInitials: string; readonly userInitials: string;
} }
const WorkStackHeader = ({ const WorkStackHeader = ({
activeCount, activeCount,
onOpenSettings, onOpenSettings,
onProjectSelect,
projects,
selectedProjectId,
userInitials, userInitials,
}: WorkStackHeaderProps) => ( }: WorkStackHeaderProps) => (
<header className="flex h-[96px] shrink-0 items-center bg-white px-4"> <header className="flex h-[96px] shrink-0 items-center bg-white px-4">
@@ -57,10 +65,12 @@ const WorkStackHeader = ({
<h1 className="text-[20px] leading-6 font-semibold tracking-[-0.03em] text-[#0b0b0a]"> <h1 className="text-[20px] leading-6 font-semibold tracking-[-0.03em] text-[#0b0b0a]">
Zopu Zopu
</h1> </h1>
<p className="mt-0.5 flex items-center gap-2 text-[13px] leading-4 text-[#7c7772]"> <MobileProjectSelect
<span className="size-1.5 rounded-full bg-[#64ad1f]" /> activeCount={activeCount}
{activeCount} active work units onProjectSelect={onProjectSelect}
</p> projects={projects}
selectedProjectId={selectedProjectId}
/>
</div> </div>
<button <button
aria-label="Open settings" aria-label="Open settings"
@@ -291,6 +301,7 @@ export interface MobileWorkStackScreenProps {
readonly onComposerChange: (value: string) => void; readonly onComposerChange: (value: string) => void;
readonly onComposerSubmit: (event: FormEvent<HTMLFormElement>) => void; readonly onComposerSubmit: (event: FormEvent<HTMLFormElement>) => void;
readonly onOpenUnit: (workUnitId: string) => void; readonly onOpenUnit: (workUnitId: string) => void;
readonly onProjectSelect: (projectId: string) => void;
readonly onRestoreChecked: () => void; readonly onRestoreChecked: () => void;
readonly onSettingsClose: () => void; readonly onSettingsClose: () => void;
readonly onSettingsOpen: () => void; readonly onSettingsOpen: () => void;
@@ -314,6 +325,7 @@ export const MobileWorkStackScreen = ({
onComposerChange, onComposerChange,
onComposerSubmit, onComposerSubmit,
onOpenUnit, onOpenUnit,
onProjectSelect,
onRestoreChecked, onRestoreChecked,
onSettingsClose, onSettingsClose,
onSettingsOpen, onSettingsOpen,
@@ -339,6 +351,9 @@ export const MobileWorkStackScreen = ({
<WorkStackHeader <WorkStackHeader
activeCount={data.activeCount} activeCount={data.activeCount}
onOpenSettings={onSettingsOpen} onOpenSettings={onSettingsOpen}
onProjectSelect={onProjectSelect}
projects={data.projects}
selectedProjectId={data.selectedProjectId}
userInitials={userInitials} userInitials={userInitials}
/> />
<main className="relative min-h-0 flex-1 overflow-hidden bg-[#f7f8f5] px-5 pt-[26px]"> <main className="relative min-h-0 flex-1 overflow-hidden bg-[#f7f8f5] px-5 pt-[26px]">