Oregami
Repositories/oxedyne/daimond

oxedyne/daimond/dev/verify_lockkeys.mjs

5.5 KiB, 1 run

created by r2519314175:513, 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_lockkeys.mjs — a locked Daimond holds no readable key.
2//
3// That is the contract, and it used to be kept by one line: `cfg.apiKey = ''`, because there was
4// one key and it lived there. There are now a key per PROVIDER, held in memory by DaimondModels,
5// and a built agent for every chat and every Diamond with its key already handed to the wasm.
6// `DaimondModels.lock()` was written to forget the first of those -- and never called.
7//
8// So the lock is tested from the outside: after locking, can anything still name a key, and can
9// any built agent still reach a provider? Asking the app whether it FEELS locked would prove
10// nothing; the question is whether the key is gone.
11import { open, chat, errors } from './harness.mjs';
12import http from 'node:http';
13
14const ok = [], bad = [];
15const check = (name, pass, detail) => {
16 (pass ? ok : bad).push(name + (detail ? ' — ' + detail : ''));
17 console.log((pass ? ' ok ' : ' FAIL ') + name + (detail ? ' — ' + detail : ''));
18};
19
20// A provider that records every request it is sent.
21const seen = [];
22const srv = http.createServer((req, res) => {
23 if (req.method === 'OPTIONS') {
24 res.writeHead(204, {
25 'access-control-allow-origin': '*',
26 'access-control-allow-headers': '*',
27 'access-control-allow-methods': 'POST, GET, OPTIONS',
28 });
29 return res.end();
30 }
31 let body = '';
32 req.on('data', c => (body += c));
33 req.on('end', () => {
34 if (req.url.endsWith('/models')) {
35 res.writeHead(200, { 'content-type': 'application/json', 'access-control-allow-origin': '*' });
36 return res.end(JSON.stringify({ data: [{ id: 'test-model' }] }));
37 }
38 seen.push(req.headers.authorization || '');
39 res.writeHead(200, { 'content-type': 'application/json', 'access-control-allow-origin': '*' });
40 res.end(JSON.stringify({
41 choices: [{ message: { role: 'assistant', content: 'hi' }, finish_reason: 'stop' }],
42 usage: { prompt_tokens: 1, completion_tokens: 1 },
43 }));
44 });
45});
46srv.listen(9121, '127.0.0.1');
47const URL = 'http://127.0.0.1:9121/v1/chat/completions';
48
49const s = await open({ name: 'lockkeys', connect: false });
50const p = s.page;
51await p.waitForTimeout(1200);
52
53// A provider with a key, sealed under the passphrase, and a chat running on it.
54await p.evaluate(async (u) => {
55 const M = window.DaimondModels;
56 M.addProvider('secret', { name: 'Secret Co', url: u });
57 await M.setKey('secret', 'SUPERSECRET-KEY');
58 await M.fetchModels('secret');
59 M.setDefault('secret', 'test-model');
60}, URL);
61await p.waitForTimeout(500);
62
63await chat(s, 'hello');
64check('while unlocked, the chat reaches the provider with its key',
65 seen.length === 1 && seen[0].includes('SUPERSECRET-KEY'), seen[0] || '(nothing sent)');
66
67const unlocked = await p.evaluate(() => ({
68 key: window.DaimondModels.keyFor('secret'),
69 apps: (window.__daimondChats || []).length, // not exposed; only the key matters here
70}));
71check('and the key is readable, as it must be to work',
72 unlocked.key === 'SUPERSECRET-KEY');
73
74// ── Lock ───────────────────────────────────────────────────────────────
75
76await p.evaluate(() => {
77 // The lock is "Log out" in the user menu. Drive the real one.
78 const row = document.querySelector('.user-row');
79 if (row) row.click();
80});
81await p.waitForTimeout(400);
82const locked = await p.evaluate(async () => {
83 const item = [...document.querySelectorAll('button, .menu-item, [role="menuitem"]')]
84 .find(b => /log out/i.test(b.textContent || ''));
85 if (!item) return { drove: false };
86 item.click();
87 await new Promise(r => setTimeout(r, 900));
88 return {
89 drove: true,
90 key: window.DaimondModels.keyFor('secret'),
91 sealed: window.DaimondModels.isSealed('secret'),
92 hasKey: window.DaimondModels.hasKey('secret'),
93 gateUp: getComputedStyle(document.getElementById('identity-modal')).display !== 'none',
94 };
95});
96check('the lock is reachable from the user menu', locked.drove === true);
97check('after locking, no plaintext key can be named',
98 locked.key === '', locked.key ? 'STILL READABLE: ' + locked.key : '(gone)');
99check('the key is still THERE, just sealed — locking is not deleting',
100 locked.hasKey === true && locked.sealed === true,
101 'hasKey=' + locked.hasKey + ' sealed=' + locked.sealed);
102check('and the passphrase gate is up', locked.gateUp === true);
103
104// The sharper test: an agent built BEFORE the lock had the key handed to it. If that agent
105// survived, the lock is cosmetic — the key is still in the wasm and still reaches the provider.
106const before = seen.length;
107const survivor = await p.evaluate(async () => {
108 // Try to make the app that was serving the chat take another turn. If the lock nulled it,
109 // there is nothing here to run.
110 const input = document.getElementById('chat-input');
111 if (input && input.offsetParent !== null) {
112 input.value = 'again';
113 document.getElementById('chat-send').click();
114 await new Promise(r => setTimeout(r, 2500));
115 return 'the composer is still live';
116 }
117 return 'no composer while locked';
118});
119await p.waitForTimeout(1500);
120check('no agent built before the lock can still reach the provider',
121 seen.length === before, `${seen.length - before} request(s) after locking — ${survivor}`);
122
123const errs = errors(s).filter(e => !/favicon|404|401|502|Bad Gateway|net::ERR/.test(e));
124check('nothing throws while locking', errs.length === 0, errs[0] || '');
125
126await s.close();
127srv.close();
128console.log(`\n${ok.length} passed, ${bad.length} failed`);
129if (bad.length) console.log('FAILED:\n ' + bad.join('\n '));
130process.exit(bad.length ? 1 : 0);