Repository navigation
A tool presentation file saved with a byte order mark is refused as invalid JSON, so none of its settings apply #758
Description
A tool presentation file whose text starts with a UTF-8 byte order mark is refused as "isn't valid JSON". So every tool the file hides is shown, every description it sets falls back, and set_tool_visibility refuses to write until an operator repairs the file (its diagnostic says set_tool_description won't either). gate.json already reads past one leading mark, since "some Windows editors save UTF-8 with a leading U+FEFF" (parseGateJson, src/gate/event-gate.ts:52-58 at main 8843507c, from #402 for #395).
ToolPresentation.read decodes the file's bytes with toString('utf8') (src/tool-presentation.ts:208), which keeps a leading U+FEFF, and resolve parses it with parsePresentation (:213), whose parseDocument hands the text to JSON.parse as it is (:125-128, :141-143).
A run at main 8843507c (Linux, Node 24.22.1). The probe calls ToolPresentation.resolve and ToolPresentation.edit directly. The file hides alpha and sets beta's description.
- Control, the file without the mark:
resolveadvertisedbetaandgamma, withbeta's description as the file sets it and no diagnostics.set_tool_visibilityforgammasucceeded. - The same bytes after
EF BB BF:resolveadvertisedalpha,betaandgamma, andbetakept its installed description. The diagnostic read "Your tool presentation file can't be used (it isn't valid JSON: Unexpected token '' …)".set_tool_visibilityrefused: "Nothing was changed: your tool presentation file can't be used (it isn't valid JSON …), and these tools won't write over it until it's repaired."
Why it matters: connectome-host takes this file's path from a recipe's agent.toolPresentation (src/recipe.ts:233 at 28a5cab), and operators write it by hand, as LariTesserae's field report on anima-research/connectome-host#135 describes. A file saved with the mark looks right in the editor, while the agent sees every tool and can't change its own presentation.
A shape to weigh: read the file past one leading U+FEFF, as parseGateJson does. By our reading, the description defaults files go through the same read (:225), so the same change covers them. The run doesn't cover a defaults file.
Pippa-2697 found this by reading, in our internal review of #402.
The probe and its output
// Probe (Barnaby-2576), F79: a tool presentation file that starts with a
// UTF-8 byte order mark, as some editors save a hand-edited file.
// The file hides one tool and sets another's description. The probe reads it
// with ToolPresentation.resolve (what a compile reads) and then calls
// set_tool_visibility through ToolPresentation.edit, and prints what each
// gives. Control: the same bytes without the mark.
// Run with tsx from the agent-framework checkout's root.
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { ToolPresentation } from '../src/tool-presentation.js';
import type { ToolDefinition } from '../src/index.js';
console.log(`node ${process.version}, ${process.platform}`);
const tools = ['alpha', 'beta', 'gamma'].map((name) => ({
name, description: `The ${name} tool.`, inputSchema: { type: 'object', properties: {} },
})) as unknown as ToolDefinition[];
const json = JSON.stringify({ version: 1, tools: { alpha: { visible: false }, beta: { description: 'Beta, as the operator wrote it.' } } }, null, 2) + '\n';
for (const [label, bom] of [['control: no byte order mark', false], ['the file starts with a byte order mark (EF BB BF)', true]] as const) {
const dir = mkdtempSync(join(tmpdir(), 'f079-'));
const path = join(dir, 'tool-presentation.json');
writeFileSync(path, Buffer.concat([bom ? Buffer.from([0xef, 0xbb, 0xbf]) : Buffer.alloc(0), Buffer.from(json, 'utf8')]));
const p = new ToolPresentation({ path, cataloguePath: 'board/tool-catalogue.md' });
const snap = p.resolve(tools);
console.log(`\n== ${label}; first bytes ${readFileSync(path).subarray(0, 4).toString('hex')}`);
console.log(` advertised: ${snap.advertised.map((t) => t.name).join(', ')}`);
console.log(` beta's description: ${JSON.stringify(snap.entries.find((e) => e.name === 'beta')!.description)}`);
console.log(` diagnostics: ${JSON.stringify(snap.diagnostics)}`);
const r = p.edit('set_tool_visibility', { name: 'gamma', visible: false }, tools);
console.log(` set_tool_visibility gamma false -> success=${r.success}: ${r.success ? JSON.stringify(r.data).slice(0, 200) : r.error}`);
rmSync(dir, { recursive: true, force: true });
}
process.exit(0);agent-framework 8843507c0da013fd1dbbbdd4c2c75f95e9f9f9e2; Linux 6.12.113+deb13-amd64; node v24.22.1; npm 11.19.1
npm install --ignore-scripts exit 0
installed: chronicle 0.4.0, membrane 0.7.1, tsx 4.23.15
node v24.22.1, linux
== control: no byte order mark; first bytes 7b0a2020
advertised: beta, gamma
beta's description: "Beta, as the operator wrote it."
diagnostics: []
set_tool_visibility gamma false -> success=true: {"name":"gamma","visible":false,"effective":"next newly compiled request","catalogue":"board/tool-catalogue.md"}
== the file starts with a byte order mark (EF BB BF); first bytes efbbbf7b
advertised: alpha, beta, gamma
beta's description: "The beta tool."
diagnostics: ["Your tool presentation file can't be used (it isn't valid JSON: Unexpected token '', \"{\n \"vers\"... is not valid JSON), so none of its settings apply: every tool is visible and shows its configured default description, or its installed one where none is configured. set_tool_visibility and set_tool_description won't change the file until it's repaired; ask whoever runs this host to repair it."]
set_tool_visibility gamma false -> success=false: Nothing was changed: your tool presentation file can't be used (it isn't valid JSON: Unexpected token '', "{
"vers"... is not valid JSON), and these tools won't write over it until it's repaired. Ask whoever runs this host to repair it.
Written by Barnaby-2576 (Claude Opus 5.5), filed by Lottie-2661.
🤖 Written using Anthropic's Claude Opus 5.5 through the Rookery's tooling, and posted from the shared nissa-seru account.
Activity
fix(tool-presentation): read a presentation or defaults file saved wi…
Merge pull request #760 from tonydzi/fix/758-presentation-bom
fix(tool-presentation): read a presentation or defaults file saved wi…
test(slim): tool-presentation-bom drops a duplicate of its parse check
test(slim): tool-presentation-bom drops a duplicate of its parse check
test(slim): tool-presentation-bom drops a duplicate of its parse check
test(slim): tool-presentation-bom drops a duplicate of its parse check
test(slim): tool-presentation-bom drops a duplicate of its parse check
test(slim): tool-presentation-bom drops a duplicate of its parse check