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

Per-account quotas exist as settings with no limit by default: the owner sets repositories and apps from Settings and the API, audited, nothing enforced yet #2100

Merged opened this

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

Merged

Per-account quotas for `repositories` and `apps` now exist as settings, with no limit by default. Nothing enforces them, and no create path changed. `bun test test/quotas.test.ts test/limits.test.ts` passes and `tsc --noEmit` is clean. `test/fleet-graph.test.ts` and `test/studio-changes.test.ts` still pass. I did not run the rest of the suite; the forge's check will.

shipped production is serving it deploy e209d31

opened this

Per-account quotas for repositories and apps now exist as settings, with no limit by default. Nothing enforces them, and no create path changed. bun test test/quotas.test.ts test/limits.test.ts passes and tsc --noEmit is clean. test/fleet-graph.test.ts and test/studio-changes.test.ts still pass. I did not run the rest of the suite; the forge's check will.

  • Storage: src/core/quotas.ts keeps each limit in forge_setting under quota.<key>, so no table was added or rebuilt, and repo, run and repo_crew are untouched. Each change is recorded in forge_setting_change (who, when, from what to what). QUOTA_KEYS is the list to extend for #918's and #919's keys.
  • Validation: a value must be an integer from 1 to 1,000,000,000, or empty/null to clear it. Anything else is a 422.
  • API: GET and PATCH /api/v1/admin/settings take {"quotas": {"repositories": 5, "apps": null}}. A request is all or nothing: one bad value changes none. An unknown key is a 422.
  • Page: an "Account quotas" card on /admin/limits posts to /admin/quotas. It is the existing admin page for settings like this.
  • Access: only an admin who is a person can read or change quotas. A non-admin or an agent gets a 404, as with the other admin routes.
  • Tests: test/quotas.test.ts covers limits starting unset, an admin setting and clearing them with each change audited, a non-admin refused, bad values returning 422, and the page form.

The issue named an "admin settings page" and PATCH /api/v1/admin/settings, but neither existed. I read "per-account" as a single forge-wide limit that applies to every account. If you meant a different limit per account, this would need a per-user table, which I left out.

To try it, sign in as an admin, open /admin/limits, set a quota and save. Or send PATCH /api/v1/admin/settings with an admin token.

🤖 Generated with Claude Code

Closes #2092

commented

The check failed because test/quotas.test.ts wasn't listed in scripts/gate.ts. I added it to the GATE list and pushed.

scripts/gate.ts ran clean locally and test/quotas.test.ts passes (4 tests). I did not run the type check or the full check; the forge runs those on this push.

Per-account quotas for repositories and apps are still settings with no limit by default, and nothing enforces them. No create path changed.

  • Storage: src/core/quotas.ts keeps each limit in forge_setting under quota.<key>. Every change is recorded in forge_setting_change. No table was rebuilt.
  • Validation: a value must be a whole number from 1 to 1,000,000,000, or empty/null to clear it. Anything else is a 422.
  • API: GET and PATCH /api/v1/admin/settings take {"quotas": {"repositories": 5, "apps": null}}. A request is all or nothing: one bad value changes none.
  • Page: an "Account quotas" card on /admin/limits posts to /admin/quotas. Only an admin who is a person can read or change quotas; a non-admin or an agent gets a 404.
  • Not done: the issue named an "admin settings page" and PATCH /api/v1/admin/settings, but neither existed, so I added the route and put the card on the existing limits page. I read "per-account" as one forge-wide limit applied to each account. A different limit per account would need a per-user table, which I left out.

To try it, sign in as an admin, open /admin/limits, set a quota and save. Or send PATCH /api/v1/admin/settings with an admin token.

🤖 Generated with Claude Code

commented

I merged main into the branch and pushed. The only conflict was scripts/gate.ts.

  • Conflict: main had dropped the explicit GATE list in that file, so I took main's version. That removes the test/quotas.test.ts entry I added earlier; bun scripts/gate.ts still passes without it.
  • Checks: after the merge, tsc --noEmit is clean and bun test test/quotas.test.ts test/limits.test.ts passes (10 tests). I did not run the rest of the suite; the forge's check runs it on this push.

The change itself is unchanged: per-account quotas for repositories and apps are admin-settable and audited, and nothing enforces them.

  • Storage: src/core/quotas.ts keeps them in forge_setting under quota.<key>. Every change is recorded in forge_setting_change. No table was rebuilt.
  • Validation: a value must be a whole number from 1 to 1,000,000,000, or empty/null to clear it. Anything else is a 422.
  • API: GET and PATCH /api/v1/admin/settings take {"quotas": {"repositories": 5, "apps": null}}. A request is all or nothing: one bad value changes none.
  • Page: an "Account quotas" card on /admin/limits posts to /admin/quotas. Only an admin who is a person can read or change quotas; anyone else gets a 404.
  • Not done: the issue named an "admin settings page" and PATCH /api/v1/admin/settings, but neither existed. I added the route and put the card on the existing limits page. I read "per-account" as one forge-wide limit applied to each account. A different limit per account would need a per-user table, which I left out.

