|
| 1 | +# Coding-Agent Project Runtime |
| 2 | + |
| 3 | +This document is the integration contract for the **coding-agent runtime**: |
| 4 | +the layer that lets fastclaw scaffold a project from a template, run its |
| 5 | +dev server in a long-lived sandbox, and hand back a live preview URL. The |
| 6 | +upstream SaaS shell drives everything through the HTTP API below — it |
| 7 | +never touches the sandbox, the LLM, or the filesystem directly. |
| 8 | + |
| 9 | +## Mental model: two layers, one project |
| 10 | + |
| 11 | +| Layer | Owns | Table | Lifecycle | |
| 12 | +|-------|------|-------|-----------| |
| 13 | +| **Project** (pre-existing) | the source tree (a shared workspace folder) + chat grouping | `projects` | created by the user, persists | |
| 14 | +| **Project Runtime** (new) | the *running instance* of that tree: a long-lived dev-server container + preview URL | `project_runtimes` | booted on demand, evictable | |
| 15 | + |
| 16 | +A runtime is 1:1 with a project, keyed by the same `(user_id, agent_id, |
| 17 | +project_id)`. The two are separate tables on purpose: dropping every |
| 18 | +`project_runtimes` row degrades gracefully to "no previews" and never |
| 19 | +touches chat history or workspace files. The existing project feature is |
| 20 | +byte-for-byte unchanged. |
| 21 | + |
| 22 | +**Why the preview reflects agent edits live:** the runtime container and |
| 23 | +the agent's per-turn sandbox bind-mount the *same* host directory |
| 24 | +(`workspaces/<agent>/projects/<pid>/`). When the agent writes a file |
| 25 | +during a turn, the dev server in the runtime container sees it instantly |
| 26 | +and HMR reloads. No file sync — the bind mount is the channel. |
| 27 | + |
| 28 | +## HTTP API |
| 29 | + |
| 30 | +All endpoints are under the existing per-agent auth (`requireAgentReadable` |
| 31 | +for reads, `requireWritable` for mutations). Ownership is scoped to the |
| 32 | +caller's `user_id`, identical to `/projects`. |
| 33 | + |
| 34 | +If the deployment hasn't wired a runtime manager (non-docker sandbox |
| 35 | +backend, or `SetRuntimeManager` never called), every endpoint returns |
| 36 | +`503 {"error":"project runtime not enabled on this deployment"}`. |
| 37 | + |
| 38 | +### Runtime record shape |
| 39 | + |
| 40 | +```jsonc |
| 41 | +{ |
| 42 | + "projectId": "proj_ab12…", |
| 43 | + "templateRef": "shipany-tanstack", |
| 44 | + "status": "running", // none|scaffolding|starting|running|sleeping|crashed |
| 45 | + "devPort": 3000, // container-internal dev server port |
| 46 | + "hostPort": 49210, // published host port (0 when sleeping) |
| 47 | + "previewUrl": "https://proj_ab12.preview.example.com", |
| 48 | + "gitRef": "abc123", // last snapshot commit (for revert) |
| 49 | + "lastError": "", // populated on status=crashed |
| 50 | + "createdAt": "2026-06-12T…", |
| 51 | + "updatedAt": "2026-06-12T…" |
| 52 | +} |
| 53 | +``` |
| 54 | + |
| 55 | +### Endpoints |
| 56 | + |
| 57 | +| Method & path | Purpose | Body | Returns | |
| 58 | +|---|---|---|---| |
| 59 | +| `GET /api/agents/{id}/projects/{pid}/runtime` | current state | — | runtime record, or `404` if none | |
| 60 | +| `POST /api/agents/{id}/projects/{pid}/runtime/up` | provision + boot (idempotent) | `{"templateRef":"shipany-tanstack"}` (required on first boot, ignored after) | runtime record (`status:running`) | |
| 61 | +| `POST /api/agents/{id}/projects/{pid}/runtime/sleep` | stop container, keep files | — | `{"ok":true,"status":"sleeping"}` | |
| 62 | +| `POST /api/agents/{id}/projects/{pid}/runtime/wake` | re-boot a sleeping runtime | — | runtime record | |
| 63 | +| `DELETE /api/agents/{id}/projects/{pid}/runtime` | tear down container + forget runtime (files kept) | — | `{"ok":true}` | |
| 64 | +| `GET /api/agents/{id}/projects/{pid}/preview` | preview URL + status only | — | `{"previewUrl":…,"status":…}` | |
| 65 | +| `GET /api/agents/{id}/projects/{pid}/runtime/logs?tail=200` | dev-server log tail | — | `{"logs":"…"}` | |
| 66 | + |
| 67 | +`up` and `wake` may take minutes (scaffold + `pnpm install`); the handler |
| 68 | +allows a 10-minute deadline. The SaaS should show a "building…" state and |
| 69 | +poll `GET …/runtime` (or `…/preview`) until `status` is `running` or |
| 70 | +`crashed`. |
| 71 | + |
| 72 | +### Typical SaaS flow ("make me an X") |
| 73 | + |
| 74 | +1. `POST /api/agents/{id}/projects` → create the project (existing API), get `pid`. |
| 75 | +2. Send the build instruction via the existing chat API (`/api/chat/stream`) |
| 76 | + with `projectId=pid`. The coding-agent persona customizes the template. |
| 77 | +3. `POST …/{pid}/runtime/up` with `templateRef` → boots the dev server. |
| 78 | +4. Poll `GET …/{pid}/preview` until `status=running`, then iframe `previewUrl`. |
| 79 | +5. Further edits: just send more chat turns. HMR reflects them; no re-up needed. |
| 80 | +6. Idle: `POST …/sleep` to free compute; `POST …/wake` when the user returns. |
| 81 | + |
| 82 | +## Using it from fastclaw's own web chat (no SaaS shell) |
| 83 | + |
| 84 | +The runtime is also wired into the agent loop as two tools, so you can |
| 85 | +dogfood the whole loop in fastclaw's built-in web chat: |
| 86 | + |
| 87 | +- `start_app_preview` — scaffolds the project from the template (first |
| 88 | + call), boots the dev server, returns the preview URL. |
| 89 | +- `app_preview_logs` — tails the dev-server log to debug a bad edit. |
| 90 | + |
| 91 | +These tools appear **only** on agents that have a runtime wired |
| 92 | +(`SetProjectRuntime`), so ordinary agents are unaffected. When present, |
| 93 | +the system-prompt guidance flips from "don't start dev servers" to "use |
| 94 | +`start_app_preview` for web-app projects." |
| 95 | + |
| 96 | +**Where the app is homed.** `start_app_preview` works in any chat: |
| 97 | + |
| 98 | +- **Inside a project** → the app is homed at the project root |
| 99 | + (`projects/<pid>/`), shared and persistent across the project's chats. |
| 100 | + A coding agent's file tools address that root (not a per-chat subdir), |
| 101 | + so its edits land where the dev server serves them and HMR reloads. |
| 102 | +- **In a loose chat** (no project) → the app is homed in the chat's own |
| 103 | + workspace (`sessions/<sid>/`), which is exactly where the agent's edits |
| 104 | + already go. Great for one-off demos; the app lives with that chat. |
| 105 | + |
| 106 | +So you do **not** need to pre-create a project — it's an optional upgrade |
| 107 | +for persistence/sharing. Plain agents (no runtime wired) are unaffected |
| 108 | +and keep per-chat isolation. |
| 109 | + |
| 110 | +### Dogfood steps |
| 111 | + |
| 112 | +1. Run fastclaw with the docker sandbox backend and a template source |
| 113 | + (see env vars below). For a local template checkout: |
| 114 | + ``` |
| 115 | + FASTCLAW_SHIPANY_TEMPLATE_DIR=/Users/you/code/shipany-tanstack |
| 116 | + ``` |
| 117 | + The sandbox image still needs node + pnpm. |
| 118 | +2. Open any chat (a project chat for a persistent app, or just a new |
| 119 | + loose chat for a quick demo). |
| 120 | +3. Say e.g. *"用 shipany 模板做个 AI 抠图落地页"*. The agent calls |
| 121 | + `start_app_preview` (scaffold + boot), edits the template's copy/theme, |
| 122 | + and replies with a preview URL. Leave `FASTCLAW_PREVIEW_BASE` empty and |
| 123 | + it's `http://127.0.0.1:<port>` — open it directly. |
| 124 | +4. Keep chatting to iterate; HMR reflects edits live. |
| 125 | + |
| 126 | +> Note: the agent's `exec` tool still cwd's into the per-chat sandbox |
| 127 | +> subdir, so the runtime (not the agent) owns build/install/serve. The |
| 128 | +> agent edits files; the dev server rebuilds. If the agent needs to run a |
| 129 | +> project command itself it should `cd /workspace` first. |
| 130 | +
|
| 131 | +## Server wiring |
| 132 | + |
| 133 | +`cmd/fastclaw/main.go` constructs the manager and registers the |
| 134 | +`shipany-tanstack` template when a home dir resolves. It's active for the |
| 135 | +docker sandbox backend; other backends leave the endpoints at `503`. |
| 136 | + |
| 137 | +Template commands are env-overridable so fastclaw stays template-agnostic: |
| 138 | + |
| 139 | +| Env var | Default | Meaning | |
| 140 | +|---|---|---| |
| 141 | +| `FASTCLAW_PREVIEW_BASE` | _(empty)_ | Preview URL template. Empty → `http://127.0.0.1:<hostPort>` (local). Set to `https://{project}.preview.example.com` for the wildcard gateway (the `{project}` token is replaced with the project id). | |
| 142 | +| `FASTCLAW_SHIPANY_SCAFFOLD` | `if [ -d /template ]; then cp -a /template/. /workspace/; fi; cd /workspace && (pnpm install \|\| npm install)` | Shell run once in `/workspace` when it's empty. Populates the source tree + installs deps. | |
| 143 | +| `FASTCLAW_SHIPANY_DEV` | `pnpm dev --host 0.0.0.0 --port 3000` | Shell that starts the dev server bound to `0.0.0.0:3000`. | |
| 144 | +| `FASTCLAW_SHIPANY_TEMPLATE_DIR` | _(empty)_ | Host dir bind-mounted read-only at `/template` in the runtime container (option C). Set to a local checkout (e.g. `~/code/shipany-tanstack`) to scaffold from disk — no image bake, no git clone. The default scaffold's `cp -a /template/.` then works. | |
| 145 | + |
| 146 | +To add another template (e.g. a Next.js starter), call |
| 147 | +`rtMgr.RegisterTemplate("my-template", coderuntime.TemplateSpec{…})` — |
| 148 | +nothing in the runtime is ShipAny-specific. |
| 149 | + |
| 150 | +### Sandbox image requirements |
| 151 | + |
| 152 | +The runtime reuses the sandbox image (`FASTCLAW_SANDBOX_IMAGE`). For the |
| 153 | +ShipAny template that image must have **node + pnpm** and the template |
| 154 | +source baked at `/template` (so the default scaffold's `cp -a /template/.` |
| 155 | +works). Alternatively override `FASTCLAW_SHIPANY_SCAFFOLD` to `git clone` |
| 156 | +the template instead. |
| 157 | + |
| 158 | +## Preview gateway (deployment-side, NOT in this repo) |
| 159 | + |
| 160 | +The runtime publishes the dev port to `127.0.0.1:<hostPort>` on the host — |
| 161 | +deliberately **not** `0.0.0.0`, because the container runs LLM-generated |
| 162 | +code and must never be directly reachable. Turning `hostPort` into a |
| 163 | +shareable URL is a reverse proxy you deploy alongside fastclaw: |
| 164 | + |
| 165 | +``` |
| 166 | +*.preview.example.com |
| 167 | + │ (wildcard DNS + wildcard TLS, e.g. Caddy / Traefik) |
| 168 | + ▼ |
| 169 | + preview gateway ──looks up subdomain (= project_id) in project_runtimes──▶ 127.0.0.1:<hostPort> |
| 170 | +``` |
| 171 | + |
| 172 | +Gateway responsibilities: |
| 173 | + |
| 174 | +- **Subdomain → host port.** Resolve `proj_ab12.preview.example.com` to the |
| 175 | + `host_port` of that project's `project_runtimes` row (query the same DB, |
| 176 | + or add a small internal lookup endpoint). |
| 177 | +- **Wildcard TLS** for `*.preview.example.com` (Let's Encrypt DNS-01). |
| 178 | +- **WebSocket passthrough** — Vite HMR runs over WS. Without it, edits |
| 179 | + won't hot-reload. The template must also advertise the *published host |
| 180 | + port* in its HMR config (`server.hmr.clientPort`), since inside the |
| 181 | + container the dev server only knows port 3000. |
| 182 | + |
| 183 | +Set `FASTCLAW_PREVIEW_BASE=https://{project}.preview.example.com` so the |
| 184 | +runtime records gateway-shaped URLs; the gateway does the port mapping. |
| 185 | + |
| 186 | +For local development leave `FASTCLAW_PREVIEW_BASE` empty and hit |
| 187 | +`http://127.0.0.1:<hostPort>` directly — no gateway needed. |
| 188 | + |
| 189 | +## What's intentionally left to the integrator |
| 190 | + |
| 191 | +- **Git snapshot / revert.** The record carries `gitRef`; wiring a |
| 192 | + `git commit` after each turn and a `/git/revert` endpoint is a thin |
| 193 | + follow-up using `Manager.Exec` (it runs commands in the runtime |
| 194 | + container). Not built yet. |
| 195 | +- **Idle auto-sleep.** `ListAllProjectRuntimes` exists for a sweeper, but |
| 196 | + the background eviction loop for runtimes is not wired (the per-turn |
| 197 | + pool has its own; this is the long-lived layer). Add a ticker that |
| 198 | + `Sleep`s runtimes idle past a TTL. |
| 199 | +- **Deploy.** "Ship to production" reuses the template's own deploy skill |
| 200 | + (e.g. ShipAny's `deploy-cloudflare`) via a chat turn or `Manager.Exec`. |
0 commit comments