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). |
| 18 | import { open, chat, errors } from './harness.mjs'; |
| 19 | |
| 20 | const ok = [], bad = []; |
| 21 | const check = (name, pass, detail) => { |
| 22 | (pass ? ok : bad).push(name + (detail ? ' — ' + detail : '')); |
| 23 | console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : '')); |
| 24 | }; |
| 25 | |
| 26 | const s = await open({ name: 'permmode' }); |
| 27 | const p = s.page; |
| 28 | await chat(s, '@text hello'); |
| 29 | |
| 30 | // ── Visible, and defaulting to the careful rung ─────────────────────────── |
| 31 | |
| 32 | const 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 | })); |
| 38 | check('the chip says which mode is in force', /guarded/i.test(start.chip || ''), start.chip); |
| 39 | check('and the admin status row says it too', /guarded/i.test(start.astat || ''), start.astat); |
| 40 | |
| 41 | // ── The engine agrees with the page ─────────────────────────────────────── |
| 42 | |
| 43 | const engine = await p.evaluate(() => (window.DaimondHandMode ? DaimondHandMode.get() : null)); |
| 44 | check('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 | |
| 48 | await p.click('#hand-mode-chip', { force: true }); |
| 49 | await p.waitForTimeout(300); |
| 50 | const 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 | }); |
| 60 | check('the chip opens a picker', !!pop); |
| 61 | check('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(',') : ''); |
| 64 | check('with the one in force already chosen', |
| 65 | !!pop && pop.radio.some(r => r.v === 'guarded' && r.on)); |
| 66 | check('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) : ''); |
| 68 | check('the chip reports its own state to a screen reader', pop && pop.aria === 'true'); |
| 69 | |
| 70 | // ── Bypass explains itself exactly once ─────────────────────────────────── |
| 71 | |
| 72 | await 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 | }); |
| 76 | await p.waitForTimeout(500); |
| 77 | // The app's own dialog is `.modal.dlg` with a `.dlg-card` inside it. |
| 78 | const 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. |
| 98 | const 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 | }); |
| 115 | const 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 | }); |
| 122 | const turnsOff = explainsBypass(dlg1), leaves = kept(dlg1); |
| 123 | const missing = (o) => Object.entries(o).filter(([, v]) => !v).map(([k]) => 'no ' + k).join(', '); |
| 124 | check('choosing bypass explains what it turns off', Object.values(turnsOff).every(Boolean), |
| 125 | missing(turnsOff) || dlg1.slice(0, 90).replace(/\n/g, ' / ')); |
| 126 | check('and says plainly what it does NOT change — all five, not one of them', |
| 127 | Object.values(leaves).every(Boolean), missing(leaves)); |
| 128 | check('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 | |
| 131 | await p.evaluate(() => { |
| 132 | const b = [...document.querySelectorAll('button')].find(x => /use bypass/i.test(x.textContent)); |
| 133 | if (b) b.click(); |
| 134 | }); |
| 135 | await p.waitForTimeout(600); |
| 136 | const 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 | })); |
| 141 | check('the chip now says Bypass', /bypass/i.test(afterBypass.chip || ''), afterBypass.chip); |
| 142 | check('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. |
| 146 | const engineSays = await p.evaluate(() => DaimondCore.permissionMode()); |
| 147 | check('and the engine itself says so when asked', engineSays === 'bypass', engineSays || '(no answer)'); |
| 148 | check('the acknowledgement is recorded', afterBypass.ack === '1'); |
| 149 | |
| 150 | // Switching away must never ask, and coming back must not ask again. |
| 151 | await p.evaluate(() => DaimondHandMode.set('guarded')); |
| 152 | await p.waitForTimeout(400); |
| 153 | const away = await p.evaluate(() => ({ |
| 154 | mode: DaimondHandMode.get(), |
| 155 | dlg: !!document.querySelector('.modal.dlg'), |
| 156 | })); |
| 157 | check('switching AWAY from bypass asks nothing', away.mode === 'guarded' && !away.dlg); |
| 158 | |
| 159 | await p.evaluate(() => DaimondHandMode.set('bypass')); |
| 160 | await p.waitForTimeout(400); |
| 161 | const again = await p.evaluate(() => ({ |
| 162 | mode: DaimondHandMode.get(), |
| 163 | dlg: !!document.querySelector('.modal.dlg'), |
| 164 | })); |
| 165 | check('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 | |
| 170 | await p.evaluate(() => DaimondHandMode.set('guarded')); |
| 171 | await p.waitForTimeout(300); |
| 172 | await 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 | }); |
| 182 | await p.waitForTimeout(300); |
| 183 | await p.evaluate(() => DaimondHandMode.set('bypass')); |
| 184 | await p.waitForTimeout(400); |
| 185 | const failClosed = await p.evaluate(() => ({ |
| 186 | chip: (document.getElementById('hand-mode-chip-txt') || {}).textContent, |
| 187 | mode: DaimondHandMode.get(), |
| 188 | notice: window.__notice || '', |
| 189 | })); |
| 190 | check('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}`); |
| 193 | check('and the user is told that nothing changed', /could not be set|nothing changed/i.test(failClosed.notice), |
| 194 | failClosed.notice || '(nothing said)'); |
| 195 | |
| 196 | const errs = errors(s).filter(e => !/502|Bad Gateway|the engine refused/.test(e)); |
| 197 | check('nothing threw', errs.length === 0, errs.slice(0, 2).join(' | ')); |
| 198 | |
| 199 | await s.close(); |
| 200 | console.log(`\n${ok.length} passed, ${bad.length} failed`); |
| 201 | if (bad.length) { bad.forEach(b => console.log(' FAILED: ' + b)); process.exit(1); } |