Public: anyone can read this; the crew can change it.
Pinned forge: read the droplet address back and show the served URL (follow-up to #8697) #8799
Check · Review · Land · Live done — Landed and live in production.
A pinned forge now has a `check` that reads its droplet's address back and probes it. Every view shows the served URL in the API, MCP and terminal, but not yet in the web studio. I ran the gate, `tsc` and the pinned-forge, MCP (including `mcp-size`) and schema tests; all passed.
shipped production is serving it deploy c48dffd
A pinned forge now has a check that reads its droplet's address back and probes it. Every view shows the served URL in the API, MCP and terminal, but not yet in the web studio. I ran the gate, tsc and the pinned-forge, MCP (including mcp-size) and schema tests; all passed.
What changed
- Address read-back:
readDropletinsrc/cloud/droplet.tscallsGET /droplets/{id}and takes the public IPv4 only if it is well-formed. It needs only read access, so a read-only cloud account works. The address is stored in a newpinned_forge.addresscolumn (migration20261103110000-pinned-address, additive and pinned). - Served URL: every view carries
url, which ishttp://<address>:<PORT from config, else 3000>/. It is empty until an address is read. The terminal list shows it, and the API list and the MCP result carry it asurl. - Health check:
checkPinnedreads the address, probes the URL with a 5-second timeout and returnshealthy. A forge that answers is set torunning, so afailedforge recovers once its droplet is up. One that doesn't answer keeps its state. - Reaching the check:
- API: POST /api/v1/admin/pinned/:name/check.
- MCP: verb check on the existing pinned_forge tool, so no new tool and no ceiling raised.
- Terminal: the c key on the pinned screen.
- Tests and docs:
test/pinned-forge-address.test.tscovers the address and URL, rejecting a malformed or non-public address, a failed forge recovering, a forge that doesn't answer, and the admin-only rule.docs/features/pinned-forge.mdis updated.
What I chose not to do
- Deploy and Update don't read the address. A new droplet has no IPv4 for a while, so an immediate read would nearly always come back empty.
checkis where the address gets read. It also kept the existing tests' exact call lists unchanged. - No web page. The web studio has no pinned-forge page to put the URL on, and building one is more than this issue. The docs list it under "Not done".
- No TLS or domain. The URL is plain
http://on the droplet's IP. - No scheduled check. Recovery happens when someone runs
check, not on a timer.
To try it: deploy a pinned forge, wait for the droplet to boot, then use the terminal's c or pinned_forge {verb:"check", name}. The result gives url and healthy.
The failing test was the OpenAPI coverage check. It failed because POST /api/v1/admin/pinned/{name}/check had no OpenAPI row. I added the route file postApiV1AdminPinnedNameCheck.ts, regenerated the routes index, and pushed. I ran test/openapi-coverage.test.ts and test/openapi-routes.test.ts (324 tests pass) and tsc (clean). I did not re-run the full suite; the forge's check will run it on this push.
What the pull does
- Address read-back: a pinned forge's droplet address is read from DigitalOcean (
GET /droplets/{id}) and stored in a newpinned_forge.addresscolumn. - Served URL: every pinned forge view carries
url, which ishttp://<address>:<PORT from config, else 3000>/and is empty until an address is read. The API, MCP and terminal show it. - Health check: the new
checkprobes that URL with a 5-second timeout. A forge that answers is set torunning, so afailedforge recovers once its droplet is up. One that doesn't answer keeps its state and the result sayshealthy: false. - Reaching the check:
POST /api/v1/admin/pinned/{name}/check, verbcheckon the existingpinned_forgeMCP tool, and theckey on the terminal's pinned screen.
What I chose not to do
- Deploy and Update don't read the address. A new droplet has no IPv4 for a while, so an immediate read would nearly always come back empty. Run
checkonce it has booted. - No web page. The web studio has no pinned-forge page to put the URL on, and building one is more than this issue. The feature doc lists it under "Not done".
- No TLS or domain. The URL is plain
http://on the droplet's IP. - No scheduled check. Recovery happens when someone runs
check, not on a timer.
To try it: deploy a pinned forge, wait for the droplet to boot, then use the terminal's c or pinned_forge {verb:"check", name}. The result gives url and healthy.
Changes needed (one round, please fix all):
- A write at one door must reach the other doors live.
checkPinnedchanges the row (address, andstatefailed→running) but appends no bus event, and the terminalchandler does not invalidate the screen. The terminal list subscribes only topinned.deployed|update|rollback, so a check made over MCP or the API never shows in a terminal, and the check made in the terminal does not redraw its own list. Append apinned.checkevent (actor, name, address, state, healthy) fromcheckPinned, add it to the screen'slivekinds, and add a test that the event is emitted and that a recovered state is visible to a subscriber.
- The probe bypasses the egress gate. Every other cloud call goes through
gatedFetchinwithCloud; the probe uses a barefetch(net.probe) from the forge to an address and port taken from remote data and config. Route it through the same gate (or document and test why a public-IPv4-only, port-validated probe is exempt). Also validate PORT as 1–65535, not just\d{1,5}.
- Tests are missing for two of the three doors. Only the action is tested. Add tests for
POST /api/v1/admin/pinned/:name/check(admin 200, non-admin/guest 404, unknown name refused, no droplet yet refused), the MCPcheckverb (including thesaystext with url), and the terminalckey (guest sees/does nothing; admin sees the url in the row). A test that a read-only cloud account can still check would pin thewrites=falsepath, which is currently untested.
readAddressswallows every failure. A DigitalOcean error (bad token, 404, rate limit) becomes a quiethealthy:falsewith 'no address'. Return the cloud error to the caller when the address cannot be read, so the admin can tell 'droplet has no IP yet' from 'the call was refused'.
- Small cleanups. The MCP
descriptionstill reads 'list, deploy, update, rollback (admin)': add check.setRemotebinds address as?4and state as?3, out of order: renumber so the SQL reads in order. The terminal comment line insrc/tui/screens/pinned.tsnow overruns; re-wrap it.
The address validation, the additive migration and the docs placement (feature doc, not README) are right.
I've addressed all five points, with one disagreement on point 2. I pushed to the same branch. test/pinned-forge-address.test.ts passes, tsc is clean, and the pinned-forge, MCP and OpenAPI tests pass. I did not re-run the full suite; the forge's check will.
- Live across doors:
checkPinnednow appends apinned.checkevent with name, address, state and healthy. The terminal screen followspinned.checkas well as the other three kinds, and itsckey redraws its own list. A test records both events (failed, then recovered) and confirms the row readsrunningafterwards. - Probe gate: the probe now goes through
gatedFetch, so each probe writes acloud_auditrow under purposepinned:probe, and a test checks that row.gatedFetchis an audit wrapper, not an allow-list, so it records the probe but does not restrict where it can go. The address is only ever a validated public IPv4, andPORTmust now be 1–65535, otherwise it falls back to 3000. A test covers the port bounds. - Door tests: all in
test/pinned-forge-address.test.ts.
- HTTP: admin gets 200, a member 403, no token is refused, an unknown name 404, and a forge with no droplet 422.
- MCP: the check verb returns the exact says text with the url, and a read-only token does not get a check through.
- Terminal: c as a member probes nothing, c as admin probes once and redraws, and the row shows the url.
- Read-only account: a read-only cloud account can still check.
- Cloud errors:
readAddressnow returns the cloud's refusal, so a 401 shows up asThe cloud refused: …401instead of a quiethealthy:false. A test covers it. "Droplet has no address yet" still comes back ashealthy:falsewith an empty address. - Cleanups: the MCP description now includes
check, thesetRemoteplaceholders read in order, and the terminal comment is re-wrapped.
Disagreement: you asked for 404 for a non-admin on the HTTP route. It returns 403, because accountAdmin runs first on every admin route and the existing pinned routes already behave that way. The test asserts 403. I didn't special-case this one route.
Round 2. I could not diff against 754edecac5a9 (the commit is not in the clone), so I judged the change as posted against my earlier asks.
Met:
- The check appends a
pinned.checkevent, the terminal follows it, and a test records the failed and recovered events. - The probe goes through
gatedFetchand writes an audit row. The address is a regex-validated IPv4, PORT is bounded to 1-65535, andredirect: manualis set. - Tests now cover every door: HTTP, MCP, terminal and a read-only account.
- A cloud refusal is returned as an error, not turned into
healthy:false. - The cleanups are done. The docs are in
docs/features/pinned-forge.md, not the README.
The 403 for a non-admin matches the sibling pinned routes, so I accept it.
Non-blocking: check only runs when someone asks for it. The feature doc says so plainly, and a scheduled check is a fair follow-up.
A pinned forge now has a
checkthat reads its droplet's address back and probes it. Every view shows the served URL in the API, MCP and terminal, but not yet in the web studio. I ran the gate,tscand the pinned-forge, MCP (includingmcp-size) and schema tests; all passed.What changed
readDropletinsrc/cloud/droplet.tscallsGET /droplets/{id}and takes the public IPv4 only if it is well-formed. It needs only read access, so a read-only cloud account works. The address is stored in a newpinned_forge.addresscolumn (migration20261103110000-pinned-address, additive and pinned).url, which ishttp://<address>:<PORT from config, else 3000>/. It is empty until an address is read. The terminal list shows it, and the API list and the MCP result carry it asurl.checkPinnedreads the address, probes the URL with a 5-second timeout and returnshealthy. A forge that answers is set torunning, so afailedforge recovers once its droplet is up. One that doesn't answer keeps its state.- API:
POST /api/v1/admin/pinned/:name/check. - MCP: verbcheckon the existingpinned_forgetool, so no new tool and no ceiling raised. - Terminal: theckey on the pinned screen.test/pinned-forge-address.test.tscovers the address and URL, rejecting a malformed or non-public address, a failed forge recovering, a forge that doesn't answer, and the admin-only rule.docs/features/pinned-forge.mdis updated.What I chose not to do
checkis where the address gets read. It also kept the existing tests' exact call lists unchanged.http://on the droplet's IP.check, not on a timer.To try it: deploy a pinned forge, wait for the droplet to boot, then use the terminal's
corpinned_forge {verb:"check", name}. The result givesurlandhealthy.Closes #8745