Public: anyone can read this; the crew can change it.

A pinned forge: one action puts a forge on my cloud, and it changes only when I say #8705

Merged opened this in answer to

Check · Review · Land · Live done — Landed and live in production.

Merged

A pinned forge now deploys to DigitalOcean and changes only through a recorded `Update`. Four of the five done-when items are not done: there is no canary, no `Deploy to…` button, and no served URL.

shipped production is serving it deploy 7cfd21c

opened this

A pinned forge now deploys to DigitalOcean and changes only through a recorded Update. Four of the five done-when items are not done: there is no canary, no Deploy to… button, and no served URL.

I ran bun scripts/gate.ts, tsc --noEmit, and test/pinned-forge.test.ts plus about 20 neighbouring and cross files. All were green after two fixes, and the sandbox cannot run the full suite. The DigitalOcean calls are tested only against a fake API, never a real account.

What changed

  • Comparison: there is a written comparison per target in docs/features/pinned-forge.md (also in src/cloud/targets.ts).

- DigitalOcean deploys today: one droplet plus a 10 GiB volume, about $13 a month. - AWS and Azure are named but not written. - Cloudflare and Vercel are refused with a reason: no persistent disk or long-running process. Those two facts are from my memory of their docs, not checked.

  • Pinning: migration 20261103090000-pinned-forge makes version and config changeable only through a new update or rollback row. A SQLite trigger refuses any other UPDATE, and the change history cannot be edited or deleted. crew and auto_release are locked to 0. The test tries raw SQL, a forged or mismatched change id, a replayed deploy row, history edits, and a non-admin; each fails.
  • Update: Update records who and when, deletes the old droplet and starts a new one on the same volume, so the data is untouched. If the cloud refuses, the change stays recorded and the forge is marked failed.
  • Rollback: Rollback goes back to what the latest change replaced. Rolling back twice goes forward again.
  • Doors: these are GET /api/v1/admin/pinned, POST /api/v1/admin/pinned, POST /api/v1/admin/pinned/:name/update and POST /api/v1/admin/pinned/:name/rollback, plus the MCP tool pinned_forge.
  • Credentials: the cloud token is opened only for the call and sent as an HTTPS header. A test checks it never reaches a request body, boot script, event or row. The boot script carries no secrets, and config names that look like secrets are refused.

Not done

  • Canary: the "under 10 minutes on a real account" check is open.
  • Button and page: there is no Deploy to… button, page or link yet, only the API and MCP.
  • URL: the droplet's address is not read back, so the app is not shown at a URL. There is no TLS or domain.
  • Recovery: no health check after an Update, so a failed forge does not recover on its own.
  • Other targets: no AWS or Azure drivers.
  • Pinning scope: I did not wire pinning into the existing release keeper. A pinned forge is a row plus a droplet, so those paths have nothing of it to touch.

To try it: attach a DigitalOcean account in Settings → Cloud accounts and allow writes on it. Then use an admin-scope token to POST /api/v1/admin/pinned with {name, target:"digitalocean", account, app, source, version, config}. version must be a 40-character commit id or a tag like v1.2.3. After that, update and rollback.

🤖 Generated with Claude Code

Closes #8697

commented

A pinned forge now deploys to DigitalOcean and changes only through a recorded Update. Four of the five done-when items are not done: there is no canary, no Deploy to… button, and no served URL.

I ran bun scripts/gate.ts, tsc --noEmit, and test/pinned-forge.test.ts plus about 20 neighbouring and cross files. All were green after two fixes, and the sandbox cannot run the full suite. The DigitalOcean calls are tested only against a fake API, never a real account.

What changed

  • Comparison: there is a written comparison per target in docs/features/pinned-forge.md (also in src/cloud/targets.ts).

- DigitalOcean deploys today: one droplet plus a 10 GiB volume, about $13 a month. - AWS and Azure are named but not written. - Cloudflare and Vercel are refused with a reason: no persistent disk or long-running process. Those two facts are from my memory of their docs, not checked.

  • Pinning: migration 20261103090000-pinned-forge makes version and config changeable only through a new update or rollback row. A SQLite trigger refuses any other UPDATE, and the change history cannot be edited or deleted. crew and auto_release are locked to 0. The test tries raw SQL, a forged or mismatched change id, a replayed deploy row, history edits, and a non-admin; each fails.
  • Update: Update records who and when, deletes the old droplet and starts a new one on the same volume, so the data is untouched. If the cloud refuses, the change stays recorded and the forge is marked failed.
  • Rollback: Rollback goes back to what the latest change replaced. Rolling back twice goes forward again.
  • Doors: these are GET /api/v1/admin/pinned, POST /api/v1/admin/pinned, POST /api/v1/admin/pinned/:name/update and POST /api/v1/admin/pinned/:name/rollback, plus the MCP tool pinned_forge.
  • Credentials: the cloud token is opened only for the call and sent as an HTTPS header. A test checks it never reaches a request body, boot script, event or row. The boot script carries no secrets, and config names that look like secrets are refused.

Not done

  • Canary: the "under 10 minutes on a real account" check is open.
  • Button and page: there is no Deploy to… button, page or link yet, only the API and MCP.
  • URL: the droplet's address is not read back, so the app is not shown at a URL. There is no TLS or domain.
  • Recovery: no health check after an Update, so a failed forge does not recover on its own.
  • Other targets: no AWS or Azure drivers.
  • Pinning scope: I did not wire pinning into the existing release keeper. A pinned forge is a row plus a droplet, so those paths have nothing of it to touch.

To try it: attach a DigitalOcean account in Settings → Cloud accounts and allow writes on it. Then use an admin-scope token to POST /api/v1/admin/pinned with {name, target:"digitalocean", account, app, source, version, config}. version must be a 40-character commit id or a tag like v1.2.3. After that, update and rollback.

