Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/verify_permmode.mjs

10.4 KiB, 1 run

created by r2519314175:589, 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_permmode.mjs — is the permission mode visible, changeable, and honest?
2//
3// The user's requirement, in their words: "the permission level should be visible
4// and easy to change." Three things follow, and each is asserted here.
5//
6// VISIBLE a word in the chat header, not a setting you go and look up. The
7// word carries the state; nothing rests on the dot's colour.
8// HONEST the ENGINE's copy is the one that decides anything, so a control
9// that failed must redraw what is actually in force. A page showing
10// "Bypass" over an engine running Guarded is worse than either being
11// wrong on its own.
12// QUIET bypass explains itself once and never again, and switching AWAY
13// never asks. Being asked repeatedly was the original complaint;
14// a mode switch that nags has the same defect one layer up.
15//
16// Needs dev/serve.mjs (DAIMOND_PORT, default 8777) and dev/mockllm.mjs
17// (DAIMOND_MOCK_PORT, default 9099).
18import { open, chat, errors } from './harness.mjs';
19
20const ok = [], bad = [];
21const check = (name, pass, detail) => {
22 (pass ? ok : bad).push(name + (detail ? ' — ' + detail : ''));
23 console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : ''));
24};
25
26const s = await open({ name: 'permmode' });
27const p = s.page;
28await chat(s, '@text hello');
29
30// ── Visible, and defaulting to the careful rung ───────────────────────────
31
32const start = await p.evaluate(() => ({
33 chip: (document.getElementById('hand-mode-chip-txt') || {}).textContent,
34 engine: window.__DAIMOND_MODE_PROBE ? null : null,
35 astat: (document.getElementById('astat-hand') || {}).textContent,
36 saved: localStorage.getItem('daimond-permission-mode'),
37}));
38check('the chip says which mode is in force', /guarded/i.test(start.chip || ''), start.chip);
39check('and the admin status row says it too', /guarded/i.test(start.astat || ''), start.astat);
40
41// ── The engine agrees with the page ───────────────────────────────────────
42
43const engine = await p.evaluate(() => (window.DaimondHandMode ? DaimondHandMode.get() : null));
44check('the page and the engine name the same mode', engine === 'guarded', String(engine));
45
46// ── The picker opens, and offers the ladder in order ──────────────────────
47
48await p.click('#hand-mode-chip', { force: true });
49await p.waitForTimeout(300);
50const pop = await p.evaluate(() => {
51 const el = document.getElementById('hand-mode-pop');
52 if (!el || el.hidden) return null;
53 return {
54 rows: [...el.querySelectorAll('.mode-row-name')].map(n => n.textContent),
55 radio: [...el.querySelectorAll('input[type=radio]')].map(r => ({ v: r.value, on: r.checked })),
56 note: (el.querySelector('.pop-note') || {}).textContent || '',
57 aria: document.getElementById('hand-mode-chip').getAttribute('aria-expanded'),
58 };
59});
60check('the chip opens a picker', !!pop);
61check('offering the ladder strictest first',
62 !!pop && pop.radio.map(r => r.v).join(',') === 'ask,guarded,bypass',
63 pop ? pop.radio.map(r => r.v).join(',') : '');
64check('with the one in force already chosen',
65 !!pop && pop.radio.some(r => r.v === 'guarded' && r.on));
66check('and it says what NO mode changes, so a choice is made knowing that',
67 !!pop && /fence|folders|journal/i.test(pop.note), pop ? pop.note.slice(0, 80) : '');
68check('the chip reports its own state to a screen reader', pop && pop.aria === 'true');
69
70// ── Bypass explains itself exactly once ───────────────────────────────────
71
72await p.evaluate(() => {
73 const r = [...document.querySelectorAll('#hand-mode-pop input[type=radio]')].find(x => x.value === 'bypass');
74 r.checked = true; r.dispatchEvent(new Event('change', { bubbles: true }));
75});
76await p.waitForTimeout(500);
77// The app's own dialog is `.modal.dlg` with a `.dlg-card` inside it.
78const dlg1 = await p.evaluate(() => {
79 const d = document.querySelector('.modal.dlg .dlg-card');
80 return d ? d.innerText : '';
81});
82// THE PROPERTY, in two halves, and this dialog is the only place either is
83// said. WHAT IS GIVEN UP: the asking stops, and what will now happen unasked is
84// named -- commands run on the user's machine AND pages the model chose are
85// fetched -- including on a turn that has already read text somebody else
86// wrote. That last clause is the whole of the risk: it is the one sentence that
87// distinguishes "Daimond acts without nagging me" from "anything Daimond reads
88// can send my work somewhere", and a user who never sees it cannot weigh what
89// they are agreeing to.
90//
91// WHAT IS KEPT: all five guarantees. `/fence|journal|folders/` passed on ONE of
92// the five, so four could go and the check stayed green -- for a dialog whose
93// entire job is to bound the thing it is turning off.
94//
95// The first was `/stops asking/`, red as soon as the copy said "stops the
96// asking". Every guarantee below survived that rewrite; it was checked against
97// the string one commit before it (`git show 5b0eeb9^`), clause by clause.
98const explainsBypass = (d) => ({
99 // The asking stops, and commands on the machine are what stops being asked about.
100 asking: /\b(stops? (the )?asking|without (asking|putting)|no longer asks?)\b/i.test(d)
101 && /\bcommands?\b/i.test(d) && /\b(machine|computer)\b/i.test(d),
102 // So does fetching a page the model chose for itself.
103 //
104 // `fetched` is in the alternation because it was NOT, and the copy rewrite of
105 // 2026-08-19 reddened this check by writing "pages are fetched" -- the passive of
106 // the very verb being looked for. The instrument could see three inflections of
107 // its own keyword and not the fourth, so a dialog saying exactly the right thing
108 // failed. The property is that the dialog says pages get fetched; the voice it
109 // says it in is not the property.
110 fetching: /\b(fetch(es|ing|ed)?|opens?|requests?|loads?)\b/i.test(d) && /\b(pages?|web|url)\b/i.test(d),
111 // And it holds even on a turn that has read somebody else's words.
112 injected: /\b(somebody|someone) else\b|\bwritten by (somebody|someone)\b/i.test(d)
113 && /\b(already read|has read|a turn that)\b/i.test(d),
114});
115const kept = (d) => ({
116 fence: /\bfence|sandbox\b/i.test(d),
117 folders: /\bown folders?\b|\bonly its own\b/i.test(d),
118 syscall: /\bsystem[- ]call filter\b|\bsyscall\b|\bseccomp\b/i.test(d),
119 marked: /\b(marked|labell?ed|tagged)\b/i.test(d) && /\b(somebody|someone) else|outside\b/i.test(d),
120 journal: /\bjournal\b|\baudit\b/i.test(d) && /\b(check(ed)?|review(ed)?|afterwards|after the fact)\b/i.test(d),
121});
122const turnsOff = explainsBypass(dlg1), leaves = kept(dlg1);
123const missing = (o) => Object.entries(o).filter(([, v]) => !v).map(([k]) => 'no ' + k).join(', ');
124check('choosing bypass explains what it turns off', Object.values(turnsOff).every(Boolean),
125 missing(turnsOff) || dlg1.slice(0, 90).replace(/\n/g, ' / '));
126check('and says plainly what it does NOT change — all five, not one of them',
127 Object.values(leaves).every(Boolean), missing(leaves));
128check('and promises not to ask again',
129 /\b(not|never) be asked (this )?again\b|\bonly ask(s|ed)? (this )?once\b/i.test(dlg1));
130
131await p.evaluate(() => {
132 const b = [...document.querySelectorAll('button')].find(x => /use bypass/i.test(x.textContent));
133 if (b) b.click();
134});
135await p.waitForTimeout(600);
136const afterBypass = await p.evaluate(() => ({
137 chip: (document.getElementById('hand-mode-chip-txt') || {}).textContent,
138 mode: DaimondHandMode.get(),
139 ack: localStorage.getItem('daimond-permission-bypass-ack'),
140}));
141check('the chip now says Bypass', /bypass/i.test(afterBypass.chip || ''), afterBypass.chip);
142check('and the engine is in it', afterBypass.mode === 'bypass', afterBypass.mode);
143// The ENGINE's own answer, not the page's copy of it — the only one that decides
144// anything. A chip reading Bypass over an engine still in guarded is the failure
145// this asks about, and the page cannot detect it by consulting itself.
146const engineSays = await p.evaluate(() => DaimondCore.permissionMode());
147check('and the engine itself says so when asked', engineSays === 'bypass', engineSays || '(no answer)');
148check('the acknowledgement is recorded', afterBypass.ack === '1');
149
150// Switching away must never ask, and coming back must not ask again.
151await p.evaluate(() => DaimondHandMode.set('guarded'));
152await p.waitForTimeout(400);
153const away = await p.evaluate(() => ({
154 mode: DaimondHandMode.get(),
155 dlg: !!document.querySelector('.modal.dlg'),
156}));
157check('switching AWAY from bypass asks nothing', away.mode === 'guarded' && !away.dlg);
158
159await p.evaluate(() => DaimondHandMode.set('bypass'));
160await p.waitForTimeout(400);
161const again = await p.evaluate(() => ({
162 mode: DaimondHandMode.get(),
163 dlg: !!document.querySelector('.modal.dlg'),
164}));
165check('and going back does not explain itself a second time',
166 again.mode === 'bypass' && !again.dlg, again.mode + (again.dlg ? ' (asked again)' : ''));
167
168// ── Fail closed: a setter that will not take must not be drawn as if it did ──
169
170await p.evaluate(() => DaimondHandMode.set('guarded'));
171await p.waitForTimeout(300);
172await p.evaluate(() => {
173 // Break the seam between the page and the engine, leaving the engine where it is.
174 window.__realInit = null;
175 const hm = window.DaimondHandMode;
176 hm.init({
177 apply: function () { throw new Error('the engine refused'); },
178 confirm: async () => true,
179 notice: (m) => { window.__notice = m; },
180 });
181});
182await p.waitForTimeout(300);
183await p.evaluate(() => DaimondHandMode.set('bypass'));
184await p.waitForTimeout(400);
185const failClosed = await p.evaluate(() => ({
186 chip: (document.getElementById('hand-mode-chip-txt') || {}).textContent,
187 mode: DaimondHandMode.get(),
188 notice: window.__notice || '',
189}));
190check('a setter that throws does NOT leave "Bypass" drawn over a guarded engine',
191 !/bypass/i.test(failClosed.chip || '') && failClosed.mode !== 'bypass',
192 `chip "${failClosed.chip}", mode ${failClosed.mode}`);
193check('and the user is told that nothing changed', /could not be set|nothing changed/i.test(failClosed.notice),
194 failClosed.notice || '(nothing said)');
195
196const errs = errors(s).filter(e => !/502|Bad Gateway|the engine refused/.test(e));
197check('nothing threw', errs.length === 0, errs.slice(0, 2).join(' | '));
198
199await s.close();
200console.log(`\n${ok.length} passed, ${bad.length} failed`);
201if (bad.length) { bad.forEach(b => console.log(' FAILED: ' + b)); process.exit(1); }