fuck them garbage deploys

This commit is contained in:
-Puter
2026-07-29 20:49:52 +05:30
parent a8b2ff5e2e
commit 9f7dd3c1a6
27 changed files with 0 additions and 1543 deletions

View File

@@ -1,20 +0,0 @@
FROM oven/bun:1.3.14 AS build
WORKDIR /app
COPY . .
RUN bun install --frozen-lockfile
RUN bun run --filter @code/agents build
FROM node:24-bookworm-slim
ENV NODE_ENV=production
ENV PORT=3000
WORKDIR /app
COPY --from=build /app /app
EXPOSE 3000
CMD ["node", "packages/agents/dist/server.mjs"]

View File

@@ -1,31 +0,0 @@
services:
backend:
build:
context: ../..
dockerfile: packages/agents/Dockerfile
environment:
AGENT_BACKEND_URL: ${AGENT_BACKEND_URL}
AGENT_MODEL_API: ${AGENT_MODEL_API}
AGENT_MODEL_API_KEY: ${AGENT_MODEL_API_KEY}
AGENT_MODEL_BASE_URL: ${AGENT_MODEL_BASE_URL}
AGENT_MODEL_CONTEXT_WINDOW: ${AGENT_MODEL_CONTEXT_WINDOW}
AGENT_MODEL_MAX_TOKENS: ${AGENT_MODEL_MAX_TOKENS}
AGENT_MODEL_NAME: ${AGENT_MODEL_NAME}
AGENT_MODEL_PROVIDER: ${AGENT_MODEL_PROVIDER}
CONVEX_URL: ${CONVEX_URL}
DAEMON_ID: zopu-agent-backend
FLUE_DB_TOKEN: ${FLUE_DB_TOKEN}
GITEA_TOKEN: ${GITEA_TOKEN:-}
GITEA_URL: ${GITEA_URL:-https://git.openputer.com}
PORT: "3000"
RIVET_ENDPOINT: ${RIVET_ENDPOINT}
RIVET_PUBLIC_ENDPOINT: ${RIVET_PUBLIC_ENDPOINT}
RIVET_WORKSPACE_TOKEN: ${RIVET_WORKSPACE_TOKEN}
networks:
- default
- dokploy-network
restart: unless-stopped
networks:
dokploy-network:
external: true

View File

@@ -1,15 +0,0 @@
services:
frontend:
build:
context: ../..
dockerfile: apps/web/Dockerfile
args:
VITE_AUTH_URL: ${VITE_AUTH_URL}
VITE_CONVEX_URL: ${VITE_CONVEX_URL}
environment:
HOST: 0.0.0.0
NODE_ENV: production
PORT: "3000"
VITE_AUTH_URL: ${VITE_AUTH_URL}
VITE_CONVEX_URL: ${VITE_CONVEX_URL}
restart: unless-stopped

View File

@@ -1,23 +0,0 @@
FROM oven/bun:1.3.14 AS bun
FROM node:24-bookworm-slim
COPY --from=bun /usr/local/bin/bun /usr/local/bin/bun
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates g++ git make python3 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /opt/zopu-source
COPY . .
RUN bun install --frozen-lockfile
ENV AGENT_WORKSPACE_ROOT=/var/lib/zopu/workspaces
ENV BUN_EXECUTABLE=/usr/local/bin/bun
ENV DAEMON_ID=zopu-agentos-runner
ENV ZOPU_SOURCE_REPOSITORY=/opt/zopu-source
VOLUME ["/var/lib/zopu/workspaces"]
CMD ["bun", "packages/agents/src/runner.ts"]

View File

@@ -1,26 +0,0 @@
services:
runner:
build:
context: .
dockerfile: deploy/dokploy/runner.Dockerfile
environment:
AGENT_MODEL_API: ${AGENT_MODEL_API}
AGENT_MODEL_API_KEY: ${AGENT_MODEL_API_KEY}
AGENT_MODEL_BASE_URL: ${AGENT_MODEL_BASE_URL}
AGENT_MODEL_CONTEXT_WINDOW: ${AGENT_MODEL_CONTEXT_WINDOW}
AGENT_MODEL_MAX_TOKENS: ${AGENT_MODEL_MAX_TOKENS}
AGENT_MODEL_NAME: ${AGENT_MODEL_NAME}
AGENT_MODEL_PROVIDER: ${AGENT_MODEL_PROVIDER}
CONVEX_URL: ${CONVEX_URL}
DAEMON_ID: zopu-agentos-runner
FLUE_DB_TOKEN: ${FLUE_DB_TOKEN}
RIVET_ENDPOINT: ${RIVET_ENDPOINT}
RIVET_PUBLIC_ENDPOINT: ${RIVET_PUBLIC_ENDPOINT}
RIVET_WORKSPACE_TOKEN: ${RIVET_WORKSPACE_TOKEN}
ZOPU_SOURCE_REPOSITORY: /opt/zopu-source
volumes:
- runner-workspaces:/var/lib/zopu/workspaces
restart: unless-stopped
volumes:
runner-workspaces:

View File

@@ -1,21 +0,0 @@
FROM oven/bun:1.3.14 AS build
WORKDIR /app
COPY . .
RUN bun install --frozen-lockfile
RUN bun run --filter web build
FROM node:24-bookworm-slim
ENV HOST=0.0.0.0
ENV NODE_ENV=production
ENV PORT=3000
WORKDIR /app
COPY --from=build /app /app
EXPOSE 3000
CMD ["node", "apps/web/node_modules/.bin/react-router-serve", "apps/web/build/server/index.js"]

View File

@@ -1,86 +0,0 @@
###############################################################################
# Zopu Runtime Deployment — Environment Template
#
# Copy to .env and fill in real values. Never commit .env to the repository.
# This file documents every environment group required by the single-node
# execution plane. Lines marked REQUIRED must be set before first start.
###############################################################################
# ---------------------------------------------------------------------------
# 1. Convex (control plane) — REQUIRED
# Public URLs for the self-hosted Convex deployment.
# ---------------------------------------------------------------------------
CONVEX_URL=https://your-deployment.convex.cloud
CONVEX_SITE_URL=https://your-deployment.convex.site
SITE_URL=http://localhost:13100
VITE_AUTH_URL=http://localhost:13100
VITE_CONVEX_URL=https://your-deployment.convex.cloud
VITE_FLUE_URL=http://localhost:3583
VITE_ZOPU_SERVER_URL=http://localhost:3590
# 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. Model gateway — REQUIRED
# All model calls route through this OpenAI-compatible endpoint.
# ---------------------------------------------------------------------------
AGENT_MODEL_PROVIDER=cheaptricks
AGENT_MODEL_NAME=glm-5.2
AGENT_MODEL_API=openai-completions
AGENT_MODEL_BASE_URL=https://ai.example.invalid/v1
AGENT_MODEL_API_KEY=replace-with-model-gateway-key
AGENT_MODEL_CONTEXT_WINDOW=262000
AGENT_MODEL_MAX_TOKENS=131072
# ---------------------------------------------------------------------------
# 3. AgentOS / Rivet Engine — REQUIRED for the execution runner
# The runner creates isolated worktrees from ZOPU_SOURCE_REPOSITORY and
# connects to AgentOS through the public engine endpoint.
# ---------------------------------------------------------------------------
RIVET_ENDPOINT=https://default:@rivet.example.com
RIVET_PUBLIC_ENDPOINT=https://default@rivet.example.com
RIVET_RUNNER_VERSION=1
RIVET_WORKSPACE_TOKEN=replace-with-a-long-random-workspace-token
ZOPU_SOURCE_REPOSITORY=/opt/zopu-source
# ---------------------------------------------------------------------------
# 4. Zopu agent service (Flue)
# FLUE_DB_TOKEN authenticates the Flue persistence adapter.
# zopu-agent.service pins the Flue Node server to port 3583.
# ---------------------------------------------------------------------------
FLUE_DB_TOKEN=replace-with-long-random-token
AGENT_BACKEND_URL=https://zopu-agent.example.com
# ---------------------------------------------------------------------------
# 5. Daemon identity
# ---------------------------------------------------------------------------
DAEMON_ID=zopu-dedicated
DAEMON_NAME=Zopu-Dedicated-Server
DAEMON_VERSION=0.0.0
DAEMON_HEARTBEAT_MS=15000
DAEMON_COMMAND_LEASE_MS=60000
# ---------------------------------------------------------------------------
# 7. Docker sandbox
# The zopu service user is added to the docker group during bootstrap.
# Orb sandboxes will use Docker for full-system isolation, but the Orb
# lane contract has not landed yet. Docker access is provisioned now so
# the boundary is ready; no Docker-backed sandbox code is wired today.
# ---------------------------------------------------------------------------
# No env vars required; Docker socket access is via group membership.
# ---------------------------------------------------------------------------
# 8. Service authentication / secrets
# These tokens authenticate inter-service calls. Generate strong randoms.
# ---------------------------------------------------------------------------
# Better Auth / Convex JWT secret (if the agent service needs to mint tokens):
#AUTH_SECRET=replace-with-64-char-hex
# ---------------------------------------------------------------------------
# 9. Private networking (Tailscale) — OPTIONAL
# When Tailscale is available, set the hostname for private DNS.
# ---------------------------------------------------------------------------
#TAILSCALE_HOSTNAME=zopu-runtime

View File

@@ -1,306 +0,0 @@
# Zopu Single-Node Runtime Deployment
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.
## Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ Debian Dedicated Server │
│ ~12 CPU cores · ~40 GB RAM · single-node │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ zopu-web │ │ zopu-agent │ │ Docker │ │
│ │ (systemd) │ │ (systemd) │ │ Engine │ │
│ │ │ │ │ │ │ │
│ │ React Router │ │ Flue Node 22 │ │ Convex │ │
│ │ :13100 │ │ server.mjs │ │ backend │ │
│ │ │ │ :3583 │ │ :3210/:3211 │ │
│ └──────┬───────┘ └──────┬───────┘ └──────────────┘ │
│ │ │ │
│ ┌──────▼───────┐ │
│ │ zopu-daemon │ Bun + Effect + RivetKit :6420 │
│ └──────────────┘ │
│ │
│ systemd timers: health-check (60s), docker-cleanup (daily) │
│ cron: disk-monitor (daily 06:00) │
│ ufw: deny-incoming, SSH + tailscale0 │
│ Tailscale: optional private overlay │
└─────────────────────────────────────────────────────────────┘
```
### Single-node RivetKit topology
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.
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`.
**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.
### Docker / Orb boundary
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.
## Files
```
deploy/zopu-runtime/
├── bootstrap.sh # One-shot Debian installer
├── .env.template # Environment template (all groups documented)
├── README.md # This file (runbook)
├── systemd/
│ ├── zopu-daemon.service # Daemon systemd unit
│ ├── zopu-agent.service # Agent systemd unit
│ ├── zopu-web.service # React Router web systemd unit
│ ├── zopu-health.service # Health check oneshot
│ ├── zopu-health.timer # Health check every 60s
│ ├── zopu-docker-cleanup.service
│ └── zopu-docker-cleanup.timer
├── scripts/
│ ├── health-check.sh # TCP/process health probes
│ ├── update.sh # Update to branch or commit
│ ├── rollback.sh # Roll back to previous commit
│ ├── docker-cleanup.sh # Prune stopped containers/images/networks
│ └── disk-monitor.sh # Disk usage alerting
└── caddy/
└── Caddyfile # Optional reverse proxy config (documentation)
```
## Fresh install
```bash
# 1. SSH into the fresh Debian 12 server as root.
# 2. Set environment overrides (optional):
export ZOPU_REPO_URL="ssh://git@git.openputer.com:2222/puter/zopu-code.git"
export ZOPU_REPO_BRANCH="dogfood/v0"
# export TAILSCALE_AUTHKEY="tskey-..."
# export TAILSCALE_HOSTNAME="zopu-runtime"
# 3. Run the bootstrap script:
bash bootstrap.sh
# 4. Edit .env with real values:
nano /opt/zopu/.env
# 5. Start services:
systemctl start zopu-web zopu-daemon
sleep 3
systemctl start zopu-agent
# 6. Enable timers:
systemctl enable --now zopu-health.timer zopu-docker-cleanup.timer
# 7. Verify:
/opt/zopu/deploy/zopu-runtime/scripts/health-check.sh
```
## Start / stop / restart
```bash
# Start all services
systemctl start zopu-web zopu-daemon zopu-agent
# Stop all services
systemctl stop zopu-agent zopu-daemon zopu-web
# Restart (daemon first — it owns the RivetKit engine)
systemctl restart zopu-web zopu-daemon && sleep 3 && systemctl restart zopu-agent
# Enable on boot
systemctl enable zopu-web zopu-daemon zopu-agent
# Disable on boot
systemctl disable zopu-web zopu-daemon zopu-agent
```
## Log inspection
All service logs go to journald with `SyslogIdentifier` tags.
```bash
# Daemon logs (live follow)
journalctl -u zopu-daemon -f
# Agent logs (live follow)
journalctl -u zopu-agent -f
# Last 100 lines of daemon
journalctl -u zopu-daemon -n 100
# Logs since boot
journalctl -u zopu-daemon -b
# Health check timer logs
journalctl -u zopu-health.service -n 50
# Docker cleanup logs
journalctl -u zopu-docker-cleanup.service -n 50
# Disk monitor logs (cron → file)
tail -100 /var/log/zopu/disk-monitor.log
# All Zopu syslog identifiers
journalctl -t zopu-daemon -t zopu-agent --since "1 hour ago"
```
## Health checks
```bash
# Manual health check (prints all probes)
/opt/zopu/deploy/zopu-runtime/scripts/health-check.sh
# Quiet mode (exit code only)
/opt/zopu/deploy/zopu-runtime/scripts/health-check.sh --quiet
# Check systemd timer is running
systemctl status zopu-health.timer
systemctl list-timers zopu-health.timer
```
The health check probes:
1. `zopu-daemon` systemd unit is active
2. `zopu-agent` systemd unit is active
3. RivetKit engine port (default 6420) accepts TCP connections
4. Flue agent port (default 3583) accepts TCP connections
5. Docker daemon responds to `docker info`
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.
## Update to commit
```bash
# Update to latest of dogfood/v0 (default)
/opt/zopu/deploy/zopu-runtime/scripts/update.sh
# Update to a specific branch
/opt/zopu/deploy/zopu-runtime/scripts/update.sh dogfood/runtime-deploy
# Update to a specific commit
/opt/zopu/deploy/zopu-runtime/scripts/update.sh abc123def456
```
The update script:
1. Records current HEAD to `.last-deployed-sha`
2. Fetches, resolves branch-or-commit, checks out
3. `bun install`, builds daemon and agent
4. Restarts daemon, waits, restarts agent
5. Runs health check; reports failure and rollback instructions
## Rollback
```bash
# Roll back to the previously deployed commit
/opt/zopu/deploy/zopu-runtime/scripts/rollback.sh
# Roll back to a specific commit
/opt/zopu/deploy/zopu-runtime/scripts/rollback.sh abc123def456
```
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.
## Docker cleanup
```bash
# Manual cleanup
/opt/zopu/deploy/zopu-runtime/scripts/docker-cleanup.sh
# Check Docker disk usage
docker system df
# Timer runs daily; check its schedule
systemctl list-timers zopu-docker-cleanup.timer
```
Cleanup prunes:
- Stopped containers older than 24 hours
- Dangling (untagged) images
- Unused networks
Named volumes and running containers are never removed.
## Disk-space monitoring
```bash
# Manual check
/opt/zopu/deploy/zopu-runtime/scripts/disk-monitor.sh
# Custom threshold (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`. Default alert threshold is 80%.
## Firewall and private networking
The firewall (`ufw`) is deny-by-default:
- SSH (port 22) is allowed on all interfaces
- All traffic on `tailscale0` is allowed (Tailscale private overlay)
- All other incoming traffic is denied
The RivetKit engine (`:6420`) and Flue agent (`:3583`) ports are **not** exposed on public interfaces. Reachability options:
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`.
2. **Custom private interface**: if you have a non-Tailscale private network (e.g. a VLAN or wireguard interface), add an explicit rule:
```bash
ufw allow in on eth1 # or your private interface name
```
Do NOT assume direct private IP access works by default — the deny-incoming 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.
## Environment groups
See [`.env.template`](./.env.template) for the full annotated template. The eight required groups:
| Group | Variables |
| --- | --- |
| Convex | `CONVEX_URL`, `CONVEX_SITE_URL`, `SITE_URL` |
| Gitea | `GITEA_URL`, `GITEA_TOKEN` |
| Model gateway | `AGENT_MODEL_*` |
| AgentOS/RivetKit | `RIVET_ENDPOINT` (optional) |
| 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` |
| Docker sandbox | group membership (no env vars) |
| 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
- **Kubernetes**: no container orchestration.
- **PostgreSQL for Rivet**: the in-process RivetKit engine uses its own storage; no external PostgreSQL is required.
- **Multi-node coordination**: single-node only.
- **Public administration endpoints**: no admin HTTP surface.
- **Secrets in source**: `.env` is never committed; `.env.template` contains 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.
## Reproducibility
The deployment does not require the developer's MacBook to remain online. Once bootstrap completes and `.env` is filled in:
1. Services run under systemd with `Restart=always`.
2. Logs persist in journald.
3. Health checks run every 60 seconds via systemd timer.
4. Docker cleanup runs daily.
5. Disk usage is monitored daily.
6. Unattended-upgrades handles Debian security patches.

View File

@@ -1,290 +0,0 @@
#!/usr/bin/env bash
#
# bootstrap.sh — One-shot installer for the Zopu single-node execution plane.
#
# Run as root on a fresh Debian 12 host:
#
# bash bootstrap.sh
#
# Environment overrides (set before running):
# ZOPU_REPO_URL — SSH clone URL (default: ssh://git@git.openputer.com:2222/puter/zopu-code.git)
# ZOPU_REPO_BRANCH — branch to deploy (default: dogfood/v0)
# ZOPU_INSTALL_DIR — install path (default: /opt/zopu)
# ZOPU_SERVICE_USER — system user (default: zopu)
# TAILSCALE_AUTHKEY — if set, configure Tailscale
# TAILSCALE_HOSTNAME — Tailscale hostname (default: zopu-runtime)
#
# Installs: Docker Engine, Node.js 22, Bun, clones the repo, runs bun install, builds the
# web app, daemon, and agent, creates a non-root service user, installs systemd
# units, and configures firewall/Tailscale defaults.
set -euo pipefail
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
REPO_URL="${ZOPU_REPO_URL:-ssh://git@git.openputer.com:2222/puter/zopu-code.git}"
REPO_BRANCH="${ZOPU_REPO_BRANCH:-dogfood/v0}"
INSTALL_DIR="${ZOPU_INSTALL_DIR:-/opt/zopu}"
SERVICE_USER="${ZOPU_SERVICE_USER:-zopu}"
DEPLOY_DIR="${INSTALL_DIR}/deploy/zopu-runtime"
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'
log() { echo -e "${GREEN}[bootstrap]${NC} $*"; }
warn() { echo -e "${YELLOW}[bootstrap]${NC} $*"; }
err() { echo -e "${RED}[bootstrap]${NC} $*" >&2; }
# runuser is part of util-linux (essential on Debian) and always available.
# sudo is NOT assumed on minimal Debian installs.
run_as_service() {
runuser -u "$SERVICE_USER" -- "$@"
}
# ---------------------------------------------------------------------------
# Pre-flight
# ---------------------------------------------------------------------------
if [[ "$EUID" -ne 0 ]]; then
err "This script must be run as root."
exit 1
fi
if [[ -f /etc/debian_version ]]; then
log "Detected Debian $(cat /etc/debian_version)"
else
warn "This script targets Debian 12. Other distributions may need manual adjustments."
fi
# ---------------------------------------------------------------------------
# 1. System packages
# ---------------------------------------------------------------------------
log "Updating apt and installing base packages..."
apt-get update -y
apt-get install -y \
ca-certificates \
curl \
gnupg \
ufw \
git \
jq \
netcat-openbsd \
openssh-client \
unattended-upgrades \
rsyslog
# ---------------------------------------------------------------------------
# 2. Docker Engine
# ---------------------------------------------------------------------------
if ! command -v docker &>/dev/null; then
log "Installing Docker Engine..."
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg \
-o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
https://download.docker.com/linux/debian \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
> /etc/apt/sources.list.d/docker.list
apt-get update -y
apt-get install -y \
docker-ce \
docker-ce-cli \
containerd.io \
docker-buildx-plugin \
docker-compose-plugin
else
log "Docker Engine already installed: $(docker --version)"
fi
systemctl enable --now docker
# ---------------------------------------------------------------------------
# 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
log "Installing Bun..."
curl -fsSL https://bun.sh/install | bash
install -m 0755 /root/.bun/bin/bun /usr/local/bin/bun
else
log "Bun already installed: $(bun --version)"
fi
# ---------------------------------------------------------------------------
# 5. Service user
# ---------------------------------------------------------------------------
if ! id "$SERVICE_USER" &>/dev/null; then
log "Creating service user: $SERVICE_USER"
useradd -r -m -d "/home/$SERVICE_USER" -s /bin/bash "$SERVICE_USER"
fi
if ! id -nG "$SERVICE_USER" | grep -qw docker; then
usermod -aG docker "$SERVICE_USER"
log "Added $SERVICE_USER to docker group"
fi
# ---------------------------------------------------------------------------
# 6. Clone or update repository
# ---------------------------------------------------------------------------
if [[ -d "$INSTALL_DIR/.git" ]]; then
log "Repository exists at $INSTALL_DIR, fetching latest..."
cd "$INSTALL_DIR"
git fetch origin
git checkout "$REPO_BRANCH"
git reset --hard "origin/$REPO_BRANCH"
else
log "Cloning $REPO_URL (branch $REPO_BRANCH) into $INSTALL_DIR..."
git clone --branch "$REPO_BRANCH" "$REPO_URL" "$INSTALL_DIR"
cd "$INSTALL_DIR"
fi
# ---------------------------------------------------------------------------
# 6b. Hand ownership of the checkout to the service user
# ---------------------------------------------------------------------------
log "Setting ownership of $INSTALL_DIR to $SERVICE_USER..."
chown -R "$SERVICE_USER":"$SERVICE_USER" "$INSTALL_DIR"
# ---------------------------------------------------------------------------
# 7. Install dependencies and build
# ---------------------------------------------------------------------------
log "Running bun install..."
run_as_service bun install
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
log "Building agent service..."
run_as_service bun run build:agents
# ---------------------------------------------------------------------------
# 8. Environment file
# ---------------------------------------------------------------------------
ENV_FILE="$INSTALL_DIR/.env"
if [[ ! -f "$ENV_FILE" ]]; then
log "Copying .env.template to .env — EDIT BEFORE STARTING SERVICES"
cp "$DEPLOY_DIR/.env.template" "$ENV_FILE"
chown "$SERVICE_USER":"$SERVICE_USER" "$ENV_FILE"
chmod 600 "$ENV_FILE"
warn "Edit $ENV_FILE with real values before starting services."
else
log ".env already exists at $ENV_FILE"
# Ensure correct ownership and permissions on existing .env
chown "$SERVICE_USER":"$SERVICE_USER" "$ENV_FILE"
chmod 600 "$ENV_FILE"
fi
# ---------------------------------------------------------------------------
# 9. Persistent log directory
# ---------------------------------------------------------------------------
LOG_DIR="/var/log/zopu"
mkdir -p "$LOG_DIR"
chown "$SERVICE_USER":"$SERVICE_USER" "$LOG_DIR"
# ---------------------------------------------------------------------------
# 10. Install systemd units (substitute placeholders)
# ---------------------------------------------------------------------------
log "Installing systemd units..."
for unit in zopu-web.service zopu-daemon.service zopu-agent.service \
zopu-health.timer zopu-health.service \
zopu-docker-cleanup.timer zopu-docker-cleanup.service; do
SRC="$DEPLOY_DIR/systemd/$unit"
DST="/etc/systemd/system/$unit"
if [[ -f "$SRC" ]]; then
sed \
-e "s|__INSTALL_DIR__|$INSTALL_DIR|g" \
-e "s|__SERVICE_USER__|$SERVICE_USER|g" \
"$SRC" > "$DST"
log " installed $unit"
fi
done
systemctl daemon-reload
# ---------------------------------------------------------------------------
# 11. Firewall (deny-by-default, explicit allow for Tailscale)
# ---------------------------------------------------------------------------
log "Configuring firewall..."
if ! ufw status 2>/dev/null | grep -q "Status: active"; then
ufw allow 22/tcp
ufw default deny incoming
ufw default allow outgoing
# Allow all traffic on the Tailscale interface (if present)
# This lets the agent and engine ports be reached over the private overlay.
ufw allow in on tailscale0 || warn "tailscale0 not present yet; rule will activate when interface appears"
ufw --force enable
log "Firewall enabled: SSH (22) allowed, tailscale0 allowed."
warn "Agent and RivetKit ports are NOT exposed on public interfaces."
warn "Reachability is via Tailscale (tailscale0) only."
else
log "Firewall already active. Ensuring tailscale0 rule..."
ufw allow in on tailscale0 2>/dev/null || true
fi
# ---------------------------------------------------------------------------
# 12. Tailscale (optional)
# ---------------------------------------------------------------------------
if [[ -n "${TAILSCALE_AUTHKEY:-}" ]]; then
log "Installing and configuring Tailscale..."
if ! command -v tailscaled &>/dev/null; then
curl -fsSL https://tailscale.com/install.sh | sh
fi
tailscale up --authkey "$TAILSCALE_AUTHKEY" \
--hostname "${TAILSCALE_HOSTNAME:-zopu-runtime}" \
--accept-routes
log "Tailscale configured: $(tailscale ip -4 2>/dev/null || echo 'waiting for IP')"
# Re-apply the tailscale0 firewall rule now that the interface exists
ufw allow in on tailscale0 2>/dev/null || true
else
warn "TAILSCALE_AUTHKEY not set — skipping Tailscale setup."
warn "Without Tailscale, services are reachable only via localhost."
warn "To use a private network interface, add an explicit UFW rule:"
warn " ufw allow in on <interface>"
fi
# ---------------------------------------------------------------------------
# 13. Disk-space monitoring cron
# /etc/cron.d format REQUIRES a username field.
# ---------------------------------------------------------------------------
log "Installing disk-space monitor (daily at 06:00)..."
CRON_LINE="0 6 * * * ${SERVICE_USER} ${DEPLOY_DIR}/scripts/disk-monitor.sh --warn-percent 80 >> /var/log/zopu/disk-monitor.log 2>&1"
echo "$CRON_LINE" > /etc/cron.d/zopu-disk-monitor
chmod 644 /etc/cron.d/zopu-disk-monitor
# ---------------------------------------------------------------------------
# Done
# ---------------------------------------------------------------------------
log "Bootstrap complete."
echo ""
echo "Next steps:"
echo " 1. Edit $ENV_FILE with real values"
echo " 2. Start services:"
echo " systemctl start zopu-web zopu-daemon zopu-agent"
echo " 3. Enable health monitoring:"
echo " systemctl enable --now zopu-health.timer zopu-docker-cleanup.timer"
echo " 4. Verify health:"
echo " $DEPLOY_DIR/scripts/health-check.sh"
echo ""
warn "Services are NOT started automatically. Edit .env first."

View File

@@ -1,23 +0,0 @@
# Caddyfile — Public reverse proxy for the Zopu web + API.
#
# The web frontend (port 5173) is served at the root, Better Auth is proxied
# first-party under /api/auth, and Flue is mounted under /api/flue.
zopu.cheaptricks.puter.wtf {
bind 135.181.82.179 2a01:4f9:c013:4a64::1
encode zstd gzip
handle /api/auth/* {
reverse_proxy https://befitting-dalmatian-161.convex.site {
header_up Host befitting-dalmatian-161.convex.site
header_up X-Forwarded-Host {host}
header_up X-Forwarded-Proto {scheme}
}
}
handle_path /api/flue/* {
reverse_proxy 127.0.0.1:3585
}
reverse_proxy 127.0.0.1:5173
}

View File

@@ -1,25 +0,0 @@
{
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

@@ -1,13 +0,0 @@
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

@@ -1,32 +0,0 @@
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

@@ -1,23 +0,0 @@
FROM oven/bun:1.3.14 AS bun
FROM node:24-bookworm-slim
COPY --from=bun /usr/local/bin/bun /usr/local/bin/bun
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates g++ git make python3 \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /opt/zopu-source
COPY . .
RUN bun install --frozen-lockfile
ENV AGENT_WORKSPACE_ROOT=/var/lib/zopu/workspaces
ENV BUN_EXECUTABLE=/usr/local/bin/bun
ENV DAEMON_ID=zopu-agentos-runner
ENV ZOPU_SOURCE_REPOSITORY=/opt/zopu-source
VOLUME ["/var/lib/zopu/workspaces"]
CMD ["bun", "packages/agents/src/runner.ts"]

View File

@@ -1,26 +0,0 @@
services:
zopu-agentos-runner:
build:
context: ../../..
dockerfile: deploy/zopu-runtime/runner/Dockerfile
environment:
AGENT_MODEL_API: ${AGENT_MODEL_API}
AGENT_MODEL_API_KEY: ${AGENT_MODEL_API_KEY}
AGENT_MODEL_BASE_URL: ${AGENT_MODEL_BASE_URL}
AGENT_MODEL_CONTEXT_WINDOW: ${AGENT_MODEL_CONTEXT_WINDOW}
AGENT_MODEL_MAX_TOKENS: ${AGENT_MODEL_MAX_TOKENS}
AGENT_MODEL_NAME: ${AGENT_MODEL_NAME}
AGENT_MODEL_PROVIDER: ${AGENT_MODEL_PROVIDER}
CONVEX_URL: ${CONVEX_URL}
DAEMON_ID: zopu-agentos-runner
FLUE_DB_TOKEN: ${FLUE_DB_TOKEN}
RIVET_ENDPOINT: ${RIVET_ENDPOINT}
RIVET_PUBLIC_ENDPOINT: ${RIVET_PUBLIC_ENDPOINT}
RIVET_WORKSPACE_TOKEN: ${RIVET_WORKSPACE_TOKEN}
ZOPU_SOURCE_REPOSITORY: /opt/zopu-source
volumes:
- zopu-agentos-workspaces:/var/lib/zopu/workspaces
restart: unless-stopped
volumes:
zopu-agentos-workspaces:

View File

@@ -1,63 +0,0 @@
#!/usr/bin/env bash
#
# disk-monitor.sh — Check disk usage and alert when above threshold.
#
# Usage:
# disk-monitor.sh # print usage, warn at 80%
# disk-monitor.sh --warn-percent 90 # custom threshold
#
# Requires GNU coreutils df (standard on Debian). Uses --output for
# deterministic column ordering regardless of locale.
#
# Exit code 0 if under threshold, 1 if at or above.
set -euo pipefail
WARN_PERCENT=80
while [[ $# -gt 0 ]]; do
case "$1" in
--warn-percent)
WARN_PERCENT="$2"
shift 2
;;
*)
echo "Unknown argument: $1" >&2
exit 2
;;
esac
done
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'
EXIT_CODE=0
# Partitions to check: install root and /var (Docker data-root is often here)
PARTITIONS="${PARTITIONS:-/ /var}"
for partition in $PARTITIONS; do
if [[ ! -d "$partition" ]]; then
continue
fi
# GNU df --output columns: pcent (Use%), size, avail, target (Mounted on)
read -r USAGE_PCT SIZE AVAIL MOUNT <<< "$(df -h --output=pcent,size,avail,target "$partition" | awk 'NR==2 {gsub(/%/,"",$1); print $1, $2, $3, $4}')"
if [[ -z "${USAGE_PCT:-}" || ! "${USAGE_PCT:-}" =~ ^[0-9]+$ ]]; then
echo " [skip] Could not read usage for ${partition}"
continue
fi
if [[ "$USAGE_PCT" -ge "$WARN_PERCENT" ]]; then
echo -e " ${RED}WARN${NC} ${MOUNT}: ${USAGE_PCT}% used (${AVAIL} avail of ${SIZE}) — threshold ${WARN_PERCENT}%"
EXIT_CODE=1
elif [[ "$USAGE_PCT" -ge $((WARN_PERCENT - 10)) ]]; then
echo -e " ${YELLOW}NOTE${NC} ${MOUNT}: ${USAGE_PCT}% used (${AVAIL} avail of ${SIZE}) — approaching threshold"
else
echo -e " ${GREEN}OK${NC} ${MOUNT}: ${USAGE_PCT}% used (${AVAIL} avail of ${SIZE})"
fi
done
exit "$EXIT_CODE"

View File

@@ -1,36 +0,0 @@
#!/usr/bin/env bash
#
# docker-cleanup.sh — Remove stopped containers, dangling images, and unused networks.
#
# Safe to run via systemd timer (daily). Uses Docker's built-in pruning
# commands with conservative scope.
#
# Does NOT prune volumes. Named volumes may hold durable data and cannot be
# safely auto-pruned in a deployment that mixes stateful workloads. When the
# Orb lane lands with labeled resources, volume pruning can be scoped to
# Orb-managed labels (e.g. --filter label=org.openputer.orb). Until then,
# manage volumes manually.
set -euo pipefail
echo "[docker-cleanup] $(date -u +%Y-%m-%dT%H:%M:%SZ)"
# Remove stopped containers older than 24 hours
echo "[docker-cleanup] Pruning stopped containers (>24h old)..."
docker container prune -f --filter "until=24h"
# Remove dangling images (untagged intermediate layers only)
echo "[docker-cleanup] Pruning dangling images..."
docker image prune -f
# Remove unused networks
echo "[docker-cleanup] Pruning unused networks..."
docker network prune -f
# Volumes are intentionally NOT pruned. See header comment.
# Show remaining disk usage
echo "[docker-cleanup] Docker disk usage:"
docker system df
echo "[docker-cleanup] Done."

View File

@@ -1,122 +0,0 @@
#!/usr/bin/env bash
#
# health-check.sh — Probe the Zopu execution plane.
#
# Checks (TCP/process-level; no assumed HTTP health routes):
# 1. zopu-daemon systemd unit is active
# 2. zopu-agent systemd unit is active
# 3. RivetKit engine TCP port (RIVET_ENDPOINT, default 6420) accepts connections
# 4. Flue agent TCP port (PORT, default 3583) accepts connections
# 5. Docker daemon is reachable
#
# Flue does not expose a health endpoint by design. RivetKit's health route
# is internal to the registry runtime. We use TCP connection checks only.
#
# Usage:
# health-check.sh # print results
# health-check.sh --quiet # suppress output, exit 0 only if all healthy
#
# Exit codes: 0 = all healthy, 1 = one or more unhealthy
set -euo pipefail
QUIET=false
[[ "${1:-}" == "--quiet" ]] && QUIET=true
GREEN='\033[0;32m'
RED='\033[0;31m'
NC='\033[0m'
pass() { $QUIET || echo -e " ${GREEN}PASS${NC} $*"; }
fail() { echo -e " ${RED}FAIL${NC} $*" >&2; FAILURES=$((FAILURES + 1)); }
FAILURES=0
# ---------------------------------------------------------------------------
# Load environment
# ---------------------------------------------------------------------------
ENV_FILE="${ENV_FILE:-/opt/zopu/.env}"
if [[ -f "$ENV_FILE" ]]; then
# shellcheck source=/dev/null
set -a
. "$ENV_FILE"
set +a
fi
RIVET_PORT="6420"
if [[ -n "${RIVET_ENDPOINT:-}" ]]; then
RIVET_PORT=$(echo "$RIVET_ENDPOINT" | sed -n 's|.*://[^:]*:\([0-9]*\).*|\1|p')
[[ -z "$RIVET_PORT" ]] && RIVET_PORT="6420"
fi
AGENT_PORT="${PORT:-3583}"
# ---------------------------------------------------------------------------
# TCP probe helper: works on Debian (nc from netcat-openbsd) and macOS.
# Falls back to bash /dev/tcp if nc is unavailable.
# ---------------------------------------------------------------------------
tcp_probe() {
local host="$1" port="$2"
if command -v nc &>/dev/null; then
nc -z -w 5 "$host" "$port" 2>/dev/null
else
timeout 5 bash -c "echo > /dev/tcp/${host}/${port}" 2>/dev/null
fi
}
# ---------------------------------------------------------------------------
# 1. Daemon systemd unit
# ---------------------------------------------------------------------------
if systemctl is-active --quiet zopu-daemon 2>/dev/null; then
pass "zopu-daemon service is active"
else
fail "zopu-daemon service is not active"
fi
# ---------------------------------------------------------------------------
# 2. Agent systemd unit
# ---------------------------------------------------------------------------
if systemctl is-active --quiet zopu-agent 2>/dev/null; then
pass "zopu-agent service is active"
else
fail "zopu-agent service is not active"
fi
# ---------------------------------------------------------------------------
# 3. RivetKit engine TCP port
# ---------------------------------------------------------------------------
if tcp_probe localhost "$RIVET_PORT"; then
pass "RivetKit engine port ${RIVET_PORT} is accepting connections"
else
fail "RivetKit engine port ${RIVET_PORT} is not accepting connections"
fi
# ---------------------------------------------------------------------------
# 4. Flue agent TCP port
# ---------------------------------------------------------------------------
if tcp_probe localhost "$AGENT_PORT"; then
pass "Flue agent port ${AGENT_PORT} is accepting connections"
else
fail "Flue agent port ${AGENT_PORT} is not accepting connections"
fi
# ---------------------------------------------------------------------------
# 5. Docker daemon
# ---------------------------------------------------------------------------
if docker info &>/dev/null; then
pass "Docker daemon is reachable"
else
fail "Docker daemon is not reachable"
fi
# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
$QUIET || echo ""
if [[ "$FAILURES" -eq 0 ]]; then
$QUIET || echo -e "${GREEN}All checks passed.${NC}"
exit 0
else
echo -e "${RED}${FAILURES} check(s) failed.${NC}" >&2
exit 1
fi

View File

@@ -1,104 +0,0 @@
#!/usr/bin/env bash
#
# rollback.sh — Roll back to the previously deployed commit.
#
# Usage:
# rollback.sh # roll back to .last-deployed-sha
# rollback.sh <sha> # roll back to a specific commit
#
# .env is gitignored and is never touched by git operations. It survives
# updates and rollbacks unchanged.
#
# Must be run as root (uses runuser to build as the service user).
set -euo pipefail
INSTALL_DIR="${ZOPU_INSTALL_DIR:-/opt/zopu}"
SERVICE_USER="${ZOPU_SERVICE_USER:-zopu}"
GREEN='\033[0;32m'
RED='\033[0;31m'
NC='\033[0m'
log() { echo -e "${GREEN}[rollback]${NC} $*"; }
err() { echo -e "${RED}[rollback]${NC} $*" >&2; }
run_as_service() {
runuser -u "$SERVICE_USER" -- "$@"
}
cd "$INSTALL_DIR"
# Determine target
ROLLBACK_SHA="${1:-}"
if [[ -z "$ROLLBACK_SHA" ]]; then
LAST_SHA_FILE="${INSTALL_DIR}/.last-deployed-sha"
if [[ ! -f "$LAST_SHA_FILE" ]]; then
err "No previous deployment recorded in ${LAST_SHA_FILE}."
err "Pass a commit SHA explicitly: rollback.sh <sha>"
exit 1
fi
ROLLBACK_SHA=$(cat "$LAST_SHA_FILE")
fi
# Validate commit exists
if ! git rev-parse --verify "${ROLLBACK_SHA}^{commit}" &>/dev/null; then
err "Commit ${ROLLBACK_SHA} does not exist in the local repository."
exit 1
fi
CURRENT_SHA=$(git rev-parse HEAD)
log "Current: ${CURRENT_SHA:0:12}"
log "Rolling back to: ${ROLLBACK_SHA:0:12}"
# Save current state before rolling back (enables re-rollback)
echo "$CURRENT_SHA" > "${INSTALL_DIR}/.pre-rollback-sha"
# Checkout target
git checkout "$ROLLBACK_SHA"
# Restore ownership of the checkout to the service user after git operations
log "Setting ownership of checkout to $SERVICE_USER..."
chown -R "$SERVICE_USER":"$SERVICE_USER" "$INSTALL_DIR"
# Confirm .env is intact and has correct permissions
if [[ -f "${INSTALL_DIR}/.env" ]]; then
log ".env preserved."
chmod 600 "${INSTALL_DIR}/.env"
else
err ".env is missing! Restore it from backup before starting services."
fi
log "Running bun install..."
run_as_service bun install
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
log "Building agent service..."
run_as_service bun run build:agents
log "Restarting services..."
systemctl restart zopu-daemon
sleep 3
systemctl restart zopu-agent
systemctl restart zopu-web
sleep 5
# Health check
HEALTH_SCRIPT="${INSTALL_DIR}/deploy/zopu-runtime/scripts/health-check.sh"
if [[ -x "$HEALTH_SCRIPT" ]]; then
log "Running health check..."
if "$HEALTH_SCRIPT"; then
log "Rollback complete and healthy."
else
err "Health check failed after rollback!"
err " journalctl -u zopu-daemon -n 50"
err " journalctl -u zopu-agent -n 50"
err " journalctl -u zopu-web -n 50"
exit 1
fi
fi

View File

@@ -1,113 +0,0 @@
#!/usr/bin/env bash
#
# update.sh — Update the Zopu runtime to a specific branch or commit.
#
# Usage:
# update.sh # update to latest dogfood/v0
# update.sh dogfood/v0 # update to latest of branch dogfood/v0
# update.sh <commit-sha> # checkout and build a specific commit
#
# The argument is treated as a branch name first; if no matching remote
# tracking branch exists it is treated as a commit SHA.
#
# .env is gitignored and is never touched by git operations. It survives
# updates and rollbacks unchanged.
#
# Must be run as root (uses runuser to build as the service user).
set -euo pipefail
INSTALL_DIR="${ZOPU_INSTALL_DIR:-/opt/zopu}"
SERVICE_USER="${ZOPU_SERVICE_USER:-zopu}"
TARGET="${1:-dogfood/v0}"
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'
log() { echo -e "${GREEN}[update]${NC} $*"; }
warn() { echo -e "${YELLOW}[update]${NC} $*"; }
err() { echo -e "${RED}[update]${NC} $*" >&2; }
run_as_service() {
runuser -u "$SERVICE_USER" -- "$@"
}
cd "$INSTALL_DIR"
# Record current commit for rollback
CURRENT_SHA=$(git rev-parse HEAD)
log "Current HEAD: ${CURRENT_SHA:0:12}"
echo "$CURRENT_SHA" > "${INSTALL_DIR}/.last-deployed-sha"
# Fetch all remotes
log "Fetching from origin..."
git fetch origin
# Resolve target: branch first, then commit
if git show-ref --verify --quiet "refs/remotes/origin/${TARGET}"; then
log "Target is a branch: ${TARGET}"
git checkout -B "$TARGET" "origin/${TARGET}"
elif git rev-parse --verify "${TARGET}^{commit}" &>/dev/null; then
log "Target is a commit: ${TARGET:0:12}"
git checkout "$TARGET"
else
err "'${TARGET}' is not a known branch or valid commit."
err "Available branches: $(git branch -r | tr '\n' ' ')"
exit 1
fi
NEW_SHA=$(git rev-parse HEAD)
log "Now at: ${NEW_SHA:0:12}"
# Restore ownership of the checkout to the service user after git operations
log "Setting ownership of checkout to $SERVICE_USER..."
chown -R "$SERVICE_USER":"$SERVICE_USER" "$INSTALL_DIR"
# Confirm .env is intact and has correct permissions
if [[ -f "${INSTALL_DIR}/.env" ]]; then
log ".env preserved."
chmod 600 "${INSTALL_DIR}/.env"
else
err ".env is missing! Restore it from backup before starting services."
fi
# Install and build
log "Running bun install..."
run_as_service bun install
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
log "Building agent service..."
run_as_service bun run build:agents
# Graceful restart: daemon first (owns RivetKit engine), then agent and web
log "Restarting services..."
systemctl restart zopu-daemon
sleep 3
systemctl restart zopu-agent
systemctl restart zopu-web
sleep 5
# Health check
HEALTH_SCRIPT="${INSTALL_DIR}/deploy/zopu-runtime/scripts/health-check.sh"
if [[ -x "$HEALTH_SCRIPT" ]]; then
log "Running health check..."
if "$HEALTH_SCRIPT"; then
log "Update complete and healthy."
else
err "Health check failed after update!"
err " journalctl -u zopu-daemon -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"
exit 1
fi
else
log "Update complete. Verify manually."
fi

View File

@@ -1,40 +0,0 @@
[Unit]
Description=Zopu Agent Service (Flue Node server)
After=network-online.target zopu-daemon.service
Wants=network-online.target
[Service]
Type=simple
User=__SERVICE_USER__
Group=__SERVICE_USER__
WorkingDirectory=__INSTALL_DIR__/packages/agents
EnvironmentFile=__INSTALL_DIR__/.env
Environment=PORT=3583
# Flue's Node target requires Node built-ins such as node:sqlite.
# Listens on the service-pinned port 3583.
ExecStart=/usr/bin/node dist/server.mjs
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
SyslogIdentifier=zopu-agent
# Resource limits
MemoryMax=8G
CPUWeight=80
# Security hardening
NoNewPrivileges=true
ProtectSystem=full
ProtectHome=true
PrivateTmp=true
# Docker socket access for Orb sandbox runtime (lives in agent process)
SupplementaryGroups=docker
[Install]
WantedBy=multi-user.target

View File

@@ -1,42 +0,0 @@
[Unit]
Description=Zopu Daemon (Bun/Effect + AgentOS/RivetKit)
After=network-online.target docker.service
Wants=network-online.target
[Service]
Type=simple
User=__SERVICE_USER__
Group=__SERVICE_USER__
WorkingDirectory=__INSTALL_DIR__
EnvironmentFile=__INSTALL_DIR__/.env
Environment=HOME=/var/lib/zopu
StateDirectory=zopu
# RivetKit in-process engine: envoy mode (serverful).
# Run the Bun source entrypoint so AgentOS software assets retain their real
# node_modules paths; Bun compiled executables do not embed .aospkg files.
ExecStart=/usr/local/bin/bun --env-file=__INSTALL_DIR__/.env apps/daemon/src/index.ts
Restart=always
RestartSec=10
StandardOutput=journal
StandardError=journal
SyslogIdentifier=zopu-daemon
# Resource limits (12-core, 40 GB host)
MemoryMax=8G
CPUWeight=100
# Security hardening
NoNewPrivileges=true
ProtectSystem=full
ProtectHome=true
PrivateTmp=true
# Docker socket access (for future Orb sandboxes; group membership required)
SupplementaryGroups=docker
[Install]
WantedBy=multi-user.target

View File

@@ -1,7 +0,0 @@
[Unit]
Description=Zopu Docker Cleanup
After=docker.service
[Service]
Type=oneshot
ExecStart=__INSTALL_DIR__/deploy/zopu-runtime/scripts/docker-cleanup.sh

View File

@@ -1,9 +0,0 @@
[Unit]
Description=Zopu Docker Cleanup (daily)
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.target

View File

@@ -1,11 +0,0 @@
[Unit]
Description=Zopu Health Check
[Service]
Type=oneshot
User=__SERVICE_USER__
EnvironmentFile=__INSTALL_DIR__/.env
# health-check.sh reads ENV_FILE to find the .env to source. Set it to the
# install path so custom install dirs work correctly.
Environment=ENV_FILE=__INSTALL_DIR__/.env
ExecStart=__INSTALL_DIR__/deploy/zopu-runtime/scripts/health-check.sh --quiet

View File

@@ -1,10 +0,0 @@
[Unit]
Description=Zopu Health Check (every 60s)
[Timer]
OnBootSec=30
OnUnitActiveSec=60
AccuracySec=5
[Install]
WantedBy=timers.target

View File

@@ -1,26 +0,0 @@
[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