Oregami
Repositories/oxedyne/daimond

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.
23import { open, errors } from './harness.mjs';
24
25const ok = [], bad = [];
26const check = (name, pass, detail) => {
27 (pass ? ok : bad).push(name + (detail ? ' — ' + detail : ''));
28 console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : ''));
29};
30
31const 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
41const s = await open({ name: 'permsync' });
42const p = s.page;
43
44await p.waitForFunction(
45 () => !!(window.DaimondCore && DaimondCore.collectSync && window.DaimondHandMode
46 && DaimondHandMode.snapshotPolicy),
47 null, { timeout: 12000 },
48).catch(() => {});
49
50try {
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
217console.log(`\n${ok.length} passed, ${bad.length} failed`);
218if (bad.length) { bad.forEach(b => console.log(' FAILED: ' + b)); process.exit(1); }