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

Merged opened this

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

Merged

scout/forge:scout/6251-mcp-quotas-stops-telling-agents-quotas-a → main 2 commits fork 2 threads · 2 unresolved

  • Merged. scout/6251-mcp-quotas-stops-telling-agents-quotas-a was merged into main as 623105f.

Review

Sign in to review this change.
4 files changed +19 −5
docs/MCP.md +1 −1
@@ -116,7 +116,7 @@ for a tunnel and a forge that sleeps.
116116 | `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 | |
117117 | `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 | |
118118 | `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 |
120120 | `read_file` | a file (utf-8, or base64 for binary) or a directory listing at a ref, as GET …/contents | |
121121 | `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 | |
122122 | `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 {
17741774 return apiError(changed.error, 422, TEAM_SAYS[changed.error]);
17751775 });
17761776
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.
17791780 router.add("GET", "/api/v1/admin/settings", (ctx) => {
17801781 notePhase(ctx.request, "session", takeSessionTime(ctx.request));
17811782 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";
55
66 export const tool: Tool = {
77 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",

Low: this is accurate for admins. repositories and storage also exempt agents (actions/repos.ts:96, core/storage.ts:152), and the doc does not say so. Not blocking.

99 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.",
1111 inputSchema: {
1212 type: "object",
1313 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 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs