oxedyne/daimond/dev/verify_diamondcwd.mjs
8.8 KiB, 1 run
created by r2519314175:359, 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_diamondcwd.mjs — where a Diamond's command starts, now that the |
| 2 | // Diamond itself is not on the machine. |
| 3 | // |
| 4 | // A Diamond lives in the browser's storage under `diamonds/<id>` whatever |
| 5 | // workspace root is open (`is_store_path`, src/tools.rs). `default_cwd` used to |
| 6 | // hand `Tool::Run` that very path, and `fence_spec` turned it into |
| 7 | // `<granted-root>/diamonds/<id>` — a directory on the user's disk that Daimond |
| 8 | // no longer writes and that in general does not exist. The hand refuses a `cwd` |
| 9 | // it cannot canonicalise, so every command in a Diamond with no explicit `cwd` |
| 10 | // would have been refused, for ever, with "cannot be resolved to a directory on |
| 11 | // this machine": true, unhelpful, and pointing at a path the user never chose. |
| 12 | // |
| 13 | // What is pinned: |
| 14 | // * a Diamond's command starts in its first ATTACHED folder, which is a real |
| 15 | // place on the machine and inside the fence; |
| 16 | // * a read-only attachment is still somewhere to start; |
| 17 | // * a Diamond with nothing attached is refused in words that name the thing to |
| 18 | // do about it, and the hand is never asked; |
| 19 | // * an explicit `cwd` from the model is still honoured; |
| 20 | // * an ordinary, unscoped turn still starts at the granted root. |
| 21 | // |
| 22 | // The hand is a stand-in that records what it was sent — the question here is |
| 23 | // which `cwd` the ENGINE composes, and a real hand would answer it by refusing |
| 24 | // a directory that does not exist on this machine, which is a different fact. |
| 25 | // dev/verify_scope.mjs drives the real binary for what the fence contains. |
| 26 | // |
| 27 | // Run with dev/serve.mjs (DAIMOND_PORT, default 8777) up. |
| 28 | // |
| 29 | // node dev/verify_diamondcwd.mjs |
| 30 | import { open } from './harness.mjs'; |
| 31 | |
| 32 | const ok = [], bad = []; |
| 33 | const check = (name, pass, detail) => { |
| 34 | (pass ? ok : bad).push(name); |
| 35 | console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : '')); |
| 36 | }; |
| 37 | |
| 38 | const ROOT = '/home/somebody/project'; |
| 39 | |
| 40 | const s = await open({ name: 'diamondcwd', connect: false }); |
| 41 | const p = s.page; |
| 42 | await p.waitForTimeout(1500); |
| 43 | |
| 44 | await p.evaluate(async (root) => { |
| 45 | const mod = await import('../pkg/oxedyne_daimond.js'); |
| 46 | window.__d = { mod, root }; |
| 47 | // A stand-in hand: it says it can fence, and it writes down every spec. |
| 48 | window.__sent = []; |
| 49 | window.DaimondHand = { |
| 50 | status: () => Promise.resolve(JSON.stringify({ |
| 51 | paired: true, transport: 'machine', machine: 'test', os: 'linux', |
| 52 | root: root, caps: ['fence:linux', 'landlock:abi-8'], |
| 53 | })), |
| 54 | run: (spec) => { |
| 55 | window.__sent.push(JSON.parse(spec)); |
| 56 | return Promise.resolve(JSON.stringify({ |
| 57 | t: 'exec_result', exit: 0, stdout: 'ran', stderr: '', |
| 58 | out_bytes: 3, err_bytes: 0, |
| 59 | })); |
| 60 | }, |
| 61 | }; |
| 62 | window.__app = (scope) => { |
| 63 | const app = new mod.DaimondApp( |
| 64 | 'http://127.0.0.1/v1/chat/completions', '', 'none', 256, '', true); |
| 65 | if (scope) { |
| 66 | app.set_diamond_scope(scope.own, JSON.stringify(scope.attached || []), |
| 67 | JSON.stringify(scope.read_only || []), JSON.stringify(scope.toolkits || [])); |
| 68 | } |
| 69 | return app; |
| 70 | }; |
| 71 | // One `run`, and what the hand was sent for it. |
| 72 | window.__run = async (scope, args) => { |
| 73 | window.__sent = []; |
| 74 | const app = __app(scope); |
| 75 | const said = await app.run_tool('run', JSON.stringify(args)); |
| 76 | return { said: said, sent: window.__sent }; |
| 77 | }; |
| 78 | }, ROOT); |
| 79 | |
| 80 | const run = (scope, args) => p.evaluate(([sc, a]) => __run(sc, a), [scope, args]); |
| 81 | |
| 82 | // ── A Diamond with a folder attached ──────────────────────────────────── |
| 83 | |
| 84 | const attached = await run( |
| 85 | { own: 'diamonds/d1', attached: ['src/api'] }, { argv: ['ls'] }); |
| 86 | check('a Diamond with a folder attached runs its command there', |
| 87 | attached.sent.length === 1 && attached.sent[0].cwd === ROOT + '/src/api', |
| 88 | JSON.stringify(attached.sent.map((x) => x.cwd)) + ' | ' + attached.said.slice(0, 80)); |
| 89 | check('and not in its own directory, which is not on this machine at all', |
| 90 | !JSON.stringify(attached.sent).includes('diamonds/d1"') || attached.sent[0].cwd !== ROOT + '/diamonds/d1', |
| 91 | attached.sent.length ? attached.sent[0].cwd : '(nothing was sent)'); |
| 92 | |
| 93 | const readOnly = await run( |
| 94 | { own: 'diamonds/d2', attached: [], read_only: ['refs'] }, { argv: ['ls'] }); |
| 95 | check('a read-only attachment is still somewhere to start', |
| 96 | readOnly.sent.length === 1 && readOnly.sent[0].cwd === ROOT + '/refs', |
| 97 | JSON.stringify(readOnly.sent.map((x) => x.cwd)) + ' | ' + readOnly.said.slice(0, 80)); |
| 98 | |
| 99 | // ── A Diamond with nothing attached ───────────────────────────────────── |
| 100 | |
| 101 | const bare = await run({ own: 'diamonds/d3', attached: [] }, { argv: ['ls'] }); |
| 102 | check('a Diamond with nothing attached is refused', |
| 103 | /^Refused/.test(bare.said), bare.said.slice(0, 120)); |
| 104 | check('and told what to do about it, in the user\'s own vocabulary', |
| 105 | /attach/i.test(bare.said) && /Workspace panel/i.test(bare.said) |
| 106 | && /cwd/.test(bare.said), |
| 107 | bare.said.slice(0, 200)); |
| 108 | check('and the hand was never asked, so nothing ran', |
| 109 | bare.sent.length === 0, JSON.stringify(bare.sent).slice(0, 120)); |
| 110 | |
| 111 | // ── What the model asked for still wins ───────────────────────────────── |
| 112 | |
| 113 | const named = await run( |
| 114 | { own: 'diamonds/d4', attached: ['src/api'] }, { argv: ['ls'], cwd: 'src/api/inner' }); |
| 115 | check('a cwd the model named is still honoured', |
| 116 | named.sent.length === 1 && named.sent[0].cwd === ROOT + '/src/api/inner', |
| 117 | JSON.stringify(named.sent.map((x) => x.cwd))); |
| 118 | |
| 119 | // ── "." is not a place, it is "here" ──────────────────────────────────── |
| 120 | // |
| 121 | // A daimon writes `cwd: "."` because it is the most ordinary working directory |
| 122 | // there is. Taken literally it normalises to the workspace ROOT, which a scoped |
| 123 | // turn may not run in, so it met "'.' is not in this chat's workspace" — correct |
| 124 | // and useless, and one call spent every time. Three live self-development runs on |
| 125 | // 2026-08-24 were lost to it. It is now the same request as no `cwd` at all. |
| 126 | const dot = await run( |
| 127 | { own: 'diamonds/d6', attached: ['src/api'] }, { argv: ['ls'], cwd: '.' }); |
| 128 | check('a command asked for in "." runs in the folder the user marked', |
| 129 | dot.sent.length === 1 && dot.sent[0].cwd === ROOT + '/src/api', |
| 130 | JSON.stringify(dot.sent.map((x) => x.cwd)) + ' | ' + dot.said.slice(0, 100)); |
| 131 | |
| 132 | // The empty spellings of the same thing, so the answer cannot be one branch that |
| 133 | // happens to catch a full stop. |
| 134 | const dotSlash = await run( |
| 135 | { own: 'diamonds/d7', attached: ['src/api'] }, { argv: ['ls'], cwd: './' }); |
| 136 | check('and so does one asked for in "./"', |
| 137 | dotSlash.sent.length === 1 && dotSlash.sent[0].cwd === ROOT + '/src/api', |
| 138 | JSON.stringify(dotSlash.sent.map((x) => x.cwd))); |
| 139 | |
| 140 | // A Diamond with nothing on the machine gets the sentence that says what to do, |
| 141 | // because "." now takes the same branch an absent cwd takes. It used to get a |
| 142 | // sentence about ".", which names nothing anybody can act on. |
| 143 | const dotBare = await run({ own: 'diamonds/d8', attached: [] }, { argv: ['ls'], cwd: '.' }); |
| 144 | check('a Diamond with nothing attached is refused in the words that name the fix', |
| 145 | /^Refused/.test(dotBare.said) && /attach/i.test(dotBare.said) |
| 146 | && /Workspace panel/i.test(dotBare.said) && dotBare.sent.length === 0, |
| 147 | dotBare.said.slice(0, 160)); |
| 148 | |
| 149 | // NOTHING IS WIDENED. The two refusals either side of "." are untouched: a named |
| 150 | // place outside the Diamond, and an absolute path. |
| 151 | const dotAbs = await run( |
| 152 | { own: 'diamonds/d9', attached: ['src/api'] }, { argv: ['ls'], cwd: '/etc' }); |
| 153 | check('an absolute cwd is still refused, and told which convention it met', |
| 154 | /^Refused/.test(dotAbs.said) && /is absolute/.test(dotAbs.said) |
| 155 | && /relative to the workspace/.test(dotAbs.said) && dotAbs.sent.length === 0, |
| 156 | dotAbs.said.slice(0, 140)); |
| 157 | |
| 158 | // A Diamond may not name a cwd outside its own workspace, which is a different |
| 159 | // refusal and must not be swallowed by the new one. |
| 160 | const outside = await run( |
| 161 | { own: 'diamonds/d5', attached: ['src/api'] }, { argv: ['ls'], cwd: 'elsewhere' }); |
| 162 | check('and one outside the Diamond is still refused as out of scope', |
| 163 | /^Refused/.test(outside.said) && /not in this Diamond's workspace/.test(outside.said) |
| 164 | && outside.sent.length === 0, |
| 165 | outside.said.slice(0, 120)); |
| 166 | |
| 167 | // ── The ordinary turn is untouched ────────────────────────────────────── |
| 168 | |
| 169 | const plain = await run(null, { argv: ['ls'] }); |
| 170 | check('an unscoped turn still starts at the granted root', |
| 171 | plain.sent.length === 1 && plain.sent[0].cwd === ROOT, |
| 172 | JSON.stringify(plain.sent.map((x) => x.cwd)) + ' | ' + plain.said.slice(0, 80)); |
| 173 | |
| 174 | const noise = s.errs.filter((e) => |
| 175 | !/favicon|ERR_ABORTED|net::ERR|Failed to load resource/i.test(e)); |
| 176 | check('the page threw nothing along the way', noise.length === 0, noise.slice(0, 3).join(' | ')); |
| 177 | |
| 178 | await s.close(); |
| 179 | console.log('\n' + ok.length + ' ok, ' + bad.length + ' failed'); |
| 180 | process.exit(bad.length ? 1 : 0); |