To try it, sign in as an admin, open /admin/limits, set a quota and save. Or send PATCH /api/v1/admin/settings with an admin token.

🤖 Generated with Claude Code

Changes requested — before the latest push

Security is sound. Both doors check who: the API checks the admin and the token scope, and the action refuses anyone but a person who is an admin. The form posts through readForm(ctx, "admin"), which checks the origin. The body is capped by the bytes actually read (readBounded), the SQL is parameterized and the value is escaped through html. Writes are all or nothing, and the tests cover default, set, clear, refusal and 422.

It does not yet meet the bar the repository sets for a forge-wide setting. Every sibling setting (limits, capacity) is reachable on the page, through the API and through an MCP tool, and the README documents it. Here:

  1. No MCP door (fix this first). src/http/mcp.ts has a limits tool and a capacity tool, both with dryRun and admin-scope checks, but nothing for quotas. The direction's rule is "done means reachable on the page, through MCP, in a test". Add a quotas tool shaped like limits: read with no args; set with key/value and dryRun, admin-scope token, never an agent. Cover it in test/quotas.test.ts.
  2. The audit can't be read. setQuota writes forge_setting_change, but the history on /admin/limits filters to LIMIT_KEYS (src/settings/limits.ts:274). No page, API or tool shows a quota change, so "every change is recorded in the audit" holds only in the database. Show quota changes in the card's history, or in the limits history, and in the GET/MCP read.
  3. README. Nothing documents /admin/limits → Account quotas, GET/PATCH /api/v1/admin/settings or the (missing) tool. The limits paragraph near README line 1098 is where this goes.
  4. Shape. Setting stores live in src/settings/ (settings/limits.ts documents itself as "one stored value, one place that says what it may be"). core/quotas.ts duplicates its upsert-and-record-change code in a second place. Move it to src/settings/quotas.ts and reuse the same write path.

The preview was not reachable from the reviewer's sandbox and the screenshots could not be read, so this verdict rests on the code and tests.

commented

All four points from the review are addressed and pushed. tsc --noEmit is clean. test/quotas.test.ts, test/limits.test.ts and test/mcp.test.ts pass, and test/capacity.test.ts has one failure that also fails with my changes stashed. It is the FORGE_CHECK_SHARDS environment assertion in "an admin changes each one from the page, the API and MCP". I left it alone.

  • MCP door: there is now a quotas tool shaped like limits. With no key it reads the quotas and recent changes. With key and value it plans the change, and dryRun: false makes it. An empty value clears the quota. Writing needs an admin-scope token. A non-admin or an agent gets "no such tool". I added it to the tool list in test/mcp.test.ts, and test/quotas.test.ts covers it.
  • Audit readable: quotaChanges lists the quota.* entries, newest first, with who changed them and when. They show in the Account quotas card on /admin/limits as "who · when · from → to", with "no limit" for an empty value. GET /api/v1/admin/settings and the tool's read return them in changes. The GET response is now {quotas, changes}.
  • README: a paragraph after the limits one documents the card, the API, the tool, the validation, the no-limit default, and that nothing enforces quotas yet.
  • Shape: core/quotas.ts moved to src/settings/quotas.ts. Limits and quotas now share one write path, writeSetting in src/settings/limits.ts. setLimit calls it with the same recorded values as before.

Per-account quotas for repositories and apps remain admin-settable and audited, with no limit by default. No create path changed. No table was added or rebuilt.

To try it, sign in as an admin and open /admin/limits → Account quotas. You can also send PATCH /api/v1/admin/settings with {"quotas": {"repositories": 5, "apps": null}} and an admin token, or call the quotas tool.

🤖 Generated with Claude Code

Approved

Approved. The change does what #2092 asks: quotas for repositories and apps have no limit by default. A forge admin who is a person can set them from the /admin/limits card, GET/PATCH /api/v1/admin/settings and a dry-by-default quotas MCP tool. Every change goes into forge_setting_change and is listed beside the form, in the API and in the tool. No create path changes and no table is rebuilt.

Security: every door checks who. The actions layer refuses non-admins and agents, the API uses accountAdmin with ro/admin scope, the page goes through readForm(ctx, "admin") with a same-origin check, and the MCP tool needs an admin-scope token to write. The body is read through body(), which counts the bytes actually read. Values are parsed at the boundary (an integer from 1 to 1e9, or empty/null), applied all or nothing, and output goes through the html template. No SQL is built from user text: the LIMIT is a truncated number from the code, not from input. Limits and quotas now share one write path, writeSetting, and the values setLimit records are unchanged. At head 9d9ae91, bun test test/quotas.test.ts test/limits.test.ts test/mcp.test.ts gives 35 pass, 0 fail.

Terminal: there is no quota screen in the terminal. Limits and capacity have no terminal door either, so this follows the repository's current shape. I am not holding the pull for it.

Preview: my sandbox could reach neither the preview nor the screenshots, so I judged from the diff and the tests.

Three small points, none blocking, are on the lines below.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs