fix: charge the fallback SKILL.md read to the catalog byte budget; review round 1 (#2294)

- A "dynamic" skill's only read is the fallback SKILL.md fetch, which was not
  charged, so a catalog of dynamic skills never advanced catalogBytes and a
  configured byte budget bounded nothing. Charge it like a manifest read.
- Add verifySkills cases for a configured byte budget and for dynamic skills.
- Wire onSkillCatalogLimitChange into the ServerSettingsForm story.
- Reword the new field descriptions, JSDoc and config table rows; rename the
  route test to cover every invalid value it sends.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P6i1HBSdSS9kL4npZ2dDSe
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-09-14 00:25:19 -04:00
commit 467c97307e
2 changed files with 102 additions and 9 deletions

View file

@ -504,6 +504,12 @@ export async function verifySkills(
files.push({ uri: entry.uri, status: "read-error", reason });
try {
const invocation = await client.readResource(entry.uri, metadata);
// ⚠️ Charged like a manifest read — raw response, or the decoded
// length when larger. Uncharged, a catalog of `"dynamic"` skills (whose
// ONLY read is this one) left `catalogBytes` at zero, so the run's byte
// budget bounded nothing for it and even a configured limit of 1 read
// every skill up to the count limit (Copilot, #2294).
let charged = responseBytes(invocation.result);
const contents = contentsFor(invocation.result, entry.uri);
if (!contents) {
fail(
@ -512,10 +518,12 @@ export async function verifySkills(
} else {
try {
entryBytes = skillFileBytes(contents);
charged = Math.max(charged, entryBytes.byteLength);
} catch (err) {
fail(reasonOf(err));
}
}
receivedBytes += charged;
// If the DECLARED manifest lists this file but the read bounds
// excluded it, verify it here too. The fallback exists for the
// frontmatter check, but reading a file and then not checking the

View file

@ -172,14 +172,16 @@ export type StoredMCPServer = MCPServerConfig & {
*/
maxFetchRequests?: number;
/**
* Most skills one skills verification run reads from (#2294). Omitted on
* disk when it equals `SKILL_MAX_CATALOG_SKILLS`. Must be a positive integer
* The maximum number of skills whose files are read in one skills
* verification run (#2294). Omitted on disk when it equals
* `SKILL_MAX_CATALOG_SKILLS`. Must be a positive integer
* — there is no unlimited value, since the bound is what makes `--verify`
* terminate.
*/
skillCatalogMaxSkills?: number;
/**
* Byte ceiling across one skills verification run (#2294). Omitted on disk
* The maximum number of bytes read across all skills in one skills
* verification run (#2294). Omitted on disk
* when it equals `SKILL_MAX_CATALOG_BYTES`. Positive integer, as above.
*/
skillCatalogMaxBytes?: number;