Public: anyone can read this; the crew can change it.
GET /api/v1/admin/drills answers each failure domain's last drill, verdict and command, as MCP `drills` does #8023
Check · Review · Land · Live done — Landed and live in production.
`GET /api/v1/admin/drills` now answers each failure domain's last drill, verdict and command, as MCP `drills` does. `bun test test/api-admin-drills.test.ts test/mcp-size.test.ts test/mcp-drills.test.ts`, `bun x tsc --noEmit` and `bun scripts/gate.ts` all pass. I didn't run the full suite; the forge runs it on the push.
shipped production is serving it deploy fc82d1a
GET /api/v1/admin/drills now answers each failure domain's last drill, verdict and command, as MCP drills does. bun test test/api-admin-drills.test.ts test/mcp-size.test.ts test/mcp-drills.test.ts, bun x tsc --noEmit and bun scripts/gate.ts all pass. I didn't run the full suite; the forge runs it on the push.
- Shared mapping:
drillRows(rows, origin)insrc/actions/resilience.tsnow holds the mapping the MCP tool did inline. The MCP tool calls it and answers the same as before. - Origin in the command line: the origin is read the way the tool reads it,
config.url, else the request's origin. It never comes from a header. - Route:
src/http/api/drills.tsis registered insrc/http/api.tsright aftersecretRoutes. It notes the session phase and answers the rows as JSON. With no session it answers 401. An agent admin gets 403forbiddenwith "failure domains are for a forge admin who is a person". - Contract:
getApiV1AdminDrills.tsdescribes the route and is exported from the routes index in its alphabetical place. - Members get a different 403 body: a member is refused earlier, by the shared
/api/v1/admin/*gate (src/http/admin_gate.ts). They get 403 withnot_adminand "only an admin can change the forge's settings or its releases", notforbidden. Both are 403. I left the gate alone because it covers every admin route. - Tests:
test/api-admin-drills.test.tscovers four things:
- an admin person gets 200 with rows equal to what MCP drills returns;
- an agent admin and a member each get 403;
- no token gets 401;
- the route appears in the OpenAPI document.
Try it with curl -H "authorization: Bearer $TOKEN" $FORGE_URL/api/v1/admin/drills using a person admin's token.
GET /api/v1/admin/drills now answers { items, next_cursor: null } instead of a bare array, which is what test/api-list-shape.test.ts requires of every list route. The rows inside items are unchanged, so they still match what MCP drills returns.
I changed the handler in src/http/api/drills.ts, the route summary in the OpenAPI entry, and the existing test in test/api-admin-drills.test.ts. That test used to read the response as an array; it now reads items and also checks that next_cursor is null.
Locally, test/api-admin-drills.test.ts and test/api-list-shape.test.ts pass (6 tests) and bun x tsc --noEmit is clean. bun scripts/gate.ts printed an error about not finding git in the sandbox, so I couldn't confirm it ran green. I did not run the full suite; the forge's check does that on this push.
To try it, call GET /api/v1/admin/drills with a person admin's token and read the rows from items.
Clean. The route checks a person admin (401 with no session, 403 for an agent admin, and for a member at the shared admin gate). The mapping is shared with MCP drills through drillRows, so the two cannot drift. The origin comes from config or the request URL, never a header. It is read-only and has no user input, and the list shape follows next_cursor. The tests cover 200 equal to MCP, the agent and member refusals, 401 and the OpenAPI entry. I could only check the preview unauthenticated-equivalent (my token is not a person admin, so it answered 401), so the 200 path rests on the test. No security findings.
GET /api/v1/admin/drillsnow answers each failure domain's last drill, verdict and command, as MCPdrillsdoes.bun test test/api-admin-drills.test.ts test/mcp-size.test.ts test/mcp-drills.test.ts,bun x tsc --noEmitandbun scripts/gate.tsall pass. I didn't run the full suite; the forge runs it on the push.drillRows(rows, origin)insrc/actions/resilience.tsnow holds the mapping the MCP tool did inline. The MCP tool calls it and answers the same as before.config.url, else the request's origin. It never comes from a header.src/http/api/drills.tsis registered insrc/http/api.tsright aftersecretRoutes. It notes the session phase and answers the rows as JSON. With no session it answers 401. An agent admin gets 403forbiddenwith "failure domains are for a forge admin who is a person".getApiV1AdminDrills.tsdescribes the route and is exported from the routes index in its alphabetical place./api/v1/admin/*gate (src/http/admin_gate.ts). They get 403 withnot_adminand "only an admin can change the forge's settings or its releases", notforbidden. Both are 403. I left the gate alone because it covers every admin route.test/api-admin-drills.test.tscovers four things:- an admin person gets 200 with rows equal to what MCP
drillsreturns; - an agent admin and a member each get 403; - no token gets 401; - the route appears in the OpenAPI document.Try it with
curl -H "authorization: Bearer $TOKEN" $FORGE_URL/api/v1/admin/drillsusing a person admin's token.Closes #8013