Public: anyone can read this; the crew can change it.
Sentinel: shared:01m23vygyffx5hg2f5vm8r74sr:u9xwjdoo36 was degraded 73 minutes today: trevato/forge: "tools/list lists pulls and stays under 40 KB" failed on 3 pulls in the last hour — the lane's red, not … #4255
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/4216-sentinel-shared-01m23vygyffx5hg2f5vm8r74 → main 1 commit fork 1 thread · 1 unresolved
2 files
changed +22
−15
src/http/mcp.ts +21 −14
| @@ -370,6 +370,16 @@ async function archiveMany(ctx: Ctx, user: User, args: Args): Promise<Answer> { | ||
| 370 | 370 | }); |
| 371 | 371 | } |
| 372 | 372 | |
| 373 | // tools/list is held under 40 KB (test/mcp-pulls.test.ts). The server checks | |
| 374 | // every length at the boundary, so the listing leaves the maxLength hints out. | |
| 375 | function lean(schema: unknown): unknown { | |
| 376 | if (Array.isArray(schema)) return schema.map(lean); | |
| 377 | if (schema === null || typeof schema !== "object") return schema; | |
| 378 | return Object.fromEntries( | |
| 379 | Object.entries(schema).filter(([k]) => k !== "maxLength").map(([k, v]) => [k, lean(v)]), | |
| 380 | ); | |
| 381 | } | |
| 382 | ||
| 373 | 383 | const APP_ARG = { |
| 374 | 384 | type: "string", |
| 375 | 385 | description: "owner/name", |
| @@ -1768,7 +1778,7 @@ const TOOLS: readonly Tool[] = [ | ||
| 1768 | 1778 | type: "object", |
| 1769 | 1779 | properties: { |
| 1770 | 1780 | repo: REPO_ARG, |
| 1771 | ref: { type: "string", maxLength: 255, description: "A branch, tag or commit; default the default branch." }, | |
| 1781 | ref: { type: "string", maxLength: 255, description: "Branch, tag or commit; default branch." }, | |
| 1772 | 1782 | limit: { type: "integer", minimum: 1, maximum: 100 }, |
| 1773 | 1783 | }, |
| 1774 | 1784 | required: ["repo"], |
| @@ -2404,7 +2414,7 @@ const TOOLS: readonly Tool[] = [ | ||
| 2404 | 2414 | properties: { |
| 2405 | 2415 | repo: REPO_ARG, |
| 2406 | 2416 | name: { type: "string" }, |
| 2407 | visibility: { type: "string", enum: ["private", "public"], description: "Private by default; public only when the original is." }, | |
| 2417 | visibility: { type: "string", enum: ["private", "public"], description: "Private by default." }, | |
| 2408 | 2418 | dryRun: { type: "boolean" }, |
| 2409 | 2419 | }, |
| 2410 | 2420 | required: ["repo"], |
| @@ -3170,18 +3180,15 @@ const TOOLS: readonly Tool[] = [ | ||
| 3170 | 3180 | }, |
| 3171 | 3181 | pillar: { |
| 3172 | 3182 | type: "string", |
| 3173 | description: | |
| 3174 | 'The pillar this serves, by short form or name: "P0" or "P0 Trust". Its umbrella issue then waits for it.', | |
| 3183 | description: 'Pillar served: "P0" or "P0 Trust".', | |
| 3175 | 3184 | }, |
| 3176 | 3185 | later: { |
| 3177 | 3186 | type: "boolean", |
| 3178 | description: | |
| 3179 | "True when no pillar: opens behind the Later gate. Needs why.", | |
| 3187 | description: "No pillar: needs why.", | |
| 3180 | 3188 | }, |
| 3181 | 3189 | why: { |
| 3182 | 3190 | type: "string", |
| 3183 | description: | |
| 3184 | "With later: one sentence why.", | |
| 3191 | description: "With later: why.", | |
| 3185 | 3192 | }, |
| 3186 | 3193 | }, |
| 3187 | 3194 | required: ["key", "title"], |
| @@ -3230,7 +3237,7 @@ const TOOLS: readonly Tool[] = [ | ||
| 3230 | 3237 | inputSchema: { |
| 3231 | 3238 | type: "object", |
| 3232 | 3239 | properties: { |
| 3233 | since: { type: "string", maxLength: 40, description: "ISO time; default the last day, at most 14 days back." }, | |
| 3240 | since: { type: "string", maxLength: 40, description: "ISO time; default last day, max 14 days." }, | |
| 3234 | 3241 | ua: { type: "string", maxLength: 100, description: "Part of the user agent." }, |
| 3235 | 3242 | path: { type: "string", maxLength: 200, description: "Path prefix." }, |
| 3236 | 3243 | }, |
| @@ -3450,10 +3457,10 @@ const TOOLS: readonly Tool[] = [ | ||
| 3450 | 3457 | verb: { type: "string", enum: ["attach", "verify", "rotate", "rename", "remove", "allow_writes", "audit"] }, |
| 3451 | 3458 | provider: { type: "string", enum: [...PROVIDERS] }, |
| 3452 | 3459 | name: { type: "string", maxLength: 32 }, |
| 3453 | value: { type: "string", maxLength: 4096, description: "The token, for attach and rotate." }, | |
| 3460 | value: { type: "string", maxLength: 4096, description: "Token, for attach and rotate." }, | |
| 3454 | 3461 | to: { type: "string", maxLength: 32 }, |
| 3455 | allowed: { type: "boolean", description: "For allow_writes: may runs that declare writes hold the token." }, | |
| 3456 | repo: { type: "string", maxLength: 200, description: "For audit: owner/name, where the findings are filed as issues." }, | |
| 3462 | allowed: { type: "boolean", description: "For allow_writes." }, | |
| 3463 | repo: { type: "string", maxLength: 200, description: "For audit: owner/name." }, | |
| 3457 | 3464 | }, |
| 3458 | 3465 | required: ["verb", "provider", "name"], |
| 3459 | 3466 | }, |
| @@ -3574,7 +3581,7 @@ const TOOLS: readonly Tool[] = [ | ||
| 3574 | 3581 | type: "object", |
| 3575 | 3582 | properties: { |
| 3576 | 3583 | slug: { type: "string", maxLength: 40 }, |
| 3577 | panel: { type: "integer", minimum: 0, maximum: 23, description: "One panel's data, by its place on the board from 0." }, | |
| 3584 | panel: { type: "integer", minimum: 0, maximum: 23, description: "One panel's data, by place from 0." }, | |
| 3578 | 3585 | from: { type: "string", maxLength: 64 }, |
| 3579 | 3586 | to: { type: "string", maxLength: 64 }, |
| 3580 | 3587 | step: { type: "string", maxLength: 16, description: "With panel: seconds per bucket, or a count of s, m, h or d." }, |
| @@ -3740,7 +3747,7 @@ function rpcOne( | ||
| 3740 | 3747 | ...TOOLS.filter((t) => t.adminOnly !== true || user.isAdmin).map((t) => ({ |
| 3741 | 3748 | name: t.name, |
| 3742 | 3749 | description: t.description, |
| 3743 | inputSchema: t.inputSchema, | |
| 3750 | inputSchema: lean(t.inputSchema), | |
| 3744 | 3751 | annotations: { readOnlyHint: !t.writes }, |
| 3745 | 3752 | ...(CARD_OF_TOOL[t.name] === undefined |
| 3746 | 3753 | ? {} |
test/mcp-pulls.test.ts +1 −1
| @@ -144,7 +144,7 @@ test("a private repository the caller cannot read is not found, as pull says", a | ||
| 144 | 144 | |
| 145 | 145 | test("tools/list lists pulls and stays under 40 KB", async () => { |
| 146 | 146 | const { text } = await rpc(tok["ada"]!, "tools/list"); |
| 147 | expect(Buffer.byteLength(text)).toBeLessThanOrEqual(40_000); | |
| 147 | expect(Buffer.byteLength(text)).toBeLessThanOrEqual(38_500); // headroom: a new tool must not tip the lane red | |
| 148 | 148 | expect(JSON.parse(text).result.tools.map((t: any) => t.name)).toContain("pulls"); |
| 149 | 149 | }); |
| 150 | 150 |
new above
offline
Nit: the title says "stays under 40 KB" but the bound is now 38,500. Rename it (e.g. "…stays under 38.5 KB, leaving headroom under the 40 KB budget") so a failure reads right.