commit 3f74c143c755983276bda90c84da94c21a57152f
parent ce7eb8cf5c2614bafb9c062ee6bf2474d63bf9de
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Wed, 19 Aug 2026 00:04:26 -0400
umtool: the manifest writer refuses any non-clip, not just a card
Same family as the previous commit, found by auditing for it. updateClip()
guarded `type === "card"`, so a window could have been written onto the `scroll`
or `chart` entries one real manifest carries. It now refuses anything that is not
`type === "clip"` and names what it found.
Defence in depth rather than a live hole: /api/report/window resolves through
clipsOf(), which was already right. The CLI's `umtool window` reaches this
directly, though.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat:
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/umtool/lib/report/manifest.mjs b/umtool/lib/report/manifest.mjs
@@ -131,7 +131,10 @@ export async function updateClip(dir, clipId, patch, { token = null } = {}) {
const manifest = JSON.parse(raw);
const entry = (manifest.timeline ?? []).find((e) => e.id === clipId);
if (!entry) throw new Error(`no timeline entry with id ${clipId}`);
- if (entry.type === "card") throw new Error(`${clipId} is a card, not a clip`);
+ // `!== "clip"`, not `=== "card"`. The timeline's vocabulary is open -- one
+ // real manifest carries `scroll` and `chart` entries -- and the card-only
+ // check would have let a window be written onto one of those.
+ if (entry.type !== "clip") throw new Error(`${clipId} is a ${entry.type ?? "non-clip"} entry, not a clip`);
const before = { start: entry.start, end: entry.end };
for (const k of WINDOW_FIELDS) {