fix: address Copilot review round 2 on #2251

- SkillsScreen: key verdicts and React elements by manifest ROW INDEX,
  not URI. The checker deliberately tolerates a duplicated URI so it can
  report `duplicate-resource`; a URI key collided those rows into one
  verdict, so verifying either updated both and "Verify all" raced two
  different digest/size declarations into the same slot.
- SkillsScreen: hold the invalidation generation in React state keyed by
  the manifest, not a ref bumped during render. `useValueChange` runs its
  callback during render and requires setState-only purity — an abandoned
  render left the ref incremented and silently discarded a live
  verification.
- SkillsScreen: title a size disagreement "Size mismatch" and render its
  reason. `verifySkillResource` catches it before hashing, so the alert
  was showing "actual undefined" under "Digest mismatch".
- SkillsScreen: pass `contents` to ContentViewer so a base64 SKILL.md
  renders. The text-block form substituted "" and painted a blank preview
  for a file verification had just read correctly.
- skills.ts: add `malformed-size` for a size that is not a non-negative
  safe integer, and exclude such values from the 16 MiB total. A negative
  one could pull the sum back under the limit and hide a violation.
- managedSkillsState: make the overlap guard per-session instead of a
  boolean. A disconnect during an in-flight walk left it set, so the
  reconnect's own load no-oped and was never retried — permanently, if
  the stale request never settled. A stale `finally` can no longer clear
  the live session's guard either.
- skillsSchemas: require the `{ skill }` envelope for `skills/get`. The
  accepted SEP settles it, and normalizing an inline entry would let a
  non-conforming response past the one place that could report it.
- #2248 and the PR description: corrected — they said
  `ReadResourceDirectoryResultSchema` was already declared, which round 1
  removed. #2248 now owns defining it, and records the frontmatter
  cross-check gap too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018kfrH3uRNUM4JAk1reKtyY
Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-09-04 21:45:49 -04:00
commit bfeebd440d

View file

@ -126,6 +126,7 @@ export type SkillIssueCode =
| "missing-digest"
| "malformed-digest"
| "missing-size"
| "malformed-size"
| "duplicate-resource"
| "resource-outside-skill-root"
| "manifest-missing-self"
@ -288,6 +289,13 @@ export function checkSkillConformance(entry: SkillEntry): SkillIssue[] {
"Manifest entry declares no size, so it is excluded from the 16 MiB total and its length cannot be cross-checked.",
resourceUri: resource.uri,
});
} else if (!isUsableSize(resource.size)) {
issues.push({
code: "malformed-size",
severity: "error",
message: `Size ${resource.size} is not a non-negative integer byte length.`,
resourceUri: resource.uri,
});
}
}
@ -295,13 +303,28 @@ export function checkSkillConformance(entry: SkillEntry): SkillIssue[] {
}
/**
* Sum of the manifest's declared `size` fields. An entry that omits `size`
* contributes nothing rather than failing the sum — the limit check is about
* catching a server that is demonstrably over, and an incomplete manifest can
* only ever understate the total, so this never produces a false positive.
* Whether a declared `size` is a usable byte length. SEP-2640 defines it as the
* raw byte count, so anything that is not a non-negative safe integer is
* nonsense — and a *negative* one is worse than nonsense, because summing it
* would pull the manifest total back under the 16 MiB limit and hide a
* violation. Reported as `malformed-size` and excluded from the sum.
*/
function isUsableSize(size: number | undefined): size is number {
return size !== undefined && Number.isSafeInteger(size) && size >= 0;
}
/**
* Sum of the manifest's declared `size` fields. An entry that omits `size` — or
* declares an unusable one — contributes nothing rather than failing the sum:
* the limit check is about catching a server that is demonstrably over, and an
* incomplete manifest can only ever understate the total, so this never
* produces a false positive.
*/
export function totalSkillBytes(resources: readonly SkillResource[]): number {
return resources.reduce((sum, r) => sum + (r.size ?? 0), 0);
return resources.reduce(
(sum, r) => sum + (isUsableSize(r.size) ? r.size : 0),
0,
);
}
/** Outcome of comparing a fetched file against its advertised digest. */