diff --git a/apps/web/src/components/mobile-workspace/mobile-flow-page.tsx b/apps/web/src/components/mobile-workspace/mobile-flow-page.tsx index 59c3bed..f7d38e2 100644 --- a/apps/web/src/components/mobile-workspace/mobile-flow-page.tsx +++ b/apps/web/src/components/mobile-workspace/mobile-flow-page.tsx @@ -140,6 +140,11 @@ export const MobileFlowPage = ({ screen }: MobileFlowPageProps) => { onOpenUnit={(selectedWorkUnitId) => navigate(`/chat/${selectedWorkUnitId}`) } + onProjectSelect={(projectId) => + projectWorkspace.projectWorkspace.setSelectedProjectId( + projectId as Id<"projects"> + ) + } onRestoreChecked={handleRestoreChecked} onSettingsClose={() => workspace.setSettingsOpen(false)} onSettingsOpen={() => workspace.setSettingsOpen(true)} diff --git a/deploy/zopu-runtime/.env.template b/deploy/zopu-runtime/.env.template index 045209d..48ce716 100644 --- a/deploy/zopu-runtime/.env.template +++ b/deploy/zopu-runtime/.env.template @@ -8,11 +8,20 @@ # --------------------------------------------------------------------------- # 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_SITE_URL=https://your-deployment.convex.site 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 @@ -46,16 +55,15 @@ AGENT_MODEL_MAX_TOKENS=131072 # --------------------------------------------------------------------------- # 5. Zopu agent service (Flue) # 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 -PORT=3583 # --------------------------------------------------------------------------- # 6. Daemon identity # --------------------------------------------------------------------------- DAEMON_ID=zopu-dedicated -DAEMON_NAME=Zopu Dedicated Server +DAEMON_NAME=Zopu-Dedicated-Server DAEMON_VERSION=0.0.0 DAEMON_HEARTBEAT_MS=15000 DAEMON_COMMAND_LEASE_MS=60000 diff --git a/deploy/zopu-runtime/README.md b/deploy/zopu-runtime/README.md index 28f94e9..305909e 100644 --- a/deploy/zopu-runtime/README.md +++ b/deploy/zopu-runtime/README.md @@ -1,9 +1,6 @@ # Zopu Single-Node Runtime Deployment -Deployment artifacts for the Zopu execution plane on a single Debian dedicated -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. +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 @@ -13,23 +10,17 @@ service, Docker Engine for future Orb sandboxes, and supporting infrastructure. │ ~12 CPU cores · ~40 GB RAM · single-node │ │ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ -│ │ zopu-daemon │ │ zopu-agent │ │ Docker │ │ +│ │ zopu-web │ │ zopu-agent │ │ Docker │ │ │ │ (systemd) │ │ (systemd) │ │ Engine │ │ │ │ │ │ │ │ │ │ -│ │ Effect daemon│ │ Flue Node │ │ Orb sandboxes│ │ -│ │ + RivetKit │ │ server.mjs │ │ (future) │ │ -│ │ in-process │ │ :3583 │ │ │ │ -│ │ engine │ │ │ │ │ │ -│ │ + native │ │ │ │ │ │ -│ │ sidecar │ │ │ │ │ │ -│ │ :6420 │ │ │ │ │ │ +│ │ React Router │ │ Flue Node 22 │ │ Convex │ │ +│ │ :13100 │ │ server.mjs │ │ backend │ │ +│ │ │ │ :3583 │ │ :3210/:3211 │ │ │ └──────┬───────┘ └──────┬───────┘ └──────────────┘ │ │ │ │ │ -│ └────────┬───────────┘ │ -│ │ │ -│ ┌────────▼─────────┐ │ -│ │ Convex (cloud) │ ← control plane (pre-deployed)│ -│ └──────────────────┘ │ +│ ┌──────▼───────┐ │ +│ │ zopu-daemon │ Bun + Effect + RivetKit :6420 │ +│ └──────────────┘ │ │ │ │ systemd timers: health-check (60s), docker-cleanup (daily) │ │ cron: disk-monitor (daily 06:00) │ @@ -40,31 +31,15 @@ service, Docker Engine for future Orb sandboxes, and supporting infrastructure. ### 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. +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`. +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. +**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. +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 @@ -76,6 +51,7 @@ deploy/zopu-runtime/ ├── 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 @@ -108,7 +84,7 @@ bash bootstrap.sh nano /opt/zopu/.env # 5. Start services: -systemctl start zopu-daemon +systemctl start zopu-web zopu-daemon sleep 3 systemctl start zopu-agent @@ -122,20 +98,20 @@ systemctl enable --now zopu-health.timer zopu-docker-cleanup.timer ## Start / stop / restart ```bash -# Start both services -systemctl start zopu-daemon zopu-agent +# Start all services +systemctl start zopu-web zopu-daemon zopu-agent -# Stop both services -systemctl stop zopu-agent zopu-daemon +# Stop all services +systemctl stop zopu-agent zopu-daemon zopu-web # 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 -systemctl enable zopu-daemon zopu-agent +systemctl enable zopu-web zopu-daemon zopu-agent # Disable on boot -systemctl disable zopu-daemon zopu-agent +systemctl disable zopu-web zopu-daemon zopu-agent ``` ## Log inspection @@ -183,16 +159,14 @@ 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. +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 @@ -208,6 +182,7 @@ publicly addressable on the engine endpoint. ``` 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 @@ -224,9 +199,7 @@ The update script: /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. +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 @@ -242,11 +215,11 @@ 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 @@ -259,69 +232,71 @@ Named volumes and running containers are never removed. /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%. +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: +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: +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. + 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: +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`, `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` | | 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 -- **Web app**: the web frontend is not deployed in this lane. - **Kubernetes**: no container orchestration. -- **PostgreSQL for Rivet**: the in-process RivetKit engine uses its own - storage; no external PostgreSQL is required. +- **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. -- **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. +- **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: +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. diff --git a/deploy/zopu-runtime/bootstrap.sh b/deploy/zopu-runtime/bootstrap.sh index 68e5deb..ec9d30e 100755 --- a/deploy/zopu-runtime/bootstrap.sh +++ b/deploy/zopu-runtime/bootstrap.sh @@ -14,9 +14,9 @@ # TAILSCALE_AUTHKEY — if set, configure Tailscale # TAILSCALE_HOSTNAME — Tailscale hostname (default: zopu-runtime) # -# Installs: Docker Engine, Bun, clones the repo, runs bun install, builds the -# daemon and agent, creates a non-root service user, installs systemd units, -# and configures firewall/Tailscale defaults. +# 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 @@ -105,18 +105,31 @@ fi 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 log "Installing Bun..." 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 log "Bun already installed: $(bun --version)" fi # --------------------------------------------------------------------------- -# 4. Service user +# 5. Service user # --------------------------------------------------------------------------- if ! id "$SERVICE_USER" &>/dev/null; then log "Creating service user: $SERVICE_USER" @@ -129,7 +142,7 @@ if ! id -nG "$SERVICE_USER" | grep -qw docker; then fi # --------------------------------------------------------------------------- -# 5. Clone or update repository +# 6. Clone or update repository # --------------------------------------------------------------------------- if [[ -d "$INSTALL_DIR/.git" ]]; then log "Repository exists at $INSTALL_DIR, fetching latest..." @@ -144,25 +157,28 @@ else 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..." chown -R "$SERVICE_USER":"$SERVICE_USER" "$INSTALL_DIR" # --------------------------------------------------------------------------- -# 6. Install dependencies and build +# 7. Install dependencies and build # --------------------------------------------------------------------------- log "Running 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 log "Building agent service..." run_as_service bun run build:agents # --------------------------------------------------------------------------- -# 7. Environment file +# 8. Environment file # --------------------------------------------------------------------------- ENV_FILE="$INSTALL_DIR/.env" if [[ ! -f "$ENV_FILE" ]]; then @@ -179,17 +195,17 @@ else fi # --------------------------------------------------------------------------- -# 8. Persistent log directory +# 9. Persistent log directory # --------------------------------------------------------------------------- LOG_DIR="/var/log/zopu" mkdir -p "$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..." -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-docker-cleanup.timer zopu-docker-cleanup.service; do SRC="$DEPLOY_DIR/systemd/$unit" @@ -205,7 +221,7 @@ done systemctl daemon-reload # --------------------------------------------------------------------------- -# 10. Firewall (deny-by-default, explicit allow for Tailscale) +# 11. Firewall (deny-by-default, explicit allow for Tailscale) # --------------------------------------------------------------------------- log "Configuring firewall..." if ! ufw status 2>/dev/null | grep -q "Status: active"; then @@ -227,7 +243,7 @@ else fi # --------------------------------------------------------------------------- -# 11. Tailscale (optional) +# 12. Tailscale (optional) # --------------------------------------------------------------------------- if [[ -n "${TAILSCALE_AUTHKEY:-}" ]]; then log "Installing and configuring Tailscale..." @@ -249,7 +265,7 @@ else fi # --------------------------------------------------------------------------- -# 12. Disk-space monitoring cron +# 13. Disk-space monitoring cron # /etc/cron.d format REQUIRES a username field. # --------------------------------------------------------------------------- log "Installing disk-space monitor (daily at 06:00)..." @@ -265,7 +281,7 @@ echo "" echo "Next steps:" echo " 1. Edit $ENV_FILE with real values" 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 " systemctl enable --now zopu-health.timer zopu-docker-cleanup.timer" echo " 4. Verify health:" diff --git a/deploy/zopu-runtime/caddy/Caddyfile b/deploy/zopu-runtime/caddy/Caddyfile index 7d5192b..2be8415 100644 --- a/deploy/zopu-runtime/caddy/Caddyfile +++ b/deploy/zopu-runtime/caddy/Caddyfile @@ -17,10 +17,28 @@ # reverse_proxy localhost:3583 # } # -# --- Public deployment with automatic TLS (only if needed) --- +# --- Public deployment through Cloudflare Tunnel --- # -# agent.example.invalid { -# reverse_proxy localhost:3583 +# Cloudflare terminates TLS and forwards these hosts to Caddy over loopback. +# +# 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. diff --git a/deploy/zopu-runtime/caddy/Caddyfile.production b/deploy/zopu-runtime/caddy/Caddyfile.production new file mode 100644 index 0000000..1a6e9b1 --- /dev/null +++ b/deploy/zopu-runtime/caddy/Caddyfile.production @@ -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 +} diff --git a/deploy/zopu-runtime/cloudflared/config.production.yml b/deploy/zopu-runtime/cloudflared/config.production.yml new file mode 100644 index 0000000..d3a9ac6 --- /dev/null +++ b/deploy/zopu-runtime/cloudflared/config.production.yml @@ -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 diff --git a/deploy/zopu-runtime/convex/docker-compose.yml b/deploy/zopu-runtime/convex/docker-compose.yml new file mode 100644 index 0000000..460ea49 --- /dev/null +++ b/deploy/zopu-runtime/convex/docker-compose.yml @@ -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 diff --git a/deploy/zopu-runtime/scripts/rollback.sh b/deploy/zopu-runtime/scripts/rollback.sh index 33cc1eb..6bd46eb 100755 --- a/deploy/zopu-runtime/scripts/rollback.sh +++ b/deploy/zopu-runtime/scripts/rollback.sh @@ -72,7 +72,10 @@ fi log "Running 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 log "Building agent service..." @@ -82,6 +85,7 @@ log "Restarting services..." systemctl restart zopu-daemon sleep 3 systemctl restart zopu-agent +systemctl restart zopu-web sleep 5 # Health check @@ -94,6 +98,7 @@ if [[ -x "$HEALTH_SCRIPT" ]]; then 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 diff --git a/deploy/zopu-runtime/scripts/update.sh b/deploy/zopu-runtime/scripts/update.sh index bd1ac01..df5cec1 100755 --- a/deploy/zopu-runtime/scripts/update.sh +++ b/deploy/zopu-runtime/scripts/update.sh @@ -77,17 +77,21 @@ fi log "Running 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 log "Building agent service..." 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..." systemctl restart zopu-daemon sleep 3 systemctl restart zopu-agent +systemctl restart zopu-web sleep 5 # Health check @@ -100,6 +104,7 @@ if [[ -x "$HEALTH_SCRIPT" ]]; then 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 diff --git a/deploy/zopu-runtime/systemd/zopu-agent.service b/deploy/zopu-runtime/systemd/zopu-agent.service index 170d58a..39cc638 100644 --- a/deploy/zopu-runtime/systemd/zopu-agent.service +++ b/deploy/zopu-runtime/systemd/zopu-agent.service @@ -10,10 +10,11 @@ Group=__SERVICE_USER__ WorkingDirectory=__INSTALL_DIR__/packages/agents EnvironmentFile=__INSTALL_DIR__/.env +Environment=PORT=3583 -# Flue build output: packages/agents/dist/server.mjs -# Listens on PORT (default 3583 per .env.template). -ExecStart=/usr/local/bin/bun dist/server.mjs +# 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 diff --git a/deploy/zopu-runtime/systemd/zopu-daemon.service b/deploy/zopu-runtime/systemd/zopu-daemon.service index 12488db..cf677f9 100644 --- a/deploy/zopu-runtime/systemd/zopu-daemon.service +++ b/deploy/zopu-runtime/systemd/zopu-daemon.service @@ -10,11 +10,13 @@ Group=__SERVICE_USER__ WorkingDirectory=__INSTALL_DIR__ EnvironmentFile=__INSTALL_DIR__/.env +Environment=HOME=/var/lib/zopu +StateDirectory=zopu # RivetKit in-process engine: envoy mode (serverful). -# registry.start() boots the native Rust sidecar and actor envoy. -# RIVET_ENDPOINT defaults to http://localhost:6420 inside the daemon process. -ExecStart=__INSTALL_DIR__/apps/daemon/dist/code-daemon +# 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 diff --git a/deploy/zopu-runtime/systemd/zopu-web.service b/deploy/zopu-runtime/systemd/zopu-web.service new file mode 100644 index 0000000..1f74557 --- /dev/null +++ b/deploy/zopu-runtime/systemd/zopu-web.service @@ -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 diff --git a/packages/ui/src/components/mobile-workspace/mobile-project-select.tsx b/packages/ui/src/components/mobile-workspace/mobile-project-select.tsx new file mode 100644 index 0000000..bf6e9d9 --- /dev/null +++ b/packages/ui/src/components/mobile-workspace/mobile-project-select.tsx @@ -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) => ( +
+ + + + {activeCount} active +
+); diff --git a/packages/ui/src/components/mobile-workspace/mobile-work-stack-screen.tsx b/packages/ui/src/components/mobile-workspace/mobile-work-stack-screen.tsx index 1e8c6ec..70bbc2e 100644 --- a/packages/ui/src/components/mobile-workspace/mobile-work-stack-screen.tsx +++ b/packages/ui/src/components/mobile-workspace/mobile-work-stack-screen.tsx @@ -10,8 +10,10 @@ import { } from "lucide-react"; import type { FormEvent, MouseEvent } from "react"; +import { MobileProjectSelect } from "./mobile-project-select"; import { MobileSettingsPanel } from "./mobile-settings-panel"; import type { + MobileProjectView, MobileWorkspaceView, MobileWorkUnitTone, MobileWorkUnitView, @@ -41,12 +43,18 @@ const dateLabel = new Intl.DateTimeFormat("en", { interface WorkStackHeaderProps { readonly activeCount: number; readonly onOpenSettings?: () => void; + readonly onProjectSelect: (projectId: string) => void; + readonly projects: readonly MobileProjectView[]; + readonly selectedProjectId?: string; readonly userInitials: string; } const WorkStackHeader = ({ activeCount, onOpenSettings, + onProjectSelect, + projects, + selectedProjectId, userInitials, }: WorkStackHeaderProps) => (
@@ -57,10 +65,12 @@ const WorkStackHeader = ({

Zopu

-

- - {activeCount} active work units -

+