Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/verify_scope.mjs

42.0 KiB, 1 run

created by r2519314175:659, 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// verify_scope.mjs — a worker is confined to its own Diamond, proved by running a command.
2//
3// `DaimondApp::set_diamond_scope` existed for a day with no caller anywhere in
4// the repository. It is the only thing that ever assigns `ToolContext::no_write`
5// in the browser build, so while it had none:
6//
7// * `fence_spec` took its `scoped == false` branch and pushed the whole
8// granted root into `rw`. Every command a dispatched agent ran was fenced to
9// the entire folder the user gave the machine hand, not to one Diamond.
10// * `Bound::Toolkit` could not arrive at all, so `Kit::resolve` always
11// resolved to nothing and a granted toolchain was inert.
12//
13// This file is the acceptance test for the caller, and it is BEHAVIOURAL: it
14// does not assert that a function was called or that a JSON string has a shape.
15// It creates two Diamonds with a real secret in each, dispatches a worker into
16// one, and asks the KERNEL whether that worker's command can read the other.
17//
18// ── What a scope fences, since 2026-08-13 ───────────────────────────
19//
20// A scope fences WRITING and RUNNING and leaves READING free inside whatever the
21// user already opened (`Bound::OnlyWriteUnder` in src/tools.rs). That changes the
22// shape of the scope this file reads back — it arrives in `write_allow`, not in
23// `allow` — and changes NOTHING about the fence below, which is the point: a
24// command is an opaque program, so `fence_spec` does not split its verbs, and the
25// compartment the kernel proves here is the same compartment it proved before.
26// If a later change makes a Diamond's command read the granted root, the three
27// `cat` checks below go red, and they are meant to.
28//
29// ── Why it is written the way it is ─────────────────────────────────
30//
31// The fence is captured from `fence_spec` rather than composed here. A test that
32// wrote out the expected fence by hand would be asserting that this file agrees
33// with itself; what matters is what the engine actually sends, so the request is
34// read back off the wire and the paths in it are compared with the Diamonds on
35// disk.
36//
37// The secret in the other Diamond is a nonce generated a moment earlier. Finding
38// it would be proof of a leak that nothing could have faked; not finding it is
39// only worth something because the SAME command finds the nonce in its own
40// Diamond, which is the control that runs beside it every time.
41//
42// ── The fixture that made this file prove the wrong world ───────────
43//
44// Until 2026-08-13 the two Diamonds were fenced to their OWN directories, and
45// this file made those directories: `fs.mkdirSync(<grant>/diamonds/<id>)`. With
46// that fixture on disk the fence resolved, the kernel duly proved the
47// compartment, and the file was green throughout the fortnight in which every
48// `run` in every chat and every Diamond terminal was refused in the field.
49//
50// A Diamond's own directory is in the BROWSER'S storage whatever folder is open
51// (`is_store_path` in src/tools.rs), so nothing on the user's machine ever
52// creates it. `fence_spec` mapped it under the granted root anyway, the hand
53// could not canonicalise the path, and it refused the WHOLE fence — taking the
54// user's real, attached, perfectly good folder down with it. The one directory
55// this file created was the one whose absence was the bug.
56//
57// So the fixture is now what the app actually produces: two folders the user
58// ATTACHED, `work-a` and `work-b`, one to each Diamond. Nothing under
59// `<grant>/diamonds/` is created here, and a check below asserts that no path
60// under it ever reaches the fence. The kernel is still the oracle and the
61// compartment is still proved through it; only the world it is proved in is now
62// the world the app inhabits.
63//
64// ── Running it ──────────────────────────────────────────────────────
65//
66// xvfb-run -a -s "-screen 0 1400x900x24" node dev/verify_scope.mjs
67//
68// --keep leave the scratch tree behind
69// --engine <dir> load the engine from `www/<dir>` instead of `www/pkg`,
70// which is how `dev/breakproof_storefence.sh` runs this
71// file against a bundle built with the fence break put
72// back. An ARGUMENT and never an environment variable: an
73// env var set once leaks into every later run in that
74// shell, and a verifier quietly measuring a package nobody
75// meant to test is the exact failure this file exists to
76// catch.
77//
78// Headed, because Chromium loads an unpacked extension in no other mode.
79import fs from 'node:fs';
80import net from 'node:net';
81import path from 'node:path';
82import { spawn, spawnSync } from 'node:child_process';
83import { fileURLToPath } from 'node:url';
84
85import { open as openApp, scratch, MOCK } from './harness.mjs';
86import { whyStaleBinary, whyStaleWasm, refuse } from './staleguard.mjs';
87
88const HERE = path.dirname(fileURLToPath(import.meta.url));
89const ROOT = path.join(HERE, '..');
90const SRC = path.join(ROOT, 'ext');
91const INSTALL = path.join(ROOT, 'hand/install/install.sh');
92const HAND = path.join(ROOT, 'hand/target/release/daimond-hand');
93
94const KEEP = process.argv.slice(2).includes('--keep');
95/// Which directory under `www/` the page imports the engine from.
96const ENGINE = (() => {
97 const i = process.argv.indexOf('--engine');
98 return i >= 0 && process.argv[i + 1] ? process.argv[i + 1] : 'pkg';
99})();
100
101const BASE = scratch('scope');
102const PROFILE = path.join(BASE, 'profile');
103const JOURNAL = path.join(BASE, 'journal');
104const GRANT = path.join(BASE, 'work');
105const HOSTS = path.join(PROFILE, 'NativeMessagingHosts');
106
107const ok = [], bad = [];
108const check = (name, pass, detail) => {
109 (pass ? ok : bad).push(name);
110 console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : ''));
111};
112const note = (s) => console.log(' · ' + s);
113const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
114const nonce = (tag) => `${tag}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 12)}`;
115
116function listening(port) {
117 return new Promise((resolve) => {
118 const s = net.connect(port, '127.0.0.1');
119 s.once('connect', () => { s.destroy(); resolve(true); });
120 s.once('error', () => resolve(false));
121 });
122}
123const started = [];
124async function serve(name, args, port) {
125 if (await listening(port)) { note(`${name} already up on ${port}`); return; }
126 const p = spawn('node', args, { cwd: ROOT, stdio: 'ignore' });
127 started.push(p);
128 for (let i = 0; i < 100; i++) {
129 if (await listening(port)) { note(`started ${name} on ${port}`); return; }
130 await sleep(100);
131 }
132 throw new Error(`${name} did not come up on ${port}`);
133}
134
135// ── Build and install ───────────────────────────────────────────────
136
137fs.rmSync(BASE, { recursive: true, force: true });
138for (const d of [PROFILE, JOURNAL, GRANT, HOSTS]) fs.mkdirSync(d, { recursive: true });
139fs.chmodSync(JOURNAL, 0o700);
140
141// CARGO_TARGET_DIR is removed, or the binary this registers is whatever was
142// built last: see the same note in `verify_kitfence.mjs`, where an inherited one
143// made a security test pass against a binary from before the fix.
144const buildEnv = { ...process.env };
145delete buildEnv.CARGO_TARGET_DIR;
146// `DAIMOND_NO_BUILD` skips the build and NOTHING else: the staleness guard below
147// runs either way, so it cannot make this file report success against code it did
148// not test — only refuse. It exists because cargo relinks an output it finds
149// backdated, so with the build in the way the guard can never be watched
150// refusing. Same hatch as `PTYEDGE_NO_BUILD` in verify_ptyedge.mjs.
151const built = process.env.DAIMOND_NO_BUILD ? { status: 0, stderr: '' }
152 : spawnSync('cargo', ['build', '--release', '--manifest-path', 'hand/Cargo.toml'],
153 { cwd: ROOT, encoding: 'utf8', env: buildEnv });
154check('the hand builds from source', built.status === 0 && fs.existsSync(HAND),
155 (built.stderr || '').split('\n').filter((l) => /^error/.test(l)).slice(0, 3).join(' | '));
156if (built.status !== 0) { console.log('\n0 ok, 1 failed'); process.exit(1); }
157
158// A build that exits 0 says the compiler was happy, not that `HAND` is what it
159// produced. Cargo's own dep-info file is the oracle — every source that went
160// into the link, this crate's and every fe2o3 crate's.
161refuse(whyStaleBinary(HAND, {
162 subject: 'The fence this file measures',
163 what: 'hand',
164 rebuild: 'cargo build --release --manifest-path hand/Cargo.toml',
165}));
166
167// ── The engine's half ───────────────────────────────────────────────
168//
169// The wasm composes the fence the hand is handed, so it is the half under test
170// here. This compared it against `src/tools.rs` and `src/wasm/app.rs` alone,
171// which on 2026-08-03 missed `src/wasm/opfs.rs`, `src/wasm/diamond.rs`,
172// `src/prompts.rs` and `src/skills.rs` — all changed that day — and misses every
173// fe2o3 crate always. There is no dep-info to be had for a wasm bundle (see
174// `dev/staleguard.mjs`), so the oracle is every `.rs` under `src/`: coarse, and
175// a superset of anything hand-picked.
176refuse(whyStaleWasm(path.join(ROOT, `www/${ENGINE}/oxedyne_daimond_bg.wasm`), path.join(ROOT, 'src'), {
177 subject: 'The scope the engine composes',
178 holds: '`diamond_bounds` and the fence it builds',
179}));
180if (ENGINE !== 'pkg') {
181 console.log(`\n !!!! THIS RUN IS NOT A RESULT. The engine under test is www/${ENGINE}, which is`);
182 console.log( ' not the app\'s bundle. It is here to be watched FAILING; a green run against');
183 console.log( ' it would mean the checks below cannot tell the two worlds apart.\n');
184}
185
186fs.writeFileSync(path.join(JOURNAL, 'root.txt'),
187 `# The one folder Daimond's machine hand may work in.\n${GRANT}\n`);
188const inst = spawnSync('bash', [INSTALL, '--dir', HOSTS, HAND], { cwd: ROOT, encoding: 'utf8' });
189check('install.sh registers the real binary in the test profile', inst.status === 0,
190 (inst.stderr || inst.stdout || '').trim().split('\n').slice(-1)[0]);
191
192process.env.DAIMOND_HAND_JOURNAL_DIR = JOURNAL;
193delete process.env.DAIMOND_HAND_ROOT;
194
195// What the children will bind: `serve.mjs` reads DAIMOND_PORT and `mockllm.mjs`
196// DAIMOND_MOCK_PORT, so the wait below is asking about the port they chose.
197const APP_PORT = Number(process.env.DAIMOND_PORT || 8777);
198const MOCK_PORT = Number(process.env.DAIMOND_MOCK_PORT || 9099);
199await serve('dev server', ['dev/serve.mjs'], APP_PORT);
200await serve('mock provider', ['dev/mockllm.mjs'], MOCK_PORT);
201
202// ── The browser ─────────────────────────────────────────────────────
203
204const s = await openApp({ headed: true, name: 'scope', extension: SRC, profile: PROFILE });
205const b = s.browser;
206const page = s.page;
207
208async function allowHand(ms = 30000) {
209 const until = Date.now() + ms;
210 while (Date.now() < until) {
211 for (const p of b.pages()) {
212 if (/grant\.html/.test(p.url())) {
213 await p.waitForLoadState('domcontentloaded').catch(() => {});
214 await sleep(300);
215 await p.click('#allow').catch(() => {});
216 return true;
217 }
218 }
219 await sleep(150);
220 }
221 return false;
222}
223
224const MINE = nonce('mine');
225const THEIRS = nonce('theirs');
226
227try {
228 await sleep(500);
229 check('the extension announced itself to the app',
230 await page.evaluate(() => !!document.documentElement.dataset.daimondHands));
231
232 await page.evaluate(() => window.DaimondHand._setWaitsForTest({ grace: 30000, slack: 60000, hello: 20000 }));
233
234 // The engine module, whichever package it comes from. `www/pkg` is the one
235 // the app booted, so it is already initialised; ANY other package has never
236 // been, and wasm-bindgen's exports are inert until it is — every call comes
237 // back "Cannot read properties of undefined (reading '__wbindgen_malloc')".
238 // Once per package, because a second `init()` instantiates a second module
239 // and the store the first one opened would be left behind it.
240 await page.evaluate(() => {
241 const done = {};
242 window.__mod = async (name) => {
243 const mod = await import(`../${name}/oxedyne_daimond.js`);
244 if (name !== 'pkg' && !done[name]) { await mod.default(); done[name] = true; }
245 return mod;
246 };
247 });
248
249 // Two Diamonds, made through the app's own edge so they are real store entries.
250 const ids = await page.evaluate(async ([mock, engine]) => {
251 const mod = await window.__mod(engine);
252 const app = new mod.DaimondApp(mock, 'k', 'mock', 256, '', true);
253 const a = await app.create_diamond('Alpha');
254 const c = await app.create_diamond('Beta');
255 return { a, c };
256 }, [MOCK, ENGINE]);
257 check('two Diamonds exist', !!ids.a && !!ids.c, JSON.stringify(ids));
258
259 // A folder each, ATTACHED — the only kind of place a Diamond has on this
260 // machine. The user makes these with a directory picker and the app never
261 // creates them; `<grant>/diamonds/<id>` is deliberately NOT created, because
262 // nothing in the field creates it either (see the note at the head of this
263 // file). The granted folder is also the workspace the file tools resolve
264 // against, so a workspace-relative bound and an absolute fence path name the
265 // same place.
266 const dirA = path.join(GRANT, 'work-a');
267 const dirB = path.join(GRANT, 'work-b');
268 const store = path.join(GRANT, 'diamonds');
269 fs.mkdirSync(dirA);
270 fs.mkdirSync(dirB);
271 fs.writeFileSync(path.join(dirA, 'own.txt'), MINE + '\n');
272 fs.writeFileSync(path.join(dirB, 'secret.txt'), THEIRS + '\n');
273 // A THIRD folder, on the disk and marked into nothing. `walk_reach` sends a start under no
274 // mark to browser storage, which has never heard of this name, so it is the exact case
275 // `dev/BLOCKERS.md` B1 measures: a walk over a folder on this computer, answered by the
276 // other filesystem. Nothing in browser storage is ever made to match it.
277 const dirC = path.join(GRANT, 'work-c');
278 fs.mkdirSync(dirC);
279 fs.writeFileSync(path.join(dirC, 'unmarked.txt'), nonce('unmarked') + '\n');
280
281 // ── The fence the engine actually composes ──────────────────────
282 //
283 // Captured from `fence_spec` through the same edge `Tool::run` uses, rather
284 // than written out here: what matters is what the ENGINE sends.
285 //
286 // `pty_request` asks the relay where the hand's grant is, and the relay now refuses to
287 // answer for a page whose workspace is not that folder (`hand/REVIEW.md` §1.14). This
288 // browser's workspace is the OPFS sandbox — no automated one can be anything else, because
289 // a real folder needs a native dialog — so `status` is stood in for here with what the real
290 // hand actually reported a moment ago. The COMPOSER is the real one, and the fence it
291 // returns is what is then sent down the real relay to the real hand.
292 await page.evaluate((root) => {
293 const real = window.DaimondHand.status;
294 window.DaimondHand._realStatus = real;
295 window.DaimondHand.status = () => Promise.resolve(JSON.stringify({
296 paired: true, transport: 'machine', machine: 'test', os: 'linux',
297 root: root, caps: ['fence:linux', 'landlock:abi-8'],
298 }));
299 }, GRANT);
300
301 const fenced = await page.evaluate(async ({ id, root, mock, engine }) => {
302 const mod = await window.__mod(engine);
303 const app = new mod.DaimondApp(mock, 'k', 'mock', 256, '', true);
304 const before = JSON.parse(app.diamond_scope());
305 app.set_diamond_scope('diamonds/' + id, '["work-a"]', '[]', '[]');
306 const after = JSON.parse(app.diamond_scope());
307 // The pty composer is the one edge that hands a fence back as text, and it
308 // is the SAME `fence_spec` a command goes through.
309 //
310 // `cwd` is the Diamond's own directory because that is what the Terminal
311 // panel sends — `cwd: b.own_dir` in `Files.bounds`'s caller — and a fence
312 // question answered against a tidier request than the app makes is a
313 // question about a different app.
314 const req = JSON.parse(await mod.pty_request(JSON.stringify({
315 own_dir: 'diamonds/' + id, attached: ['work-a'], read_only: [], toolkits: [],
316 cwd: 'diamonds/' + id, cols: 80, rows: 24,
317 })));
318 // And the same Diamond with nothing attached to it, which is what a fresh
319 // one is: no folder on this machine, so nothing to fence and nothing to
320 // open. The sentence matters as much as the refusal — see the check.
321 const bare = JSON.parse(await mod.pty_request(JSON.stringify({
322 own_dir: 'diamonds/' + id, attached: [], read_only: [], toolkits: [],
323 cwd: 'diamonds/' + id, cols: 80, rows: 24,
324 })));
325 return { before, after, req, bare, root };
326 }, { id: ids.a, root: GRANT, mock: MOCK, engine: ENGINE });
327
328 check('an unscoped agent declares no allow-list at all',
329 Array.isArray(fenced.before.allow) && fenced.before.allow.length === 0
330 && (fenced.before.write_allow || []).length === 0
331 && fenced.before.nowhere === false,
332 JSON.stringify(fenced.before));
333 // Read back off `write_allow`, which is where a scope lands. `allow` is the
334 // both-verb list and is empty for every scope this build composes; a check
335 // written against it would pass only in a world where a daimon could not read
336 // its user's files, and `scopeAgentTo` in daimond.js reads the same field.
337 check('a scoped agent reads back the Diamond it was confined to',
338 (fenced.after.write_allow || []).indexOf('diamonds/' + ids.a) >= 0
339 && (fenced.after.write_allow || []).indexOf('work-a') >= 0
340 && (fenced.after.allow || []).length === 0,
341 JSON.stringify(fenced.after));
342 const fence = fenced.req.fence || {};
343 const rw = fence.rw || [];
344 check('and the fence the engine composed names the folder attached to it, not the whole grant',
345 rw.indexOf(dirA) >= 0 && rw.indexOf(GRANT) < 0,
346 JSON.stringify(fenced.req).slice(0, 300));
347 // THE REGRESSION. A Diamond's own directory is in the browser's storage, so
348 // `<grant>/diamonds/<id>` is a directory nobody makes; a fence naming it is a
349 // fence the hand cannot resolve, and it refuses the whole spec — the real
350 // folder beside it included. Asserted over every list at once, because the
351 // path was equally fatal in `rw`, in `ro` and in a read carve-out.
352 check('and no part of it names a place inside Daimond\'s own storage, which is not on the disk',
353 JSON.stringify(fence).indexOf(store) < 0, JSON.stringify(fence).slice(0, 300));
354 check('and it does not name the other Diamond, or the folder attached to it, in any list',
355 !JSON.stringify(fence).includes(ids.c) && !JSON.stringify(fence).includes(dirB),
356 JSON.stringify(fence));
357 // The panel asks for a terminal in the Diamond's own directory, because that
358 // is what a Diamond is to it. The session has to start somewhere real, and
359 // `tools::start_dir` says where: the first folder the user attached.
360 check('and the session starts in that folder, though the panel asked for the Diamond itself',
361 fenced.req.cwd === dirA, JSON.stringify(fenced.req.cwd));
362 // A wall a person can get through. The refusal is shown to the USER, in the
363 // Terminal panel, verbatim: it has to name the thing they can do about it.
364 check('a Diamond with nothing attached is refused a terminal, and told to attach a folder',
365 !!fenced.bare.refused && fenced.bare.t === undefined
366 && /attach/i.test(fenced.bare.refused) && /Workspace panel/.test(fenced.bare.refused),
367 JSON.stringify(fenced.bare).slice(0, 300));
368
369 // ── The command, and the kernel ─────────────────────────────────
370 //
371 // The fence just captured, sent down the real relay to the real hand. Not a
372 // fence this file composed: the object came out of `fence_spec`.
373 const grant = allowHand();
374 // `cwd` defaults to the fence's first writable root and can be named, so that
375 // a fence under test is the only thing under test: a working directory that
376 // does not exist is refused for its own reasons, and would stand in for the
377 // refusal being asked about.
378 const send = async (argv, fence, cwd) => {
379 const raw = await page.evaluate(({ argv, fence, cwd }) =>
380 window.DaimondHand.run(JSON.stringify({
381 t: 'exec', id: 'sc-' + Math.random().toString(36).slice(2, 8),
382 argv, cwd: cwd || fence.rw[0], env: [], stdin: null,
383 timeout_ms: 30000, capture: 'both', fence, toolkits: [],
384 })).then((v) => ({ ok: v }), (e) => ({ err: (e && e.message) || String(e) })),
385 { argv, fence, cwd: cwd || null });
386 if (raw.err) return { refused: raw.err };
387 try { return { out: JSON.parse(raw.ok) }; } catch (e) { return { refused: raw.ok }; }
388 };
389
390 const own = await send(['/bin/cat', path.join(dirA, 'own.txt')], fenced.req.fence);
391 await grant;
392 check('a command in Diamond A reads the folder attached to Diamond A',
393 !!own.out && String(own.out.stdout || '').includes(MINE),
394 JSON.stringify(own).slice(0, 300));
395
396 const theirs = await send(['/bin/cat', path.join(dirB, 'secret.txt')], fenced.req.fence);
397 check('and the same command cannot read the folder attached to Diamond B',
398 !own.refused && !(theirs.out && String(theirs.out.stdout || '').includes(THEIRS)),
399 JSON.stringify(theirs).slice(0, 300));
400 check('and it is the KERNEL that refuses it, not a string check',
401 !!theirs.out && /Permission denied/i.test(String(theirs.out.stderr || '')),
402 JSON.stringify(theirs).slice(0, 300));
403 check('and Diamond B\'s secret never came back at all',
404 !JSON.stringify(theirs).includes(THEIRS), JSON.stringify(theirs).slice(0, 200));
405
406 const wrote = await send(
407 ['/bin/sh', '-c', `echo x > ${path.join(dirB, 'planted.txt')}`], fenced.req.fence);
408 check('nor write into Diamond B\'s folder', !fs.existsSync(path.join(dirB, 'planted.txt')),
409 JSON.stringify(wrote).slice(0, 200));
410
411 // ── The other door, on the same scope ───────────────────────────
412 //
413 // The kernel has just refused a COMMAND the other Diamond. The file tools are
414 // the other half of the same bound list, and they answer differently on
415 // purpose: reading is free, writing is not. Asserted here rather than left to
416 // the unit tests, because "the scope took" is a claim about the engine the page
417 // actually loaded, and because a refusal with no permission beside it would
418 // pass just as well on a scope that had failed shut.
419 //
420 // In the browser's own storage, which is where a file tool works: this browser
421 // has no real folder open (no automated one can — a real folder needs a native
422 // dialog), so the paths below are OPFS paths and never the disk fixtures above.
423 const doors = await page.evaluate(async ({ id, mock, engine }) => {
424 const mod = await window.__mod(engine);
425 const free = new mod.DaimondApp(mock, 'k', 'mock', 256, '', true);
426 // The folders first: a hand is paired and no folder is open, so a write that would
427 // INVENT one in browser storage is refused rather than landed in the filesystem
428 // nobody meant (`write_place`, src/tools.rs, 2026-08-24).
429 await free.run_tool('dir_create', JSON.stringify({ path: 'work-b' }));
430 await free.run_tool('dir_create', JSON.stringify({ path: '.daimond' }));
431 await free.run_tool('file_write', JSON.stringify({
432 path: 'work-b/secret.txt', content: 'THE OTHER DIAMONDS FILE\n' }));
433 await free.run_tool('file_write', JSON.stringify({
434 path: '.daimond/config.json', content: '{"seeded":true}\n' }));
435 const app = new mod.DaimondApp(mock, 'k', 'mock', 256, '', true);
436 app.set_diamond_scope('diamonds/' + id, '["work-a"]', '[]', '[]');
437 const t = (n, a) => app.run_tool(n, JSON.stringify(a)).then(String);
438 return {
439 read: await t('file_read', { path: 'work-b/secret.txt' }),
440 write: await t('file_write', { path: 'work-b/secret.txt', content: 'clobbered\n' }),
441 own: await t('file_read', { path: '.daimond/config.json' }),
442 after: await free.run_tool('file_read',
443 JSON.stringify({ path: 'work-b/secret.txt' })).then(String),
444 };
445 }, { id: ids.a, mock: MOCK, engine: ENGINE });
446 check('the same scope reads a file nobody attached, through the file tools',
447 /THE OTHER DIAMONDS FILE/.test(doors.read), doors.read.slice(0, 120));
448 check('and cannot write it — which is the half that must never widen',
449 /Refused/.test(doors.write), doors.write.slice(0, 120));
450 check('and that refusal is real: the file is untouched',
451 /THE OTHER DIAMONDS FILE/.test(doors.after) && !/clobbered/.test(doors.after),
452 doors.after.slice(0, 120));
453 check('while Daimond\'s own directory is refused BOTH ways, seeded so it is a denial and not an absence',
454 /Refused/.test(doors.own), doors.own.slice(0, 120));
455
456 // ── The FILE tools reaching this machine, and stopping where a command stops ──
457 //
458 // Since 2026-08-25 a file tool whose path is under a folder the user marked in
459 // changes the real file on this computer, through the hand, behind the fence
460 // `fence_spec` builds (`dev/BLOCKERS.md` B2). That is a second road to the disk
461 // and it has to be fenced like the first, so it is proved here, through the
462 // kernel, in the same world and against the same two nonces.
463 //
464 // The decoy is what makes the second check worth something. `work-b/secret.txt`
465 // exists TWICE — on disk holding Diamond B's nonce, and in browser storage
466 // holding "THE OTHER DIAMONDS FILE", written by the free app a moment ago. A
467 // door that leaked would answer with the nonce, which nothing could have faked;
468 // answering with the decoy is the door correctly reading the other filesystem.
469 const filedoor = await page.evaluate(async ({ id, mock, engine, mine }) => {
470 const mod = await window.__mod(engine);
471 const app = new mod.DaimondApp(mock, 'k', 'mock', 256, '', true);
472 app.set_diamond_scope('diamonds/' + id, '["work-a"]', '[]', '[]');
473 const t = (n, a) => app.run_tool(n, JSON.stringify(a)).then(String);
474 return {
475 read: await t('file_read', { path: 'work-a/own.txt' }),
476 edit: await t('file_edit', {
477 path: 'work-a/own.txt', old_string: mine, new_string: mine + '-EDITED' }),
478 // A second file, so the ambiguous edit below cannot disturb the one just checked.
479 seed: await t('file_write', { path: 'work-a/twice.txt', content: 'x\nx\n' }),
480 twice: await t('file_edit', {
481 path: 'work-a/twice.txt', old_string: 'x', new_string: 'y' }),
482 theirs: await t('file_read', { path: 'work-b/secret.txt' }),
483 plant: await t('file_write', {
484 path: 'work-b/planted.txt', content: 'planted by a file tool\n' }),
485 };
486 }, { id: ids.a, mock: MOCK, engine: ENGINE, mine: MINE });
487 check('a file tool in Diamond A reads the folder attached to Diamond A, off the disk',
488 filedoor.read.includes(MINE), filedoor.read.slice(0, 160));
489 check('and EDITS the real file on this computer, with no command anywhere',
490 fs.readFileSync(path.join(dirA, 'own.txt'), 'utf8').includes(MINE + '-EDITED'),
491 filedoor.edit.slice(0, 200));
492 check('and a file_write of a new file lands on the disk too',
493 fs.existsSync(path.join(dirA, 'twice.txt')), filedoor.seed.slice(0, 200));
494 check('and an old_string that is not unique is refused WITH ITS COUNT, changing nothing',
495 /appears 2 times/.test(filedoor.twice)
496 && fs.readFileSync(path.join(dirA, 'twice.txt'), 'utf8') === 'x\nx\n',
497 filedoor.twice.slice(0, 200));
498 check('and the same file tool cannot see the folder attached to Diamond B',
499 !filedoor.theirs.includes(THEIRS), filedoor.theirs.slice(0, 200));
500 check('nor write into it: the disk is untouched and nothing was planted',
501 !fs.existsSync(path.join(dirB, 'planted.txt')), filedoor.plant.slice(0, 200));
502 // ── A relay that cannot carry a file operation SAYS SO, and does not hang ──
503 //
504 // Found by `dev/verify_chatfence.mjs` hanging for eight minutes on 2026-08-25. A
505 // `#[wasm_bindgen(method)]` import that is not on the object throws when it is
506 // called, and a throw out of a declared-infallible import does not become an
507 // `Err`: the promise is never made and the tool call waits for ever. Every relay
508 // older than the file door is that case, and so is every test stub written before
509 // the verb existed.
510 //
511 // The race is the check. A hang cannot be asserted by waiting for it, so the call
512 // is given eight seconds and losing the race IS the failure.
513 const stale = await page.evaluate(async ({ id, mock, engine }) => {
514 const keep = window.DaimondHand.file;
515 delete window.DaimondHand.file;
516 try {
517 const mod = await window.__mod(engine);
518 const app = new mod.DaimondApp(mock, 'k', 'mock', 256, '', true);
519 app.set_diamond_scope('diamonds/' + id, '["work-a"]', '[]', '[]');
520 return await Promise.race([
521 app.run_tool('file_edit', JSON.stringify({
522 path: 'work-a/own.txt', old_string: 'x', new_string: 'y' })).then(String),
523 new Promise((r) => setTimeout(() => r('__HUNG__'), 8000)),
524 ]);
525 } finally {
526 window.DaimondHand.file = keep;
527 }
528 }, { id: ids.a, mock: MOCK, engine: ENGINE });
529 check('a relay too old to carry a file operation answers a sentence rather than hanging',
530 stale !== '__HUNG__' && /older than this version/.test(stale), stale.slice(0, 200));
531 check('and it does NOT quietly write into browser storage instead',
532 !/Wrote|Edited/.test(stale), stale.slice(0, 160));
533
534 // ── The SEARCH door, which walks rather than opens one path ────────────
535 //
536 // A search is the one file operation whose reach is decided by a walk, so a
537 // fence that holds for `file_read` says nothing about it: the walk chooses its
538 // own paths as it goes. Diamond B's folder holds a nonce and Diamond A's holds
539 // another, and the same pattern is asked for in one call from a scope that has
540 // only A. Finding B's would be proof of a leak that nothing could have faked.
541 const searched = await page.evaluate(async ({ id, mock, engine }) => {
542 const mod = await window.__mod(engine);
543 const app = new mod.DaimondApp(mock, 'k', 'mock', 256, '', true);
544 app.set_diamond_scope('diamonds/' + id, '["work-a"]', '[]', '[]');
545 const t = (n, a) => app.run_tool(n, JSON.stringify(a)).then(String);
546 // One file in the Diamond's OWN directory, which is browser storage by construction and
547 // is what a split walk has to report beside the disk.
548 await t('file_write', {
549 path: 'diamonds/' + id + '/own-note.txt', content: 'the Diamond\'s own side\n' });
550 return {
551 // No `path`: the walk starts at the turn's own marks, which is the
552 // default a daimon actually gets and the one that decides its reach.
553 mine: await t('file_search', { query: 'mine-' }),
554 // And aimed straight at the other Diamond, which is the real claim.
555 far: await t('file_search', { query: 'theirs-', path: 'work-b' }),
556 glob: await t('file_glob', { pattern: '**/*.txt' }),
557 };
558 }, { id: ids.a, mock: MOCK, engine: ENGINE });
559 check('a file_search from Diamond A finds the nonce in the folder attached to it',
560 searched.mine.includes(MINE), searched.mine.slice(0, 220));
561 check('and it reports a real path on the disk, not one in browser storage',
562 /own\.txt/.test(searched.mine), searched.mine.slice(0, 220));
563 check('and the same search never returns Diamond B\'s nonce',
564 !searched.mine.includes(THEIRS), searched.mine.slice(0, 300));
565 check('and a search AIMED at Diamond B comes back without it',
566 !searched.far.includes(THEIRS), searched.far.slice(0, 300));
567 check('and file_glob lists Diamond A\'s file and not Diamond B\'s',
568 /own\.txt/.test(searched.glob) && !/secret\.txt/.test(searched.glob),
569 searched.glob.slice(0, 300));
570 // A DEFAULT WALK SPANS BOTH FILESYSTEMS AND MUST REPORT BOTH. A scoped Diamond's marks
571 // always include its own directory, which is browser storage by construction, so every
572 // default `file_glob` in a Diamond with a folder attached is a split walk. `file_glob`'s
573 // machine arm composed its report and RETURNED, dropping the browser half on the floor;
574 // `file_search`'s falls through and merges. This is the check that tells the two apart.
575 check('and a default file_glob reports the Diamond\'s own storage as well as the disk',
576 /own\.txt/.test(searched.glob) && /own-note\.txt/.test(searched.glob),
577 searched.glob.slice(0, 400));
578
579 // ── AND WHEN A WALK FINDS NOTHING, WHICH WORLD IS THAT ABOUT? ──────────
580 //
581 // `dev/BLOCKERS.md` B1's open half. Both walking tools matched their directory read with a
582 // bare `continue`, so a start browser storage does not hold was walked as an empty tree and
583 // the answer came back `No matches for '<query>'.` -- true of browser storage, read as a
584 // statement about this computer, and said by the two tools a daimon reaches for FIRST.
585 //
586 // `work-c` is on the disk and under no mark, so the walk stays in browser storage, which has
587 // no such folder. The answer may still be empty; what it may not be is silent about where it
588 // looked.
589 const nowhere = await page.evaluate(async ({ id, mock, engine }) => {
590 const mod = await window.__mod(engine);
591 const app = new mod.DaimondApp(mock, 'k', 'mock', 256, '', true);
592 app.set_diamond_scope('diamonds/' + id, '["work-a"]', '[]', '[]');
593 const t = (n, a) => app.run_tool(n, JSON.stringify(a)).then(String);
594 return {
595 search: await t('file_search', { query: 'unmarked-', path: 'work-c' }),
596 glob: await t('file_glob', { pattern: '*.txt', path: 'work-c' }),
597 };
598 }, { id: ids.a, mock: MOCK, engine: ENGINE });
599 check('a file_search over a folder browser storage does not hold says which filesystem answered',
600 /this browser's own storage/.test(nowhere.search), nowhere.search.slice(0, 400));
601 check('and it says the directory would not open rather than walking past it in silence',
602 /would not open/.test(nowhere.search), nowhere.search.slice(0, 400));
603 check('and it names the other filesystem and the tool that reaches it',
604 /on this computer/.test(nowhere.search) && /Reach it with run/.test(nowhere.search),
605 nowhere.search.slice(0, 400));
606 check('and file_glob answers the same way, being the other tool a daimon reaches for first',
607 /this browser's own storage/.test(nowhere.glob) && /would not open/.test(nowhere.glob),
608 nowhere.glob.slice(0, 400));
609 // THE CONTROL, without which every check above would pass on a note printed unconditionally.
610 // The same tools, over the folder that IS marked in, answer off the disk and say none of it.
611 check('while a search that really did reach this computer says none of that',
612 searched.mine.includes(MINE) && !/this browser's own storage/.test(searched.mine)
613 && !/would not open/.test(searched.mine),
614 searched.mine.slice(0, 400));
615 check('nor does a glob that found something',
616 !/this browser's own storage/.test(searched.glob), searched.glob.slice(0, 400));
617
618 // ── And the KERNEL, not the app, is what makes that last one true ──────
619 //
620 // Every refusal above came from the engine deciding the path was not a machine
621 // path at all, which is the right first answer and is not the guarantee. The
622 // guarantee is one process further on: the fence `fence_spec` composed, sent
623 // down the real relay as a real `file` request naming Diamond B's file, with
624 // the engine's own decision taken out of the way. It reaches the same launcher
625 // a command reaches, applies the same Landlock ruleset, and the refusal comes
626 // back in the kernel's words.
627 const fsend = async (op, target, extra) => {
628 const raw = await page.evaluate(({ op, target, fence, extra }) =>
629 window.DaimondHand.file(JSON.stringify(Object.assign({
630 t: 'file', id: 'sf-' + Math.random().toString(36).slice(2, 8),
631 op, path: target, cwd: fence.rw[0], fence, toolkits: [],
632 }, extra || {}))).then((v) => ({ ok: v }), (e) => ({ err: (e && e.message) || String(e) })),
633 { op, target, fence: fenced.req.fence, extra: extra || null });
634 if (raw.err) return { refused: raw.err };
635 try { return { out: JSON.parse(raw.ok) }; } catch (e) { return { refused: raw.ok }; }
636 };
637 const kread = await fsend('read', path.join(dirB, 'secret.txt'), { offset: 1, limit: 0 });
638 const kwrite = await fsend('write', path.join(dirB, 'planted.txt'), { text: 'planted\n' });
639 const kmine = await fsend('read', path.join(dirA, 'own.txt'), { offset: 1, limit: 0 });
640 check('the same file request, sent past the engine, is refused by the kernel for Diamond B',
641 !!kread.out && kread.out.ok === false && /fence/i.test(String(kread.out.text || '')),
642 JSON.stringify(kread).slice(0, 300));
643 check('and Diamond B\'s secret never came back at all, by that road either',
644 !JSON.stringify(kread).includes(THEIRS), JSON.stringify(kread).slice(0, 200));
645 check('nor could it write there, and nothing was planted',
646 !fs.existsSync(path.join(dirB, 'planted.txt')), JSON.stringify(kwrite).slice(0, 200));
647 // The permission beside the refusal, without which every refusal above would be
648 // satisfied by a door that had failed shut.
649 check('while the same road reads Diamond A\'s own file freely',
650 !!kmine.out && kmine.out.ok === true && String(kmine.out.text || '').includes(MINE),
651 JSON.stringify(kmine).slice(0, 200));
652
653 // The kernel, again, and by the same road as the read: past the engine's own
654 // decision, with the fence `fence_spec` composed, walking straight at Diamond B.
655 const kwalk = await fsend('search', path.join(dirB, 'x'), {
656 paths: [dirB], query: 'theirs-', ci: false, glob: '', skip: [],
657 budget: 5000, cap: 1000000,
658 });
659 check('the same walk, sent past the engine, brings nothing back from Diamond B',
660 !JSON.stringify(kwalk).includes(THEIRS), JSON.stringify(kwalk).slice(0, 260));
661 check('and it RECORDS that a directory could not be opened, so an empty answer is not read as an empty folder',
662 !!kwalk.out && String(kwalk.out.text || '').split('\n')[0].split('\t')[9] === '1',
663 JSON.stringify(kwalk).slice(0, 260));
664 const kmine2 = await fsend('search', path.join(dirA, 'x'), {
665 paths: [dirA], query: 'mine-', ci: false, glob: '', skip: [],
666 budget: 5000, cap: 1000000,
667 });
668 check('while the same road searches Diamond A\'s own folder freely',
669 !!kmine2.out && kmine2.out.ok === true && String(kmine2.out.text || '').includes(MINE),
670 JSON.stringify(kmine2).slice(0, 220));
671
672
673 // Put back, so the checks below read the fixture they were written against.
674 fs.writeFileSync(path.join(dirA, 'own.txt'), MINE + '\n');
675
676 // The control, without which every refusal above proves nothing: the same
677 // file, with the fence the code used to compose — the whole granted root.
678 const unscoped = await send(['/bin/cat', path.join(dirB, 'secret.txt')],
679 { rw: [GRANT], ro: [], deny: [], net: false });
680 check('while the UNSCOPED fence — the one every agent had until today — reads it freely',
681 !!unscoped.out && String(unscoped.out.stdout || '').includes(THEIRS),
682 JSON.stringify(unscoped).slice(0, 200));
683
684 // ── The other control: the fence as it was composed until tonight ──
685 //
686 // Not a fence the engine can produce any more, so it is written out here and
687 // sent to the REAL hand, which is the end that decides. It is the good fence
688 // above with one path added: the Diamond's own directory, mapped under the
689 // granted root as `fence_spec` used to map it. Nothing creates that
690 // directory, the hand cannot canonicalise it, and it refuses the SPEC —
691 // which is why a Diamond with a perfectly good folder attached could not run
692 // a command either. The user's real folder is in this fence and reachable in
693 // principle; the hand still says no, and that is the whole shape of the
694 // outage.
695 const preFix = {
696 rw: [path.join(store, ids.a)].concat(fenced.req.fence.rw),
697 ro: fenced.req.fence.ro, deny: fenced.req.fence.deny, net: false,
698 };
699 const old = await send(['/bin/cat', path.join(dirA, 'own.txt')], preFix, dirA);
700 // The relay resolves with the hand's own sentence rather than rejecting, so
701 // the refusal arrives INSIDE the answer. Read both shapes: a check that knew
702 // only about a rejection would call a refusal that came back as data a pass.
703 const oldWhy = String((old.out && old.out.refused) || old.refused || '');
704 check('and the fence composed BEFORE tonight is refused outright by the hand, real folder and all',
705 /cannot be resolved/i.test(oldWhy) && oldWhy.includes(path.join(store, ids.a))
706 && !(old.out && String(old.out.stdout || '').includes(MINE)),
707 JSON.stringify(old).slice(0, 300));
708
709 // ── The toolkit grant, which only exists through this same call ──
710 const kits = await page.evaluate(async ({ id, mock, engine }) => {
711 const mod = await window.__mod(engine);
712 const app = new mod.DaimondApp(mock, 'k', 'mock', 256, '', true);
713 await app.set_toolkits(id, JSON.stringify(['rust', 'nonsense']));
714 const list = JSON.parse(await app.list_diamonds());
715 const row = list.find((d) => d.id === id) || {};
716 app.set_diamond_scope('diamonds/' + id, '["work-a"]', '[]', JSON.stringify(row.toolkits || []));
717 return { stored: row.toolkits || [], scope: JSON.parse(app.diamond_scope()) };
718 }, { id: ids.a, mock: MOCK, engine: ENGINE });
719 check('a toolkit grant is stored, and an unknown name is dropped rather than kept',
720 JSON.stringify(kits.stored) === '["rust"]', JSON.stringify(kits.stored));
721 check('and it reaches the turn\'s bounds, which is the only way a fence ever sees one',
722 JSON.stringify(kits.scope.toolkits) === '["rust"]', JSON.stringify(kits.scope));
723
724 // 502 is a dev server with no gateway behind it; 401 is a gateway that IS
725 // there and has nobody signed in, which is this file exactly — it holds no
726 // account and asks the gateway for nothing. The two are the same absence
727 // seen from either side, and which one the page meets depends on whether
728 // somebody else on this machine happens to have a gateway up.
729 //
730 // 402 and 403 are the same absence at other endpoints, and they are named rather than
731 // left to make this file red on a machine where a lane happens to have a gateway up.
732 // Traced on 2026-08-25 rather than assumed: one conversation answers 401 for
733 // `/api/tools` and `/api/admin?view=whoami`, **402 for `/api/sync`** and **403 for
734 // `/api/account`**, each a poll that lands only in a run long enough to reach it.
735 // Nothing in this file signs in, so all four are the page correctly being told it is
736 // nobody -- and the detail line prints the URLs beside the statuses, which is what
737 // made tracing them possible rather than guessing.
738 const noise = s.errs.filter((e) =>
739 !/favicon|ERR_ABORTED|502|Bad Gateway|401 \(Unauthorized\)|402 \(Payment Required\)|403 \(Forbidden\)/i.test(e));
740 check('the page threw nothing along the way', noise.length === 0,
741 JSON.stringify((s.net || []).filter((n) => n.status >= 400)).slice(0, 300));
742} finally {
743 await b.close().catch(() => {});
744 for (const p of started) { try { p.kill(); } catch (e) { /* already gone */ } }
745 if (!KEEP) fs.rmSync(BASE, { recursive: true, force: true });
746}
747
748console.log('\n' + ok.length + ' ok, ' + bad.length + ' failed');
749process.exit(bad.length ? 1 : 0);