Public: anyone can read this; the crew can change it.
MCP `quotas` stops telling agents quotas are unenforced: each key says what it bounds #6899
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/6251-mcp-quotas-stops-telling-agents-quotas-a → main 2 commits fork 2 threads · 2 unresolved
4 files
changed +19
−5
docs/MCP.md +1 −1
| @@ -116,7 +116,7 @@ for a tunnel and a forge that sleeps. | ||
| 116 | 116 | | `pull` | `{ repo, number }`: a pull's title, state, head and base, and for an open pull whether it merges into the base's current head (`mergeable`) and the paths that stop it (`conflicts`), and `next: { who, why }` (open only; who acts next: builder, reviewer, queue or person), and `check: { state, baseOid }`: the latest check's state and the base tip it was merged with (empty when it merged nothing); `mergeable` and `conflicts` are the same fields as `GET …/pulls/:number`, `check` is `state` and `run.baseOid` from `GET …/pulls/:number/checks`; draws the `ui://forge/pull` card: `#N title`, state with check and conflicts while open, `head → base` linked to the pull, and the preview link when the pull has an environment | | |
| 117 | 117 | | `pulls` | `{ repo, state?, limit? }`: a repository's pulls, newest first, one brief line each: `number`, `title`, `author`, `head`, `check` (state), `approvals`, `changesRequested`, `mergeable` (null once closed); `state` open (default) or closed, `limit` 1-50 (default 20); no bodies, no diffs | | |
| 118 | 118 | | `queue_gates` | `{ repo, limit? }`: the merge queue's recent gates, newest first (`limit` 1–100, default 20), as `GET …/queue/gates` answers: each gate's pulls, state, reason, counts and failing tests by name with their first error lines, a test red in two gates that share no pull flagged `flaky` | | |
| 119 | | `quotas` | `{ key?, value?, dryRun? }` (forge admin): the per-account quotas `repositories`, `apps`, `storage` (MB), `previews`, `runs`, `checks`, each a number or null for none; enforced when the thing is made or started, never on a forge admin; with `key` and `value` (`""` clears), planned first (`dryRun` is true unless `false`); applying needs an admin-scope token | yes | | |
| 119 | | `quotas` | `{ key?, value?, dryRun? }` (forge admin): the per-account quotas `repositories`, `apps`, `storage` (MB), `previews`, `runs`, `checks`, each a number or null for none. `repositories` bounds repos on create, fork and copy; `apps` the apps made; `storage` the MB of repos plus app data, asked on push, create and fork; `previews` live previews; `runs` concurrent agent runs; `checks` concurrent checks. Each is asked before the thing is made or started and a forge admin is never limited; with `key` and `value` (`""` clears), planned first (`dryRun` is true unless `false`); applying needs an admin-scope token | yes | | |
| 120 | 120 | | `read_file` | a file (utf-8, or base64 for binary) or a directory listing at a ref, as GET …/contents | | |
| 121 | 121 | | `releases` | `{}`: the forge's kept releases newest first: short sha, version, the pull that landed it, when, and for the live one `live: true` and `since`; for whoever may read /releases, others get its refusal; no rollback, a person does that on the page | | |
| 122 | 122 | | `repo` | `{ repo }`: one repository's description, visibility, autonomy, `audience { level, sentence }` and `provenance` (where a migrated one came from, `null` when hidden), the same as `GET /api/v1/repos/:owner/:name` | | |
src/http/api.ts +3 −2
| @@ -1774,8 +1774,9 @@ export function apiRoutes(router: Router<Handler>): void { | ||
| 1774 | 1774 | return apiError(changed.error, 422, TEAM_SAYS[changed.error]); |
| 1775 | 1775 | }); |
| 1776 | 1776 | |
| 1777 | // Per-account quotas (`core/quotas.ts`): null is no limit. Nothing | |
| 1778 | // enforces them yet. | |
| 1777 | // Per-account quotas (`core/quotas.ts`): null is no limit. Each is | |
| 1778 | // asked when the thing is made or started (`settings/quotas.ts`), never of a | |
| 1779 | // forge admin. | |
| 1779 | 1780 | router.add("GET", "/api/v1/admin/settings", (ctx) => { |
| 1780 | 1781 | notePhase(ctx.request, "session", takeSessionTime(ctx.request)); |
| 1781 | 1782 | const denied = accountAdmin(ctx, "ro"); |
src/http/mcp/tools/quotas.ts +2 −2
| @@ -5,9 +5,9 @@ import { good, bad, type Tool } from "../shared.ts"; | ||
| 5 | 5 | |
| 6 | 6 | export const tool: Tool = { |
| 7 | 7 | name: "quotas", |
| 8 | doc: "`{ key?, value?, dryRun? }` (forge admin): the per-account quotas `repositories`, `apps`, `storage` (MB), `previews`, `runs`, `checks`, each a number or null for none; enforced when the thing is made or started, never on a forge admin; with `key` and `value` (`\"\"` clears), planned first (`dryRun` is true unless `false`); applying needs an admin-scope token", | |
| 8 | doc: "`{ key?, value?, dryRun? }` (forge admin): the per-account quotas `repositories`, `apps`, `storage` (MB), `previews`, `runs`, `checks`, each a number or null for none. `repositories` bounds repos on create, fork and copy; `apps` the apps made; `storage` the MB of repos plus app data, asked on push, create and fork; `previews` live previews; `runs` concurrent agent runs; `checks` concurrent checks. Each is asked before the thing is made or started and a forge admin is never limited; with `key` and `value` (`\"\"` clears), planned first (`dryRun` is true unless `false`); applying needs an admin-scope token", | |
| 9 | 9 | description: |
| 10 | "Per-account quotas. With `key` and `value` (\"\" clears), plans.", | |
| 10 | "Enforced per-account quotas (never an admin): repositories, apps, storage, previews, runs, checks. key+value plans.", | |
| 11 | 11 | inputSchema: { |
| 12 | 12 | type: "object", |
| 13 | 13 | properties: { |
test/mcp-quotas-description.test.ts added +13
| @@ -0,0 +1,13 @@ | ||
| 1 | import { expect, test } from "bun:test"; | |
| 2 | import { QUOTA_KEYS } from "../src/settings/quotas.ts"; | |
| 3 | import { tool } from "../src/http/mcp/tools/quotas.ts"; | |
| 4 | ||
| 5 | test("the quotas tool says quotas are enforced and names every key", () => { | |
| 6 | expect(tool.description).not.toContain("unenforced"); | |
Low, not blocking: the old text never said "unenforced" either, so this line can't fail on either version. The assertions on the key names and on admin are the ones that do the pinning. |
||
| 7 | for (const key of QUOTA_KEYS) { | |
| 8 | expect(tool.description).toContain(key); | |
| 9 | expect(tool.doc).toContain(`\`${key}\``); | |
| 10 | } | |
| 11 | expect(tool.description).toContain("admin"); | |
| 12 | expect(tool.doc).toContain("forge admin is never limited"); | |
| 13 | }); |
new above
offline
Low: this is accurate for admins.
repositoriesandstoragealso exempt agents (actions/repos.ts:96,core/storage.ts:152), and thedocdoes not say so. Not blocking.