oxedyne/daimond/dev/verify_permsync.mjs
11.2 KiB, 1 run
created by r2519314175:2471, 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_permsync.mjs — the account-level PERMISSION POLICY travels between |
| 2 | // devices; machine-local trust does NOT. |
| 3 | // |
| 4 | // THE BUG THIS GUARDS. A turn dispatched from one device (a phone) is finished on |
| 5 | // a runner (argonaut). After a version update the runner meets a tool that has |
| 6 | // become gated -- web_search -- and, because consent was device-local, it puts |
| 7 | // the prompt on ITS OWN screen, where nobody is. The turn stalls invisibly. The |
| 8 | // fix is to carry the account's standing policy in the sync parcel, so a runner |
| 9 | // (or a freshly updated device) inherits it and does not re-ask. |
| 10 | // |
| 11 | // The split under test, exactly: |
| 12 | // TRAVELS the permission RUNG (ask/guarded/bypass) and the standing |
| 13 | // grants for gateway-brokered scopes (reading the web). Freshest |
| 14 | // -wins per fact, on a strict `at` stamp. |
| 15 | // STAYS LOCAL the bypass acknowledgement (a per-device safety tick), whether |
| 16 | // a COMMAND keeps its network on THIS machine, and this machine's |
| 17 | // autonomous / step-away postures. None of it is in the parcel. |
| 18 | // |
| 19 | // Drives the real client: DaimondHandMode's snapshotPolicy/adoptPolicy, and the |
| 20 | // real collectSync/applySync wiring the parcel rides. No gateway needed -- the |
| 21 | // policy is localStorage and the engine push is local. Needs dev/serve.mjs |
| 22 | // (DAIMOND_PORT) and dev/mockllm.mjs (DAIMOND_MOCK_PORT), like verify_permmode. |
| 23 | import { open, errors } from './harness.mjs'; |
| 24 | |
| 25 | const ok = [], bad = []; |
| 26 | const check = (name, pass, detail) => { |
| 27 | (pass ? ok : bad).push(name + (detail ? ' — ' + detail : '')); |
| 28 | console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : '')); |
| 29 | }; |
| 30 | |
| 31 | const LS = { |
| 32 | MODE: 'daimond-permission-mode', |
| 33 | MODE_AT: 'daimond-permission-mode-at', |
| 34 | SCOPES: 'daimond-permission-scopes', |
| 35 | ACK: 'daimond-permission-bypass-ack', |
| 36 | NET: 'daimond-net-standing', |
| 37 | AUTO: 'daimond-autonomous-posture', |
| 38 | HANDOFF: 'daimond-handoff-when-away', |
| 39 | }; |
| 40 | |
| 41 | const s = await open({ name: 'permsync' }); |
| 42 | const p = s.page; |
| 43 | |
| 44 | await p.waitForFunction( |
| 45 | () => !!(window.DaimondCore && DaimondCore.collectSync && window.DaimondHandMode |
| 46 | && DaimondHandMode.snapshotPolicy), |
| 47 | null, { timeout: 12000 }, |
| 48 | ).catch(() => {}); |
| 49 | |
| 50 | try { |
| 51 | // ── The scope grant travels, and a fresh device inherits it ────────────── |
| 52 | // |
| 53 | // Device A grants the reading scope for the account. The parcel must carry it, |
| 54 | // and a device that holds no grant of its own must see it granted after a |
| 55 | // pull -- which is what stops a runner re-prompting for web_search. |
| 56 | const grant = await p.evaluate(() => { |
| 57 | DaimondHandMode.grantScope('reading', true); |
| 58 | return { |
| 59 | granted: DaimondHandMode.scopeGranted('reading'), |
| 60 | snap: DaimondHandMode.snapshotPolicy(), |
| 61 | }; |
| 62 | }); |
| 63 | check('device A can grant the reading scope on the account', |
| 64 | grant.granted === true, JSON.stringify(grant.snap && grant.snap.scopes)); |
| 65 | check('the policy snapshot carries the reading grant, stamped', |
| 66 | !!(grant.snap && grant.snap.scopes && grant.snap.scopes.reading |
| 67 | && grant.snap.scopes.reading.on === 1 && grant.snap.scopes.reading.at > 0), |
| 68 | JSON.stringify(grant.snap && grant.snap.scopes)); |
| 69 | |
| 70 | // The REAL parcel carries it too (collectSync wiring, the collision surface). |
| 71 | const inParcel = await p.evaluate(async () => { |
| 72 | const parcel = await DaimondCore.collectSync(); |
| 73 | return { |
| 74 | hasPerms: !!parcel.perms, |
| 75 | readingOn: !!(parcel.perms && parcel.perms.scopes && parcel.perms.scopes.reading |
| 76 | && parcel.perms.scopes.reading.on === 1), |
| 77 | perms: parcel.perms, |
| 78 | }; |
| 79 | }); |
| 80 | check('collectSync puts the policy in the parcel under `perms`', |
| 81 | inParcel.hasPerms === true, JSON.stringify(inParcel.perms)); |
| 82 | check('and the reading grant is present in the real parcel', |
| 83 | inParcel.readingOn === true, JSON.stringify(inParcel.perms && inParcel.perms.scopes)); |
| 84 | |
| 85 | // Device B: no grant of its own, then it applies A's parcel through the REAL |
| 86 | // applySync. It must end up granted -- and must NOT have been granted before, |
| 87 | // or the check proves nothing. |
| 88 | const bReading = await p.evaluate(async (LS) => { |
| 89 | const aPerms = (await DaimondCore.collectSync()).perms; // A's policy, as it rides |
| 90 | localStorage.removeItem(LS.SCOPES); // B holds no scope grant |
| 91 | const before = DaimondHandMode.scopeGranted('reading'); |
| 92 | await DaimondCore.applySync({ perms: aPerms }); // the real apply path |
| 93 | const after = DaimondHandMode.scopeGranted('reading'); |
| 94 | return { before, after }; |
| 95 | }, LS); |
| 96 | check('a device with no grant of its own does NOT read as granted (the test is not trivial)', |
| 97 | bReading.before === false); |
| 98 | check('and after applying the parcel it sees the reading scope granted — no re-prompt', |
| 99 | bReading.after === true, `before=${bReading.before} after=${bReading.after}`); |
| 100 | |
| 101 | // ── The rung travels, freshest-wins, strict ───────────────────────────── |
| 102 | const rung = await p.evaluate(async (LS) => { |
| 103 | // A known starting point on THIS device. |
| 104 | await DaimondHandMode.set('guarded'); |
| 105 | const startMode = DaimondHandMode.get(); |
| 106 | const startAt = Number(localStorage.getItem(LS.MODE_AT) || 0); |
| 107 | // A remote policy from another device, strictly LATER, naming a different rung. |
| 108 | const newer = { v: 1, mode: 'ask', mode_at: startAt + 100000, scopes: {} }; |
| 109 | DaimondHandMode.adoptPolicy(newer); |
| 110 | const adopted = { mode: DaimondHandMode.get(), saved: localStorage.getItem(LS.MODE) }; |
| 111 | // A remote policy that is OLDER than what we now hold must not win. |
| 112 | const older = { v: 1, mode: 'guarded', mode_at: startAt + 50000, scopes: {} }; |
| 113 | DaimondHandMode.adoptPolicy(older); |
| 114 | const kept = DaimondHandMode.get(); |
| 115 | return { startMode, adopted, kept }; |
| 116 | }, LS); |
| 117 | check('the rung starts where this device set it', rung.startMode === 'guarded', rung.startMode); |
| 118 | check('a strictly-later rung from another device is adopted (page and store both)', |
| 119 | rung.adopted.mode === 'ask' && rung.adopted.saved === 'ask', |
| 120 | `mode=${rung.adopted.mode} saved=${rung.adopted.saved}`); |
| 121 | check('an older rung from another device does NOT win (freshest-wins is strict)', |
| 122 | rung.kept === 'ask', rung.kept); |
| 123 | |
| 124 | // ── Machine-local trust does NOT travel ───────────────────────────────── |
| 125 | // |
| 126 | // The bypass acknowledgement, the command-network standing answer, and the two |
| 127 | // device postures are set on A, and none of them may appear in the parcel or be |
| 128 | // created on B by adopting a policy. |
| 129 | const local = await p.evaluate(async (LS) => { |
| 130 | localStorage.setItem(LS.ACK, '1'); |
| 131 | localStorage.setItem(LS.NET, 'allow'); |
| 132 | localStorage.setItem(LS.AUTO, '1'); |
| 133 | localStorage.setItem(LS.HANDOFF, '1'); |
| 134 | const parcel = await DaimondCore.collectSync(); |
| 135 | const perms = parcel.perms || {}; |
| 136 | const permsKeys = Object.keys(perms).sort(); |
| 137 | const wholeJson = JSON.stringify(parcel); |
| 138 | // The perms section carries ONLY the account facts: version, rung, its |
| 139 | // stamp, and the scope map. Nothing machine-local. |
| 140 | const permsShapeOk = permsKeys.join(',') === 'mode,mode_at,scopes,v'; |
| 141 | const scopeKeys = Object.keys(perms.scopes || {}).sort(); |
| 142 | // A device that adopts a policy must not thereby acquire a bypass ack. |
| 143 | localStorage.removeItem(LS.ACK); |
| 144 | const aPerms = parcel.perms; |
| 145 | await DaimondCore.applySync({ perms: aPerms }); |
| 146 | const ackAfterAdopt = localStorage.getItem(LS.ACK); |
| 147 | return { |
| 148 | permsKeys, permsShapeOk, scopeKeys, |
| 149 | // The machine-local VALUES must not be findable anywhere in the parcel. |
| 150 | // ('allow' is common, so the ack/posture markers are what we hunt.) |
| 151 | netInParcel: wholeJson.includes('net-standing') || wholeJson.includes('daimond-net'), |
| 152 | ackKey: wholeJson.includes('bypass-ack'), |
| 153 | autoKey: wholeJson.includes('autonomous-posture'), |
| 154 | handoffKey: wholeJson.includes('handoff-when-away'), |
| 155 | ackAfterAdopt, |
| 156 | }; |
| 157 | }, LS); |
| 158 | check('the `perms` section carries only account facts (v, mode, mode_at, scopes)', |
| 159 | local.permsShapeOk, local.permsKeys.join(',')); |
| 160 | check('and its scope map holds only account-brokered scopes (reading)', |
| 161 | local.scopeKeys.every(k => k === 'reading'), local.scopeKeys.join(',')); |
| 162 | check('the bypass acknowledgement is NOT in the parcel', !local.ackKey); |
| 163 | check('the command-network standing answer is NOT in the parcel', !local.netInParcel); |
| 164 | check('the autonomous posture is NOT in the parcel', !local.autoKey); |
| 165 | check('the step-away posture is NOT in the parcel', !local.handoffKey); |
| 166 | check('adopting a policy does NOT grant this device the bypass acknowledgement', |
| 167 | local.ackAfterAdopt === null, `ack=${local.ackAfterAdopt}`); |
| 168 | |
| 169 | // ── A revocation travels too, and an empty policy grants nothing ───────── |
| 170 | const revoke = await p.evaluate(async (LS) => { |
| 171 | // Grant on A, then revoke; the revoke carries a strictly later stamp. |
| 172 | DaimondHandMode.grantScope('reading', true); |
| 173 | const granted = DaimondHandMode.scopeGranted('reading'); |
| 174 | DaimondHandMode.grantScope('reading', false); |
| 175 | const revoked = DaimondHandMode.scopeGranted('reading'); |
| 176 | const snap = DaimondHandMode.snapshotPolicy(); // reading { on: 0, at: Tr } |
| 177 | const Tr = snap.scopes.reading.at; |
| 178 | // Simulate device B holding an OLDER grant, then adopting A's fresher revoke. |
| 179 | localStorage.setItem(LS.SCOPES, JSON.stringify({ reading: { at: Tr - 1000, on: 1 } })); |
| 180 | const bHeld = DaimondHandMode.scopeGranted('reading'); // true, older than Tr |
| 181 | DaimondHandMode.adoptPolicy(snap); |
| 182 | const afterAdopt = DaimondHandMode.scopeGranted('reading'); |
| 183 | // An empty policy is a no-op: it resurrects nothing. |
| 184 | DaimondHandMode.adoptPolicy({ v: 1, mode: 'guarded', mode_at: 0, scopes: {} }); |
| 185 | const afterEmpty = DaimondHandMode.scopeGranted('reading'); |
| 186 | return { granted, revoked, bHeld, afterAdopt, afterEmpty }; |
| 187 | }, LS); |
| 188 | check('a grant can be revoked on the account', revoke.granted === true && revoke.revoked === false, |
| 189 | `granted=${revoke.granted} revoked=${revoke.revoked}`); |
| 190 | check('the revocation travels: a device holding an older grant drops it on adopt', |
| 191 | revoke.bHeld === true && revoke.afterAdopt === false, |
| 192 | `bHeld=${revoke.bHeld} after=${revoke.afterAdopt}`); |
| 193 | check('an empty policy grants nothing (no resurrection)', revoke.afterEmpty === false); |
| 194 | |
| 195 | // ── A parcel that predates the policy applies as a no-op ───────────────── |
| 196 | const legacy = await p.evaluate(async () => { |
| 197 | DaimondHandMode.grantScope('reading', true); |
| 198 | const held = DaimondHandMode.scopeGranted('reading'); |
| 199 | let threw = ''; |
| 200 | try { |
| 201 | const parcel = await DaimondCore.collectSync(); |
| 202 | delete parcel.perms; // a device from before `perms` |
| 203 | await DaimondCore.applySync(parcel); |
| 204 | } catch (e) { threw = String(e && e.message || e); } |
| 205 | return { held, still: DaimondHandMode.scopeGranted('reading'), threw }; |
| 206 | }); |
| 207 | check('a v-old parcel with no `perms` applies without error', |
| 208 | legacy.threw === '' || !/perms/i.test(legacy.threw), legacy.threw || '(clean)'); |
| 209 | check('and leaves this device’s policy exactly where it was', legacy.still === legacy.held); |
| 210 | |
| 211 | const errs = errors(s).filter(e => !/502|Bad Gateway|the engine refused/.test(e)); |
| 212 | check('nothing threw', errs.length === 0, errs.slice(0, 2).join(' | ')); |
| 213 | } finally { |
| 214 | await s.close(); |
| 215 | } |
| 216 | |
| 217 | console.log(`\n${ok.length} passed, ${bad.length} failed`); |
| 218 | if (bad.length) { bad.forEach(b => console.log(' FAILED: ' + b)); process.exit(1); } |