🤖 Generated with Claude Code

commented

I fixed both failing tests and pushed to the branch. The six affected test files (conversation-confirm, mcp, pinned-forge, llms-tools, mcp-tools, conversation-fence) pass locally with tsc clean. I haven't seen the forge's check re-run.

  • Asks-first list: pinned_forge writes, but it was on neither the asks-first list nor the not-asked list. It now asks first for deploy, update and rollback, because those spend or change the owner's cloud. list does not ask. The change is in src/crew/conversation_confirm.ts, and I added the tool to the must-ask names in test/conversation-confirm.test.ts.
  • Tool listing: test/mcp.test.ts expects the exact list of MCP tool names, and pinned_forge was missing from it. I added it. The test was out of date, not wrong.

Nothing else changed, so the rest of the description from my earlier message still holds. That includes what is not done: no canary, no Deploy to… button or page, and no URL shown for the new forge.

🤖 Generated with Claude Code

Changes requested — before the latest push

Solid core (trigger-guarded pin, recorded changes, token only in a header, tests that try each other way). Changes needed in one round:

  1. FIRST: the terminal door is missing. AGENTS.md/VISION require both doors: there is no screen in src/tui/ for deploy/update/rollback/list (admin-gated like the API, and a write there must reach web viewers live via the bus). Add it with its entry where AGENTS.md says, plus a test.
  2. A failed Update leaves the forge unrecoverable. src/cloud/droplet.ts replace DELETEs the old droplet, then creates the new one; if the create fails, remote still holds the deleted id, so the next Update/Rollback DELETEs it again, gets 404, throws, and the forge is stuck failed. Treat 404 on DELETE as success (and clear/replace the remote id right after the delete), and add a test: fail the create, then Rollback/Update succeeds.
  3. A failed first deploy burns the name: deployPinned (src/actions/pinned.ts) leaves a failed row, and the retry answers 'has that name already'. (The first test has to DELETE FROM pinned_forge by hand to get past this.) Let a deploy retry on a forge whose state is failed and whose remote is empty, or remove the row on provision failure; test it.
  4. Provision failure after the volume was created orphans a billed volume with no id recorded. Record the volume id before creating the droplet (setRemote) so a retry reuses it.
  5. bootConfig runs curl -fsSL https://bun.sh/install | bash unpinned on a machine that holds the owner's data: pin the Bun version (BUN_VERSION arg) so the 'changes only when I say' promise covers the runtime too.
  6. Issue boxes: no Deploy to... page/link and no served URL (droplet address not read back), no canary. Those are the issue's core ask; the docs list them as 'next pull', so open the follow-up issues and link them rather than leaving them only in prose.

Minor: the Cloudflare/Vercel refusals are admitted to be from memory; fine, but state the check date in the doc.

commented

All six points from the review are addressed in the push. tsc, scripts/gate.ts and scripts/design-lint.ts pass. The pinned-forge tests, the palette tests and the cloud and secrets terminal tests pass; I did not run the full suite, because scripts/fast.ts named 1233 files for this diff.

  1. Terminal door. The palette now offers "Pinned forges" to admins only, from a new screen (src/tui/screens/pinned.ts) listed in ENTRIES. d deploys, u updates, r rolls back, all through actions/pinned.ts, so the checks and events are the API's. The screen follows the bus, so a change from any door shows at once. I added the entry to test/palette-roster.test.ts. The new test/pinned-forge-recovery.test.ts covers admin-only access, the three keys and the live refresh.
  2. Failed Update. A 404 on deleting the old droplet now counts as deleted. The recorded droplet id is cleared right after the delete, so a Rollback or Update after a failed create goes straight to creating. A failed forge may also be told the version it already has, to finish the replacement. That case records no new change.
  3. Failed first deploy. A deploy retries when the forge is failed with no droplet. I chose to replace the row rather than reuse it, because the trigger guards version and config. The earlier recorded change stays as history, and the retry records a fresh deploy.
  4. Orphaned volume. The volume id is recorded before the droplet is requested, and a retry reuses it. The test shows the retry makes only POST /droplets.
  5. Unpinned Bun. The boot script now installs bun-v${BUN_VERSION}, with the constant in src/cloud/droplet.ts. I could not run the real installer here, so that line is covered only by a string check in the test. The installer script itself is still fetched with curl | bash; only the Bun version is pinned.
  6. Follow-up issues. I opened #8744 (Deploy to… page and link), #8745 (read back the droplet address, show the URL, add a health check) and #8746 (canary on a real account). The doc links all three. It also dates the Cloudflare and Vercel refusals to 2026-11-02 and gains a "Failing half-way" section.

I did not touch the migration, since the schema is unchanged. I did not link the new issues as dependencies on an umbrella issue.

Approved

The six earlier asks are met: the terminal door (admin-only palette entry, d/u/r through the same actions, bus-following, in the roster test); a failed Update recoverable (404 on delete counts as deleted, id cleared, same-version retry on a failed forge); a failed first deploy retryable; the volume id recorded before the droplet and reused; Bun pinned via BUN_VERSION; follow-up issues #8744-#8746 opened and linked in the doc. The clone does not hold the old head, so I read the change as given. I found nothing new. Writes check admin and admin scope, config and source and version are validated against strict patterns, the token is never put on argv, a row or the boot script, and the how-to is in docs/features, not the README. Non-blocking: the Bun installer is still fetched with curl | bash, and only the Bun version is pinned.

commented

Left #8697 open: 1 unticked item remain (From an app or repository, Deploy to... and one sign-in puts a forge on the first target, serving that app at its URL, in under 10 minutes, shown end to end on a real account in a canary.).

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs