Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/verify_writeguard.mjs

4.6 KiB, 1 run

created by r2519314175:831, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1// The stale-write guard: an agent that read a file, then finds it changed on
2// disk (another agent), must have its whole-file write REFUSED, not clobber.
3//
4// THE FIXTURE LIVES IN THE CHAT'S OWN SCRATCH, and moving it back to the workspace
5// root would make this file vacuous again. Until 2026-08-12 it seeded and wrote
6// `g.txt` at the root. Since the chat fence landed, a chat is confined to
7// `chats/<id>/work` (`scopeChatTo`, www/js/daimond.js), and `Tool::guard`
8// (src/tools.rs:5490) refuses a root path BEFORE the `Tool::FileWrite` arm at :5894,
9// which is where the `read_seen` hash comparison and the stale-write refusal live.
10// The fence preempted the guard, so nothing was written and `AGENT B WORK PRESERVED`
11// was true for the wrong reason: it passed with the entire stale-write guard deleted,
12// because the write never reached it. Seeded and written inside the scratch, the guard
13// is reachable again and the check has a subject.
14import { open, chat, newChat, errors } from './harness.mjs';
15
16const ok = [], bad = [];
17const check = (name, pass, detail) => {
18 (pass ? ok : bad).push(name + (detail ? ' — ' + detail : ''));
19 console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : ''));
20};
21
22const s = await open({ name: 'writeguard' });
23await newChat(s);
24
25// Where this chat may write, asked of the app: the fence is BUILT from
26// `chatScratch`, so a path composed here would be a second opinion about it.
27const dir = await s.page.evaluate(() => {
28 const f = window.DaimondAttach.focus();
29 return f && f.id ? window.DaimondAttach.chatScratch(f.id) : '';
30});
31check('the chat has a scratch folder, which is what the fence lets it write in',
32 !!dir, dir || '(no chat in focus)');
33const G = dir + '/g.txt';
34
35/// Write a file straight into OPFS, creating the folders on the way — this is the
36/// OTHER agent, so it goes nowhere near a turn.
37const putFile = (p, body) => s.page.evaluate(async ([p, body]) => {
38 const parts = p.split('/');
39 let d = await navigator.storage.getDirectory();
40 for (const seg of parts.slice(0, -1)) d = await d.getDirectoryHandle(seg, { create: true });
41 const fh = await d.getFileHandle(parts[parts.length - 1], { create: true });
42 const w = await fh.createWritable(); await w.write(body); await w.close();
43 return true;
44}, [p, body]);
45
46const readFile = (p) => s.page.evaluate(async (p) => {
47 const parts = p.split('/');
48 let d = await navigator.storage.getDirectory();
49 for (const seg of parts.slice(0, -1)) d = await d.getDirectoryHandle(seg);
50 const fh = await d.getFileHandle(parts[parts.length - 1]);
51 return await (await fh.getFile()).text();
52}, p).catch((e) => '(' + String(e).split('\n')[0] + ')');
53
54// Seed g.txt = v1, then have the agent READ it (records its hash in read_seen).
55await putFile(G, 'ORIGINAL-v1');
56const readOut = await chat(s, `@tool file_read {"path":"${G}"}`);
57// THE READ HAS TO LAND. Nothing below means anything if the agent never saw the
58// file: with no entry in `read_seen` a whole-file write is not stale, it is simply
59// a write, and the refusal this test is about could not arise however broken the
60// guard was.
61check('the agent really read the file, so the guard has something to compare against',
62 /ORIGINAL-v1/.test(readOut) && !/Refused/.test(readOut),
63 readOut.slice(-160).replace(/\n/g, ' | '));
64
65// Another agent changes g.txt underneath (simulated via OPFS directly).
66await putFile(G, 'AGENT-B-WROTE-THIS');
67
68// The first agent now writes a stale whole-file over it.
69const out = await chat(s, `@tool file_write {"path":"${G}","content":"STALE-CLOBBER"}`);
70const after = await readFile(G);
71console.log('--- write-turn transcript tail ---\n' + out.slice(-300));
72console.log('file after stale write:', JSON.stringify(after));
73
74check('GUARD REFUSED THE STALE WRITE', /changed on disk/.test(out),
75 out.slice(-200).replace(/\n/g, ' | '));
76check('AGENT B WORK PRESERVED — the other agent\'s bytes are still on disk',
77 after === 'AGENT-B-WROTE-THIS', JSON.stringify(after));
78// The other half, and the reason a refusal is not enough on its own: the guard
79// must refuse THIS write and not writing in general.
80const fresh = await chat(s, `@tool file_write {"path":"${dir}/h.txt","content":"UNRELATED"}`);
81check('and a file this agent never read is still written — the guard refuses staleness, not writing',
82 await readFile(dir + '/h.txt') === 'UNRELATED', fresh.slice(-140).replace(/\n/g, ' | '));
83
84const errs = errors(s).filter(e => !/502|Bad Gateway/.test(e));
85check('nothing threw', errs.length === 0, errs.slice(0, 2).join(' | '));
86
87await s.close();
88console.log(`\n${ok.length} passed, ${bad.length} failed`);
89if (bad.length) { bad.forEach(b => console.log(' FAILED: ' + b)); process.exit(1); }