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

Merged opened this

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

Merged

`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

opened this

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) in src/core/subscriptions.ts builds the rows. MCP subscriptions now calls it and answers as before. The fields are id, name, kind, apiBase, scopes, budgetUsd, spentUsd, restingUntil, restingSaid, lastUsedAt, so no token or new field is exposed.
  • The route: src/http/api/subscriptions.ts registers it. The import and the call sit directly after the agentRoutes ones in src/http/api.ts.
  • OpenAPI: getApiV1AdminSubscriptions.ts is exported from routes/index.ts in its alphabetical place.
  • Tests: test/api-admin-subscriptions.test.ts covers 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

Closes #8021

commented

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) in src/core/subscriptions.ts builds the rows. MCP subscriptions now calls it and answers as before. The fields are id, name, kind, apiBase, scopes, budgetUsd, spentUsd, restingUntil, restingSaid, lastUsedAt, so no token or new field is exposed.
  • The route: src/http/api/subscriptions.ts registers it. The import and the call sit directly after the agentRoutes ones in src/http/api.ts.
  • OpenAPI: getApiV1AdminSubscriptions.ts is exported from routes/index.ts in its alphabetical place.
  • Tests: test/api-admin-subscriptions.test.ts covers 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

commented

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 new subscriptionRows(db) in src/core/subscriptions.ts. MCP subscriptions calls 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, not forbidden as the issue says. The shared admin gate in src/http/admin_gate.ts answers not_admin for every admin read before the handler runs. The test and the OpenAPI refusal match that. The handler keeps its own forbidden check, but it is never reached.
  • Wiring: the route is in src/http/api/subscriptions.ts, registered right after agentRoutes in src/http/api.ts. getApiV1AdminSubscriptions.ts describes it and is exported from routes/index.ts in alphabetical order.
  • Tests: test/api-admin-subscriptions.test.ts covers 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 on origin/main. #8036 made x (revoke) in the terminal's egress screen ask first, and test/egress-admin.test.ts never pressed the confirming y. The revoke never completed, the first test timed out, and its leftover grant failed the member test. I added the y to 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

commented

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

Changes requested — before the latest push

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.

commented

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

commented

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/subscriptions answers { items } to a forge admin, with each account's scopes, budget and spend, and never a token.
  • Refusals: a member gets 403 not_admin from the shared admin gate. No session gets 401.
  • Shared rows: 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

Approved

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.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs