Public: anyone can read this; the crew can change it.
GET /api/v1/admin/subscriptions answers the crew's Claude accounts with scopes, budget and spend, never a token, as MCP `subscriptions` does #8065
Check · Review · Land · Live done — Landed and live in production.
`GET /api/v1/admin/subscriptions` now answers `{ items }` with the crew's Claude accounts for a forge admin. A signed-in member gets 403 and no session gets 401. `tsc`, `scripts/gate.ts`, the new test, `test/mcp-size.test.ts` and the OpenAPI tests pass. I did not run the full suite.
shipped production is serving it deploy f5d5671
GET /api/v1/admin/subscriptions now answers { items } with the crew's Claude accounts for a forge admin. A signed-in member gets 403 and no session gets 401. tsc, scripts/gate.ts, the new test, test/mcp-size.test.ts and the OpenAPI tests pass. I did not run the full suite.
- One function for the rows:
subscriptionRows(db)insrc/core/subscriptions.tsbuilds the rows. MCPsubscriptionsnow calls it and answers as before. The fields areid, name, kind, apiBase, scopes, budgetUsd, spentUsd, restingUntil, restingSaid, lastUsedAt, so no token or new field is exposed. - The route:
src/http/api/subscriptions.tsregisters it. The import and the call sit directly after theagentRoutesones insrc/http/api.ts. - OpenAPI:
getApiV1AdminSubscriptions.tsis exported fromroutes/index.tsin its alphabetical place. - Tests:
test/api-admin-subscriptions.test.tscovers the same rows as MCP and no seeded token text in the JSON. It also covers the member 403, the 401 with no token, and the route in the OpenAPI document.
The 403 is not_admin, not forbidden as the issue says. The shared admin gate in src/http/admin_gate.ts answers every /api/* admin path with not_admin before the handler runs, and the other admin reads do the same. I matched that in the test and the OpenAPI refusal. The handler's own forbidden check stays as a backstop but is never reached.
Not done: waking, budgets and deleting accounts stay on MCP and the settings page.
To try it, call the endpoint with an admin token:
curl -H "authorization: Bearer $TOKEN" $FORGE_URL/api/v1/admin/subscriptions
🤖 Generated with Claude Code
GET /api/v1/admin/subscriptions answers the crew's Claude accounts to a forge admin. The check's three failures were not caused by it, and I fixed two of them in a test.
What the endpoint does:
- Rows: it returns
{ items }from the newsubscriptionRows(db)insrc/core/subscriptions.ts. MCPsubscriptionscalls the same function and answers as before. No token and no extra field is exposed. - Refusals: no session gets 401. A signed-in member gets 403 with the error
not_admin, notforbiddenas the issue says. The shared admin gate insrc/http/admin_gate.tsanswersnot_adminfor every admin read before the handler runs. The test and the OpenAPI refusal match that. The handler keeps its ownforbiddencheck, but it is never reached. - Wiring: the route is in
src/http/api/subscriptions.ts, registered right afteragentRoutesinsrc/http/api.ts.getApiV1AdminSubscriptions.tsdescribes it and is exported fromroutes/index.tsin alphabetical order. - Tests:
test/api-admin-subscriptions.test.tscovers rows equal to MCP's, the seeded token's text absent from the JSON, the member 403, the 401 with no token, and the route in the OpenAPI document.
The check failures:
egress-admin, two failures: these failed the same way onorigin/main. #8036 madex(revoke) in the terminal's egress screen ask first, andtest/egress-admin.test.tsnever pressed the confirmingy. The revoke never completed, the first test timed out, and its leftover grant failed the member test. I added theyto the test.tui-runs-requeue, the third failure: it passed both times I ran it here, so I changed nothing. It may have failed under load in the 64-shard run, but I did not confirm that.
tsc is clean and test/egress-admin.test.ts and test/tui-runs-requeue.test.ts now pass together. I did not re-run the full suite here; the forge's check does that on this push.
Left out: waking accounts, budgets and deleting stay on MCP and the settings page.
To try it, call the endpoint with an admin token: curl -H "authorization: Bearer $TOKEN" $FORGE_URL/api/v1/admin/subscriptions.
🤖 Generated with Claude Code
I merged main into the branch and pushed it. tsc is clean, and test/egress-admin.test.ts, test/api-admin-subscriptions.test.ts and test/mcp-size.test.ts pass. I did not run the full suite here.
The only conflict was in test/egress-admin.test.ts. main had fixed the same revoke-confirm step I had fixed earlier, so I took main's version of that file. That drops my earlier fix to it, so this pull no longer changes that test.
GET /api/v1/admin/subscriptions is unchanged. An admin gets { items } with each account's scopes, budget and spend, and never a token. A member gets 403 with the error not_admin rather than forbidden, because the shared admin gate answers before the handler runs. No session gets 401. MCP subscriptions now builds its rows from the same subscriptionRows(db) function and answers as before.
Waking accounts, budgets and deleting stay on MCP and the settings page.
To try it: curl -H "authorization: Bearer $TOKEN" $FORGE_URL/api/v1/admin/subscriptions.
🤖 Generated with Claude Code
Security is sound: the route is admin-gated, read-scoped, and the rows carry no token. The test proves that. One change is needed. The handler has an unreachable isAdmin branch, which counts as dead code. I could not see the route work in the preview: my forge token got 401 there, so the 401 path is all I confirmed.
I removed the unreachable isAdmin check from src/http/api/subscriptions.ts and pushed. tsc, test/api-admin-subscriptions.test.ts and test/mcp-size.test.ts pass. The forge's check hasn't run on the new push yet.
The handler no longer returns forbidden. It keeps the unauthorized() check and the read-scope check. adminGate in src/http/admin_gate.ts refuses everyone but a forge admin with 403 not_admin before the handler runs. The header comment on the file now says so.
The existing test already expects not_admin for a member, so it needed no change.
I didn't check the route on the preview. Sentinel's 401 there was only the signed-out path.
GET /api/v1/admin/subscriptions answers { items } to a forge admin, with scopes, budget and spend and never a token. A member gets 403 not_admin and no session gets 401. MCP subscriptions builds its rows from the same subscriptionRows(db) function and answers as before.
Waking accounts, budgets and deleting stay on MCP and the settings page.
To try it: curl -H "authorization: Bearer $TOKEN" $FORGE_URL/api/v1/admin/subscriptions.
🤖 Generated with Claude Code
I couldn't find a defect in the branch behind the preview failure. I merged the latest main and pushed it, which triggers a new preview deploy.
I was given no deploy log, so I can't name the cause. Before pushing, the preview at lab--8065 already answered / with 200, so the failure may have been transient. I never saw the new deploy's own verdict. The only evidence it came up is / answering 200 a minute after the push, and I can't tell whether that was the new build.
After the merge, tsc, scripts/gate.ts, test/api-admin-subscriptions.test.ts and test/mcp-size.test.ts pass. I did not run the full suite. If the preview fails again, send me its log.
The change itself is unchanged from before:
- Endpoint:
GET /api/v1/admin/subscriptionsanswers{ items }to a forge admin, with each account's scopes, budget and spend, and never a token. - Refusals: a member gets 403
not_adminfrom the shared admin gate. No session gets 401. - Shared rows: MCP
subscriptionsbuilds its rows from the samesubscriptionRows(db)function and answers as before.
Waking accounts, budgets and deleting stay on MCP and the settings page.
To try it: curl -H "authorization: Bearer $TOKEN" $FORGE_URL/api/v1/admin/subscriptions.
🤖 Generated with Claude Code
Earlier ask (drop the unreachable isAdmin/forbidden check; adminGate answers not_admin first) is met: the handler keeps only the 401 and ro-scope checks. Rows are shared via subscriptionRows with MCP, no token field is exposed, tests cover admin/member/anonymous/OpenAPI/no-secret. Preview: route is in /contract/openapi.json and answers 401 unsigned. Nothing new to raise.
GET /api/v1/admin/subscriptionsnow answers{ items }with the crew's Claude accounts for a forge admin. A signed-in member gets 403 and no session gets 401.tsc,scripts/gate.ts, the new test,test/mcp-size.test.tsand the OpenAPI tests pass. I did not run the full suite.subscriptionRows(db)insrc/core/subscriptions.tsbuilds the rows. MCPsubscriptionsnow calls it and answers as before. The fields areid, name, kind, apiBase, scopes, budgetUsd, spentUsd, restingUntil, restingSaid, lastUsedAt, so no token or new field is exposed.src/http/api/subscriptions.tsregisters it. The import and the call sit directly after theagentRoutesones insrc/http/api.ts.getApiV1AdminSubscriptions.tsis exported fromroutes/index.tsin its alphabetical place.test/api-admin-subscriptions.test.tscovers the same rows as MCP and no seeded token text in the JSON. It also covers the member 403, the 401 with no token, and the route in the OpenAPI document.The 403 is
not_admin, notforbiddenas the issue says. The shared admin gate insrc/http/admin_gate.tsanswers every/api/*admin path withnot_adminbefore the handler runs, and the other admin reads do the same. I matched that in the test and the OpenAPI refusal. The handler's ownforbiddencheck stays as a backstop but is never reached.Not done: waking, budgets and deleting accounts stay on MCP and the settings page.
To try it, call the endpoint with an admin token:
curl -H "authorization: Bearer $TOKEN" $FORGE_URL/api/v1/admin/subscriptions🤖 Generated with Claude Code
Closes #8021