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. |
| 79 | import fs from 'node:fs'; |
| 80 | import net from 'node:net'; |
| 81 | import path from 'node:path'; |
| 82 | import { spawn, spawnSync } from 'node:child_process'; |
| 83 | import { fileURLToPath } from 'node:url'; |
| 84 | |
| 85 | import { open as openApp, scratch, MOCK } from './harness.mjs'; |
| 86 | import { whyStaleBinary, whyStaleWasm, refuse } from './staleguard.mjs'; |
| 87 | |
| 88 | const HERE = path.dirname(fileURLToPath(import.meta.url)); |
| 89 | const ROOT = path.join(HERE, '..'); |
| 90 | const SRC = path.join(ROOT, 'ext'); |
| 91 | const INSTALL = path.join(ROOT, 'hand/install/install.sh'); |
| 92 | const HAND = path.join(ROOT, 'hand/target/release/daimond-hand'); |
| 93 | |
| 94 | const KEEP = process.argv.slice(2).includes('--keep'); |
| 95 | /// Which directory under `www/` the page imports the engine from. |
| 96 | const ENGINE = (() => { |
| 97 | const i = process.argv.indexOf('--engine'); |
| 98 | return i >= 0 && process.argv[i + 1] ? process.argv[i + 1] : 'pkg'; |
| 99 | })(); |
| 100 | |
| 101 | const BASE = scratch('scope'); |
| 102 | const PROFILE = path.join(BASE, 'profile'); |
| 103 | const JOURNAL = path.join(BASE, 'journal'); |
| 104 | const GRANT = path.join(BASE, 'work'); |
| 105 | const HOSTS = path.join(PROFILE, 'NativeMessagingHosts'); |
| 106 | |
| 107 | const ok = [], bad = []; |
| 108 | const check = (name, pass, detail) => { |
| 109 | (pass ? ok : bad).push(name); |
| 110 | console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : '')); |
| 111 | }; |
| 112 | const note = (s) => console.log(' · ' + s); |
| 113 | const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); |
| 114 | const nonce = (tag) => `${tag}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 12)}`; |
| 115 | |
| 116 | function 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 | } |
| 123 | const started = []; |
| 124 | async 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 | |
| 137 | fs.rmSync(BASE, { recursive: true, force: true }); |
| 138 | for (const d of [PROFILE, JOURNAL, GRANT, HOSTS]) fs.mkdirSync(d, { recursive: true }); |
| 139 | fs.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. |
| 144 | const buildEnv = { ...process.env }; |
| 145 | delete 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. |
| 151 | const 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 }); |
| 154 | check('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(' | ')); |
| 156 | if (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. |
| 161 | refuse(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. |
| 176 | refuse(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 | })); |
| 180 | if (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 | |
| 186 | fs.writeFileSync(path.join(JOURNAL, 'root.txt'), |
| 187 | `# The one folder Daimond's machine hand may work in.\n${GRANT}\n`); |
| 188 | const inst = spawnSync('bash', [INSTALL, '--dir', HOSTS, HAND], { cwd: ROOT, encoding: 'utf8' }); |
| 189 | check('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 | |
| 192 | process.env.DAIMOND_HAND_JOURNAL_DIR = JOURNAL; |
| 193 | delete 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. |
| 197 | const APP_PORT = Number(process.env.DAIMOND_PORT || 8777); |
| 198 | const MOCK_PORT = Number(process.env.DAIMOND_MOCK_PORT || 9099); |
| 199 | await serve('dev server', ['dev/serve.mjs'], APP_PORT); |
| 200 | await serve('mock provider', ['dev/mockllm.mjs'], MOCK_PORT); |
| 201 | |
| 202 | // ── The browser ───────────────────────────────────────────────────── |
| 203 | |
| 204 | const s = await openApp({ headed: true, name: 'scope', extension: SRC, profile: PROFILE }); |
| 205 | const b = s.browser; |
| 206 | const page = s.page; |
| 207 | |
| 208 | async 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 | |
| 224 | const MINE = nonce('mine'); |
| 225 | const THEIRS = nonce('theirs'); |
| 226 | |
| 227 | try { |
| 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 | |
| 748 | console.log('\n' + ok.length + ' ok, ' + bad.length + ' failed'); |
| 749 | process.exit(bad.length ? 1 : 0); |