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
Check · Review · Land · Live done — Landed and live in production.
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
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.tskeeps each limit inforge_settingunderquota.<key>. Every change is recorded inforge_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:
GETandPATCH /api/v1/admin/settingstake{"quotas": {"repositories": 5, "apps": null}}. A request is all or nothing: one bad value changes none. - Page: an "Account quotas" card on
/admin/limitsposts 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
I merged main into the branch and pushed. The only conflict was scripts/gate.ts.
- Conflict:
mainhad dropped the explicitGATElist in that file, so I tookmain's version. That removes thetest/quotas.test.tsentry I added earlier;bun scripts/gate.tsstill passes without it. - Checks: after the merge,
tsc --noEmitis clean andbun test test/quotas.test.ts test/limits.test.tspasses (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.tskeeps them inforge_settingunderquota.<key>. Every change is recorded inforge_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:
GETandPATCH /api/v1/admin/settingstake{"quotas": {"repositories": 5, "apps": null}}. A request is all or nothing: one bad value changes none. - Page: an "Account quotas" card on
/admin/limitsposts 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
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:
- No MCP door (fix this first).
src/http/mcp.tshas alimitstool and acapacitytool, 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 likelimits: read with no args; set withkey/valueand dryRun, admin-scope token, never an agent. Cover it intest/quotas.test.ts. - The audit can't be read.
setQuotawritesforge_setting_change, but the history on/admin/limitsfilters toLIMIT_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. - README. Nothing documents
/admin/limits→ Account quotas,GET/PATCH /api/v1/admin/settingsor the (missing) tool. The limits paragraph near README line 1098 is where this goes. - Shape. Setting stores live in
src/settings/(settings/limits.tsdocuments itself as "one stored value, one place that says what it may be").core/quotas.tsduplicates its upsert-and-record-change code in a second place. Move it tosrc/settings/quotas.tsand 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.
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
quotastool shaped likelimits. With nokeyit reads the quotas and recent changes. Withkeyandvalueit plans the change, anddryRun: falsemakes 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 intest/mcp.test.ts, andtest/quotas.test.tscovers it. - Audit readable:
quotaChangeslists thequota.*entries, newest first, with who changed them and when. They show in the Account quotas card on/admin/limitsas "who · when · from → to", with "no limit" for an empty value.GET /api/v1/admin/settingsand the tool's read return them inchanges. 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.tsmoved tosrc/settings/quotas.ts. Limits and quotas now share one write path,writeSettinginsrc/settings/limits.ts.setLimitcalls 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. 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.
Per-account quotas for
repositoriesandappsnow 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.tspasses andtsc --noEmitis clean.test/fleet-graph.test.tsandtest/studio-changes.test.tsstill pass. I did not run the rest of the suite; the forge's check will.src/core/quotas.tskeeps each limit inforge_settingunderquota.<key>, so no table was added or rebuilt, andrepo,runandrepo_creware untouched. Each change is recorded inforge_setting_change(who, when, from what to what).QUOTA_KEYSis the list to extend for #918's and #919's keys.GETandPATCH /api/v1/admin/settingstake{"quotas": {"repositories": 5, "apps": null}}. A request is all or nothing: one bad value changes none. An unknown key is a 422./admin/limitsposts to/admin/quotas. It is the existing admin page for settings like this.test/quotas.test.tscovers 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 sendPATCH /api/v1/admin/settingswith an admin token.🤖 Generated with Claude Code
Closes #2092