fix(cli): address ui797 support review findings (#797)

This commit is contained in:
jason.woltje committed 2026-11-05 21:21:50 -06:00
1 parent bd8340b9b8
commit 19e6cb0f1e
10 files changed
+393 -58

No files matched your search

+2
View File
@@ -6196,3 +6196,5 @@ After: personally authored three-file skills/ms-ux-improvement coordinator packa
2026-11-05 | #797 ui797 support implementation phase | Explicit lead charter from 05fdfef9ea1fdddf58c725c04f229ca8559fc054 on feat/ui797-support: local isolated OIDC/TLS, persistent CDP browser CLI and durable metered campaign supervisor only under packages/cli/live. Queue next refused linked worktree (exit 2); canonical queue/forge ownership remains with lead. Initial author tests identified explicit-config selection and browser evidence-count defects, corrected locally; real browser SSO and full focused gates in progress. No .53/live access, push, runtime/role/contract edits, attribution trailers or stash.
2026-11-05 | #797 ui797 support author candidate verified | Added local single-subject HTTPS OIDC and fixed TLS forwarder, persistent real-input Chromium CLI with private NSS trust, and external durable per-model metered-stop ledgers. Final focused run: 34 pass / 0 fail / 0 skip (identity 6, browser 5, budget 23); dependency regressions: 111 pass / 0 fail / 0 skip; supported processing CLI regressions: 4 pass / 0 fail / 0 skip. Initial configuration-selection, Basic credential decoding, browser readiness/count and shutdown issues were corrected; all final gates pass. Source remains confined to packages/cli/live plus append-only protocol records. Local commit authorized; independent exact-candidate review and supervised-service acceptance remain with lead. No .53/live activity, model spend, push or product-runtime/role/contract changes. Evidence: /mnt/storage/scratch/mosaic-lead/mvp3/ui797-support-r1-evidence/; report: /mnt/storage/scratch/mosaic-lead/mvp3/ui797-support-r1-report.md.
2026-11-06 | #797 ui797 support R2 review corrections verified | Before: local bd8340b9b accepted unrelated named instances, allowed Chromium WebSocket escape, trusted journal token totals, bypassed halt on lock failure, split model allowances across routes, omitted the absolute cutoff/100M GLM exception, and leaked browser fixtures. After: literal ui797/config/marker validation and persisted campaign revalidation; forced origin-only HTTP proxy (loopback bypass disabled, upgrades refused, HTTPS CONNECT restricted) with real HTTP/HTTPS Chromium negatives and independent zero-TCP/zero-upgrade forbidden-listener assertions; authoritative entry-token comparison/recomputed totals; preserved stale locks with durable halt on interruption/restart/storage failure; chartered proxy GLM/Claude/Codex bindings and one allowance per provider/model; inclusive 2026-11-07T06:00:00.000Z cutoff; exact GLM 100M/other models 10M maxima; awaited reverse-order fixture shutdown with owned-process checks and an intentional-failure whole-file regression. Initial CDP/response-policy browser containment and daemon close-event attempts failed and were corrected; terminal individual whole-file runs are identity 8, browser 8, budget 33 = 49 pass / 0 fail / 0 skip / 0 cancelled. All retained review probes rerun: wrong instance/deadline/split-route refuse, numeric corruption latches/halts, locked checks halt without reclaim, forbidden WebSocket hits 0, SIGTERM/SIGKILL survivors 0. The original general probe now exits 1 at its uncaught, correctly refused split-route initialization; a logging-only companion records every outcome. Final owned-process and 17440/17442/17443 listener sweeps are empty; unrelated existing sessions untouched. Source scope packages/cli/live only plus append-only logs. Queue refused linked worktree; no queue/forge write, push, stash, model call, .53/live contact or product-runtime/role/contract/dependency change. Local author candidate only; independent exact-head approval remains with lead. Evidence/report: /mnt/storage/scratch/mosaic-lead/mvp3/ui797-support-fix-r2-evidence/ and ui797-support-fix-r2-report.md.
+1
View File
@@ -2066,3 +2066,4 @@ are never rewritten or removed; corrections are new entries.
2026-11-04 | Mos, skill integrator | #818 / Gitea #1845 | Integrated exact independently approved ms-ux-improvement candidate02 into source;3hashes/validator/whitespace pass and author histories preserved. Separate commit from MVP; its activation target staysbd8120318. No install, activation, publication or live action.
2026-11-05 | Codex ui797 implementation child | #797 isolated OIDC/TLS, browser CLI and campaign budget support from 05fdfef9e on feat/ui797-support | Author implementation/tests underway in lead-chartered worktree; queue refused linked worktree, no canonical queue writes or protected actions.
2026-11-05 | Codex ui797 implementation child | #797 feat/ui797-support author handoff | Three isolated test tools and focused tests ready for lead review; final unique gates 149 pass / 0 fail / 0 skip, real production-CLI browser OIDC sign-in included, metered-stop/ancestry limits documented. Local commit only; no remote/live activity, spend, role/contract or product runtime edits.
2026-11-06 | ui797-support-fix-r2 implementation sub-agent | #797 corrections on feat/ui797-support above bd8340b9b, confined to packages/cli/live plus one appended build entry and this line | All eight review findings addressed with regression tests; sequential whole files identity 8/browser 8/budget 33 (49 pass, 0 fail/skip/cancel), all review probes rerun, owned-process/listener sweeps empty; forced proxy chosen after browser-only containment failed. Local commit authorized; independent review pending. Queue refused linked worktree; no canonical queue/forge writes, push, stash, live/.53 contact, model spend, product-runtime/role/contract/dependency changes or attribution trailers. Report: /mnt/storage/scratch/mosaic-lead/mvp3/ui797-support-fix-r2-report.md.
+24 -3
View File
@@ -121,6 +121,9 @@ The Console validates discovery, JWKS, PKCE, nonce, token signature and its norm
session/owner rules. A missing bus, business or ancestry proof still refuses.
No human/Console binding shortcut or TLS-verification disable flag is used.
Trust `ca.pem` via `NODE_EXTRA_CA_CERTS` only in these test services, never globally.
Prepare and serve require the literal `ui797`, a `mosaic-ui797` config directory
and the current matching data-root marker. Serve revalidates persisted metadata;
the budget commands enforce the same binding.
Certificates expire after two days; do not overwrite identity material on restart.
### Step-by-step real browser CLI for tester agents
@@ -130,7 +133,14 @@ private Unix-socket daemon. Subsequent invocations reuse that daemon. Either set
`UI797_EVIDENCE` or pass `--evidence DIR` on each command. Only the exact loopback
origins `http://127.0.0.1:17440` and `https://127.0.0.1:17443` are accepted. Every
page/subresource redirect must stay on the selected origin. Other targets and
WebSockets are blocked; a blocked network attempt makes the session fail until
WebSocket connections outside that origin are blocked before the handshake by a
forced local HTTP proxy with Chromium's loopback bypass disabled. It opens sockets
only to the selected origin, refuses HTTP upgrades, and permits CONNECT only to
the selected HTTPS host/port. QUIC and non-proxied WebRTC UDP are disabled. The
installed Chromium's CDP WebSocket blocklist did not contain handshakes in the
negative probe; this external boundary does. Request interception also guards
redirects and subresources. A detected
blocked network attempt makes the session fail until
`stop`. The driver never contacts 7332 or mosaic.woltje.com.
The browser trusts the test CA through a private NSS database under its evidence
@@ -199,7 +209,7 @@ An owner creates a single 0600 `campaign.json` **before launching inference**:
"start": "2026-11-06T02:00:00.000Z",
"end": "2026-11-06T03:00:00.000Z",
"caps": [
{"provider":"proxy","route":"glm","model":"glm-5.3-flash","tokens":10000000},
{"provider":"proxy","route":"glm","model":"glm-5.3-flash","tokens":100000000},
{"provider":"proxy","route":"anthropic","model":"claude-opus-5-5","tokens":10000000},
{"provider":"proxy","route":"anthropic","model":"claude-sonnet-5-5","tokens":10000000}
]
@@ -208,7 +218,13 @@ An owner creates a single 0600 `campaign.json` **before launching inference**:
Use the approved actual absolute config path, canonical UTC campaign start/end
and **all** actual model routes; the dates above illustrate the schema. Caps are
positive safe integers at most 10,000,000; tiny values allow zero-spend tests.
positive safe integers: at most 100,000,000 for exactly
`proxy/glm/glm-5.3-flash`, and 10,000,000 for every other approved model. Lower
caps allow zero-spend tests. Only proxy GLM, Claude (`anthropic` route, `claude-*`)
and Codex (`codex` route, `gpt-*`) bindings are accepted. One provider/model has
one allowance across all routes; conflicting bindings refuse. The campaign must
end by **2026-11-07T06:00:00.000Z**; earlier windows narrow that absolute cutoff,
which checks also enforce independently.
The test bus store must exist before initialization. Run the supervisor outside
model-writable settings/data in a normally supervised owner service:
@@ -241,8 +257,13 @@ for its outcome; unknown/malformed completed usage stops the campaign.
Each provider/route/model has a separate append-only ledger, durable across
rounds/restarts. Campaign bytes, root identity and store identity are pinned;
changed caps/epoch, rewritten/disappeared usage and damaged ledger files stop.
Checks recompute each journal entry's tokens and totals from authoritative usage;
a valid-JSON token mismatch durably latches and halts.
Initialization is exclusive. There is no reset, raise or resume command. A stale
`check.lock` after interruption requires owner diagnosis; never auto-reclaim it.
An existing or uncreatable lock prevents accounting and still asserts the sticky
halt against the verified ui797 binding on every check/serve tick. The lock is
preserved, including when a stop was already latched.
A changed target config refuses rather than halting an unverified data root.
At any cap, deadline or accounting refusal, the supervisor fsyncs `stop.json`
+44 -16
View File
@@ -7,6 +7,14 @@ import { join, dirname } from 'node:path';
import { spawnSync } from 'node:child_process';
import { turnRecords } from '../../bus/src/usage.mjs';
import { exclusive, fail, isMain, options, outsideRuntime, plainPath, privateDir, readPrivate, ROOT, testInstance } from './support-common.mjs';
export const CUTOFF = Date.parse('2026-11-07T06:00:00.000Z');
const modelIdentity = c => JSON.stringify([c.provider,c.model]);
// The charter permits the GLM identity and Claude/Codex coder model families via proxy.
const approvedBinding = c => c.provider === 'proxy' && (
(c.route === 'glm' && c.model === 'glm-5.3-flash') ||
(c.route === 'anthropic' && /^claude-[a-zA-Z0-9._-]+$/.test(c.model)) ||
(c.route === 'codex' && /^gpt-[a-zA-Z0-9._-]+$/.test(c.model)));
const ceiling = c => c.provider === 'proxy' && c.route === 'glm' && c.model === 'glm-5.3-flash' ? 100000000 : 10000000;
const kinds = ['input','output','cacheWrite','cacheRead'];
const digest = x => createHash('sha256').update(typeof x === 'string' ? x : JSON.stringify(x)).digest('hex');
const identity = c => JSON.stringify([c.provider,c.route,c.model]);
@@ -15,16 +23,19 @@ const canonicalTime = s => typeof s === 'string' && Number.isFinite(Date.parse(s
function campaignAt(path) {
privateDir(dirname(path));
const text = readPrivate(path), c = JSON.parse(text);
if (c.version !== 1 || c.owner !== 'ui797-owner' || !/^[a-z][a-z0-9-]{1,63}$/.test(c.business ?? '') || !canonicalTime(c.start) || !canonicalTime(c.end) || c.end <= c.start ||
validateCampaign(c);
return { c, hash:digest(text) };
}
function validateCampaign(c) {
if (c.instance !== 'ui797' || c.version !== 1 || c.owner !== 'ui797-owner' || !/^[a-z][a-z0-9-]{1,63}$/.test(c.business ?? '') || !canonicalTime(c.start) || !canonicalTime(c.end) || c.end <= c.start || Date.parse(c.end) > CUTOFF ||
!Array.isArray(c.caps) || !c.caps.length || c.caps.length > 20) fail('campaign-invalid');
const keys = new Set();
for (const cap of c.caps) {
if (Object.keys(cap).sort().join(',') !== 'model,provider,route,tokens' || !['proxy','anthropic','zai','openai'].includes(cap.provider) ||
!['glm','anthropic','codex'].includes(cap.route) || !/^[a-zA-Z0-9][a-zA-Z0-9._:/-]{0,127}$/.test(cap.model ?? '') ||
!Number.isSafeInteger(cap.tokens) || cap.tokens < 1 || cap.tokens > 10000000 || keys.has(identity(cap))) fail('campaign-cap-invalid');
keys.add(identity(cap));
!approvedBinding(cap) || !Number.isSafeInteger(cap.tokens) || cap.tokens < 1 || cap.tokens > ceiling(cap) || keys.has(modelIdentity(cap))) fail('campaign-cap-invalid');
keys.add(modelIdentity(cap));
}
return { c, hash:digest(text) };
}
function binding(c) {
const { system } = testInstance(c.instance,c.config);
@@ -119,7 +130,7 @@ function loadLedger(ledger) {
if (p.version !== 1 || !p.binding || !p.campaign) fail('ledger-manifest-invalid');
return p;
}
export async function check({ campaign,ledger }, { now=Date.now, read=readAccounting, halt=supportedHalt }={}) {
export async function check({ campaign,ledger }, { now=Date.now, read=readAccounting, halt=supportedHalt, acquireLock=mkdirSync }={}) {
let p, integrityFailure=false;
try { p=loadLedger(ledger); } catch {
// With a damaged manifest, only the owner campaign and validated TEST marker may identify
@@ -131,20 +142,27 @@ export async function check({ campaign,ledger }, { now=Date.now, read=readAccoun
// Pin the target before any halt. A changed config cannot redirect the stop into live data.
checkBinding(p.binding,c); outsideRuntime(ledger,{dataRoot:p.binding.dataRoot},c.config);
outsideRuntime(campaign,{dataRoot:p.binding.dataRoot},c.config);
const lock=join(ledger,'check.lock'); mkdirSync(lock,{mode:0o700});
const lock=join(ledger,'check.lock'); let locked=false, lockFailure=false;
try { acquireLock(lock,{mode:0o700}); locked=true; } catch { lockFailure=true; }
// An interrupted/active/unwritable lock prevents accounting, never the verified halt.
// Only the check that created a lock may remove it on normal completion.
try {
let reason=integrityFailure?'ledger-manifest-invalid':null, totals=[];
let reason=integrityFailure?'ledger-manifest-invalid':lockFailure?'check-lock-unavailable':null, totals=[];
const stopFile=join(ledger,'stop.json');
if (existsSync(stopFile)) {
reason='stop-already-latched';
try { JSON.parse(readPrivate(stopFile)); } catch { reason='stop-latch-invalid'; }
}
try {
if (campaignAt(campaign).hash !== p.hash) fail('campaign-changed');
const clock=now(); if (clock < Date.parse(c.start)) fail('campaign-not-started'); if (clock>=Date.parse(c.end)) fail('campaign-ended');
const clock=now(); if (!Number.isFinite(clock)) fail('campaign-clock-invalid');
if (clock>=CUTOFF) fail('campaign-absolute-cutoff');
validateCampaign(c);
const owner=campaignAt(campaign);
if (owner.hash !== p.hash || JSON.stringify(owner.c) !== JSON.stringify(c)) fail('campaign-changed');
if (clock < Date.parse(c.start)) fail('campaign-not-started'); if (clock>=Date.parse(c.end)) fail('campaign-ended');
if (!reason) {
const records=await read({...p.binding,start:c.start,end:c.end}), observed=new Set();
const accounts=c.caps.map(cap=>({cap,...journal(ledger,cap)}));
const accounts=c.caps.map(cap=>({cap,...journal(ledger,cap),total:0}));
for (const r of records) {
if (!canonicalTime(r.at) || r.at<c.start || r.status!=='known' || kinds.some(k=>!Number.isSafeInteger(r[k]) || r[k]<0)) fail('usage-unknown-or-malformed');
const a=accounts.find(a=>identity(a.cap)===identity(r)); if (!a) fail('usage-uncapped-route-model');
@@ -152,14 +170,23 @@ export async function check({ campaign,ledger }, { now=Date.now, read=readAccoun
const tokens=kinds.reduce((sum,k)=>sum+r[k],0); if (!Number.isSafeInteger(tokens)) fail('usage-overflow');
const hash=digest(r), previous=a.entries.get(key);
if (previous && previous.digest !== hash) fail('usage-rewritten');
if (previous && previous.tokens !== tokens) fail('ledger-token-mismatch');
a.total+=tokens; if (!Number.isSafeInteger(a.total)) fail('ledger-overflow');
if (!previous) {
const e={key,digest:hash,tokens}; writeJournal(join(ledger,filename(a.cap)),e); a.entries.set(key,e); a.total+=tokens;
if (!Number.isSafeInteger(a.total)) fail('ledger-overflow');
const e={key,digest:hash,tokens}; writeJournal(join(ledger,filename(a.cap)),e); a.entries.set(key,e);
}
}
for (const a of accounts) for (const key of a.entries.keys()) if (!observed.has(key)) fail('usage-disappeared');
totals=accounts.map(a=>({...a.cap,total:a.total}));
if (totals.some(a=>a.total>=a.tokens)) reason='model-cap-reached';
// Route journals are evidence, never separate allowances for the same model.
const models=new Map();
for (const a of totals) {
const key=modelIdentity(a), model=models.get(key) ?? {total:0,tokens:Math.min(a.tokens,ceiling(a))};
model.total+=a.total; model.tokens=Math.min(model.tokens,a.tokens,ceiling(a));
if (!Number.isSafeInteger(model.total)) fail('ledger-overflow');
models.set(key,model);
}
if ([...models.values()].some(a=>a.total>=a.tokens)) reason='model-cap-reached';
}
} catch(e) { reason=/^[a-z-]{1,80}$/.test(e.message)?e.message:'accounting-failed'; }
if (reason) {
@@ -179,12 +206,13 @@ export async function check({ campaign,ledger }, { now=Date.now, read=readAccoun
return {stopped:true,reason,totals,halt:result};
}
return {stopped:false,totals};
} finally { rmdirSync(lock); }
} finally { if (locked) rmdirSync(lock); }
}
export function status({ campaign,ledger }) {
const p=loadLedger(ledger); checkBinding(p.binding,p.campaign);
const p=loadLedger(ledger); validateCampaign(p.campaign); checkBinding(p.binding,p.campaign);
outsideRuntime(campaign,{dataRoot:p.binding.dataRoot},p.campaign.config);
if (campaignAt(campaign).hash!==p.hash) fail('campaign-changed');
const owner=campaignAt(campaign);
if (owner.hash!==p.hash || JSON.stringify(owner.c)!==JSON.stringify(p.campaign)) fail('campaign-changed');
const totals=p.campaign.caps.map(cap=>({...cap,total:journal(ledger,cap).total}));
return { stopped:existsSync(join(ledger,'stop.json')),end:p.campaign.end,totals };
}
+113 -5
View File
@@ -1,16 +1,19 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdirSync,writeFileSync,readFileSync,readdirSync,unlinkSync,symlinkSync } from 'node:fs';
import { mkdirSync,writeFileSync,readFileSync,readdirSync,unlinkSync,symlinkSync,renameSync,existsSync,lstatSync } from 'node:fs';
import { spawn } from 'node:child_process';
import { once } from 'node:events';
import { join } from 'node:path';
import { initialize,check,status,readAccounting,supportedHalt } from './budget-supervisor.mjs';
import { initialize,check,status,readAccounting,supportedHalt,main,CUTOFF } from './budget-supervisor.mjs';
import { fixture } from './test-support.mjs';
import { Store } from '../../bus/src/store.mjs';
const NOW=Date.parse('2026-11-06T02:30:00.000Z');
const row=(extra={})=>({at:'2026-11-06T02:29:00.000Z',run:'r123456789abc',turn:'t1',source:'transcript',instance:'assistant',provider:'proxy',route:'glm',model:'glm-5.3-flash',status:'known',input:1,output:1,cacheWrite:1,cacheRead:1,...extra});
function setup(t,tokens=5) {
const f=fixture(t),store=new Store(f.dataRoot);t.after(()=>store.close());
function setup(t,tokens=5,changes={}) {
const f=fixture(t),store=new Store(f.dataRoot);f.after(()=>store.close());
const campaign=join(f.root,'campaign.json'),ledger=join(f.root,'ledger');
const c={version:1,instance:'ui797',config:f.config,business:'mvp3-ui797',owner:'ui797-owner',start:'2026-11-06T02:00:00.000Z',end:'2026-11-06T03:00:00.000Z',caps:[{provider:'proxy',route:'glm',model:'glm-5.3-flash',tokens},{provider:'proxy',route:'anthropic',model:'claude-opus-5-5',tokens:6}]};
Object.assign(c,changes);
writeFileSync(campaign,JSON.stringify(c),{mode:0o600});initialize({campaign,ledger});
const halts=[],halt=b=>{halts.push(b);return {halted:true,quiescent:true,generation:'test'};};
return {...f,store,campaign,ledger,c,halts,halt,opts:{campaign,ledger}};
@@ -110,7 +113,7 @@ test('an invalid existing stop latch still invokes the supported test halt',asyn
const result=await check(f.opts,{now:()=>NOW,read:()=>[],halt:f.halt});assert.equal(result.reason,'stop-latch-invalid');assert.equal(f.halts.length,1);
});
test('model-writable campaign paths refuse initialization',t=>{
const f=fixture(t),store=new Store(f.dataRoot);t.after(()=>store.close());
const f=fixture(t),store=new Store(f.dataRoot);f.after(()=>store.close());
const campaign=join(f.dataRoot,'campaign.json');writeFileSync(campaign,JSON.stringify({version:1,instance:'ui797',config:f.config,business:'mvp3-ui797',owner:'ui797-owner',start:'2026-11-06T02:00:00.000Z',end:'2026-11-06T03:00:00.000Z',caps:[{provider:'proxy',route:'glm',model:'glm-5.3-flash',tokens:1}]}),{mode:0o600});
assert.throws(()=>initialize({campaign,ledger:join(f.root,'ledger')}),/outside/);
});
@@ -120,3 +123,108 @@ test('a failed durable-latch write still halts and returns an explicit storage f
await assert.rejects(check(f.opts,{now:()=>NOW,read:()=>[row()],halt:f.halt}),/stop-latch-not-durable/);
assert.equal(f.halts.length,1);assert.equal(JSON.parse(readFileSync(join(f.ledger,'halt-1.json'))).storageError,'stop-latch-not-durable');
});
test('valid JSON journal token corruption durably latches and halts before accepting another turn',async t=>{
const f=setup(t),deps={now:()=>NOW,read:()=>[row()],halt:f.halt};
await check(f.opts,deps);
const file=readdirSync(f.ledger).filter(n=>n.endsWith('.jsonl')).map(n=>join(f.ledger,n)).find(n=>readFileSync(n,'utf8').includes('"key"'));
const lines=readFileSync(file,'utf8').trimEnd().split('\n').map(JSON.parse);lines[1].tokens=0;
writeFileSync(file,lines.map(JSON.stringify).join('\n')+'\n');
const result=await check(f.opts,{...deps,read:()=>[row(),row({turn:'t2'})]});
assert.equal(result.reason,'ledger-token-mismatch');assert.equal(f.halts.length,1);
assert.equal(JSON.parse(readFileSync(join(f.ledger,'stop.json'))).reason,'ledger-token-mismatch');
});
test('a check killed while holding its lock is halted on restart without reclaiming the lock',async t=>{
const f=setup(t,100);
const code=`import {check} from ${JSON.stringify(new URL('./budget-supervisor.mjs',import.meta.url).href)}; await check(${JSON.stringify(f.opts)},{now:()=>${NOW},read:()=>{process.send('locked');return new Promise(()=>{});}});`;
const child=spawn(process.execPath,['--input-type=module','-e',code],{stdio:['ignore','ignore','pipe','ipc']});
f.after(async()=>{if(child.exitCode===null&&child.signalCode===null){child.kill('SIGKILL');await once(child,'close');}});
await once(child,'message');child.kill('SIGKILL');await once(child,'close');
const lock=join(f.ledger,'check.lock'),ino=lstatSync(lock).ino;
const result=await check(f.opts,{now:()=>NOW,read:()=>assert.fail('locked accounting must not run'),halt:f.halt});
assert.equal(result.reason,'check-lock-unavailable');assert.equal(f.halts.length,1);
assert.equal(lstatSync(lock).ino,ino);assert.ok(existsSync(join(f.ledger,'stop.json')));
});
test('an existing latch reasserts halt on every locked restart and preserves lock evidence',async t=>{
const f=setup(t,1);await check(f.opts,{now:()=>NOW,read:()=>[row()],halt:f.halt});
const lock=join(f.ledger,'check.lock');mkdirSync(lock,{mode:0o700});writeFileSync(join(lock,'owner'),'diagnose');
for(let n=0;n<2;n++) assert.equal((await check(f.opts,{now:()=>NOW,halt:f.halt})).stopped,true);
assert.equal(f.halts.length,3);assert.equal(readFileSync(join(lock,'owner'),'utf8'),'diagnose');
});
test('lock storage failures still durably stop the verified test workload',async t=>{
for(const code of ['EROFS','EACCES']) {
const f=setup(t,100);
const result=await check(f.opts,{now:()=>NOW,halt:b=>{assert.equal(b.dataRoot,f.dataRoot);assert.ok(existsSync(join(f.ledger,'stop.json')));return f.halt(b);},acquireLock:()=>{throw Object.assign(new Error('storage'),{code});}});
assert.equal(result.reason,'check-lock-unavailable');assert.equal(f.halts.length,1);
}
const f=setup(t,100),lock=join(f.ledger,'check.lock');writeFileSync(lock,'not a directory',{mode:0o600});
assert.equal((await check(f.opts,{now:()=>NOW,halt:f.halt})).stopped,true);
assert.equal(readFileSync(lock,'utf8'),'not a directory');assert.equal(f.halts.length,1);
});
test('budget init, check, status and serve refuse unrelated instance metadata before accounting or halt',async t=>{
const f=setup(t,100),other=join(f.root,'mosaic-unrelated');renameSync(join(f.root,'mosaic-ui797'),other);
const config=join(other,'config.json');writeFileSync(join(f.dataRoot,'instance.json'),JSON.stringify({instanceVersion:1,name:'unrelated'}));
f.c.instance='unrelated';f.c.config=config;writeFileSync(f.campaign,JSON.stringify(f.c));
assert.throws(()=>initialize({campaign:f.campaign,ledger:join(f.root,'other-ledger')}),/campaign-invalid/);
const file=join(f.ledger,'campaign.json'),p=JSON.parse(readFileSync(file));p.campaign=f.c;writeFileSync(file,JSON.stringify(p));
await assert.rejects(check(f.opts,{now:()=>NOW,halt:f.halt}),/literal ui797/);
assert.throws(()=>status(f.opts),/campaign-invalid/);
await assert.rejects(main(['serve','--campaign',f.campaign,'--ledger',f.ledger]),/literal ui797/);
assert.equal(f.halts.length,0);
});
test('persisted campaign metadata is compared to the validated owner manifest before accounting',async t=>{
const f=setup(t,100),file=join(f.ledger,'campaign.json'),p=JSON.parse(readFileSync(file));
p.campaign.caps[0].tokens=101;writeFileSync(file,JSON.stringify(p));
assert.equal((await check(f.opts,{now:()=>NOW,read:()=>assert.fail('changed metadata'),halt:f.halt})).reason,'campaign-changed');
assert.equal(f.halts.length,1);
});
test('absolute cutoff is inclusive; later windows refuse and a persisted extension cannot delay a stop',async t=>{
const end='2026-11-07T06:00:00.000Z',f=setup(t,100,{end});
assert.equal((await check(f.opts,{now:()=>CUTOFF-1,read:()=>[],halt:f.halt})).stopped,false);
assert.equal((await check(f.opts,{now:()=>CUTOFF,read:()=>[],halt:f.halt})).reason,'campaign-absolute-cutoff');
assert.equal(f.halts.length,1);
assert.throws(()=>setup(t,100,{end:'2026-11-08T06:00:00.000Z'}),/campaign-invalid/);
const g=setup(t,100,{end}),file=join(g.ledger,'campaign.json'),p=JSON.parse(readFileSync(file));
p.campaign.end='2026-11-08T06:00:00.000Z';writeFileSync(file,JSON.stringify(p));
assert.equal((await check(g.opts,{now:()=>CUTOFF,halt:g.halt})).reason,'campaign-absolute-cutoff');
assert.equal(g.halts.length,1);
});
test('only the exact proxy/glm identity receives 100M; other approved models remain capped at 10M',t=>{
const cap=(provider,route,model,tokens)=>({provider,route,model,tokens});
const glm=cap('proxy','glm','glm-5.3-flash',100000000),claude=cap('proxy','anthropic','claude-opus-5-5',10000000),codex=cap('proxy','codex','gpt-6-astra',10000000);
for(const c of [glm,claude,codex])assert.ok(setup(t,1,{caps:[c]}));
for(const c of [glm,claude,codex])assert.throws(()=>setup(t,1,{caps:[{...c,tokens:c.tokens+1}]}),/campaign-cap-invalid/);
for(const c of [cap('zai','glm','glm-5.3-flash',100000000),cap('proxy','anthropic','glm-5.3-flash',100000000),cap('proxy','glm','claude-opus-5-5',100000000)])assert.throws(()=>setup(t,1,{caps:[c]}),/campaign-cap-invalid/);
});
test('conflicting route bindings refuse initialization and real-store split-route usage stops',async t=>{
const cap={provider:'proxy',route:'anthropic',model:'claude-opus-5-5',tokens:10000000};
assert.throws(()=>setup(t,1,{caps:[{...cap,route:'glm'},cap]}),/campaign-cap-invalid/);
assert.throws(()=>setup(t,1,{caps:[cap,{...cap}]}),/campaign-cap-invalid/);
const f=setup(t,1,{caps:[cap]});
for(const [i,route] of ['anthropic','glm'].entries()) {
const r=row({run:i?'r123456789abd':'r123456789abc',turn:`t${i}`,model:cap.model,input:6000000,output:0,cacheWrite:0,cacheRead:0});
f.store.run('INSERT INTO usage_records(at,business,run,instance,provider,model,turn,source,phase,status,input,output,cache_write,cache_read) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?)',r.at,f.c.business,r.run,r.instance,r.provider,r.model,r.turn,r.source,'end','known',r.input,0,0,0);
const dir=join(f.dataRoot,'launches',f.c.business,r.run,'bundle');mkdirSync(dir,{recursive:true,mode:0o700});
writeFileSync(join(dir,'manifest.json'),JSON.stringify({business:f.c.business,run:r.run,instance:r.instance,provider:r.provider,model:r.model,proxyRoute:route,route:'primary'}),{mode:0o600});
}
const result=await check(f.opts,{now:()=>NOW,halt:f.halt});
assert.equal(result.reason,'usage-uncapped-route-model');assert.equal(f.halts.length,1);
assert.ok(existsSync(join(f.ledger,'stop.json')));
});
test('the authorized GLM and other-model maxima halt at the exact charged ceiling',async t=>{
for(const cap of [{provider:'proxy',route:'glm',model:'glm-5.3-flash',tokens:100000000},{provider:'proxy',route:'anthropic',model:'claude-sonnet-5-5',tokens:10000000}]) {
const f=setup(t,1,{caps:[cap]});
const r=row({...cap,input:cap.tokens,output:0,cacheWrite:0,cacheRead:0});delete r.tokens;
const result=await check(f.opts,{now:()=>NOW,read:()=>[r],halt:f.halt});
assert.equal(result.reason,'model-cap-reached');assert.equal(result.totals[0].total,cap.tokens);assert.equal(f.halts.length,1);
}
});
+30 -9
View File
@@ -3,12 +3,11 @@ import assert from 'node:assert/strict';
import { createHash, verify, createPublicKey } from 'node:crypto';
import { request } from 'node:https';
import { createServer } from 'node:http';
import { readFileSync, statSync, writeFileSync } from 'node:fs';
import { readFileSync, statSync, writeFileSync,renameSync } from 'node:fs';
import { join } from 'node:path';
import { spawn } from 'node:child_process';
import { once } from 'node:events';
import { prepare,serve,SUBJECT } from './isolated-identity.mjs';
import { fixture } from './test-support.mjs';
import { fixture,closeServer,closeChild } from './test-support.mjs';
import { readSlot } from '../../webui/src/sso.mjs';
function setup(t) {
const f=fixture(t),runtime=join(f.root,'identity');
@@ -43,7 +42,7 @@ test('live/default instances, wrong owner, unclaimed root and unsafe config path
assert.throws(()=>prepare(defaults));
});
test('real HTTPS authorization redirects, PKCE and nonce bind a single-use bounded RS256 token',async t=>{
const f=setup(t),server=await serve(f.runtime);t.after(()=>{server.closeAllConnections();server.close();});
const f=setup(t),server=await serve(f.runtime);f.after(()=>closeServer(server));
const discovery=await http(f.runtime,'/oidc/.well-known/openid-configuration');assert.equal(discovery.status,200);
const verifier='a'.repeat(43),challenge=createHash('sha256').update(verifier).digest('base64url');
const q=new URLSearchParams({client_id:'mosaic-ui797',redirect_uri:'https://127.0.0.1:17443/auth/callback',response_type:'code',scope:'openid',state:'test-state',nonce:'test-nonce',code_challenge:challenge,code_challenge_method:'S256'});
@@ -67,23 +66,23 @@ test('real HTTPS authorization redirects, PKCE and nonce bind a single-use bound
test('fixed TLS forwarder preserves normal Console Host, cookies and origin without injected identity',async t=>{
const f=setup(t);let headers;
const upstream=createServer((req,res)=>{headers=req.headers;res.end('Console upstream');});
await new Promise((resolve,reject)=>{upstream.once('error',reject);upstream.listen(17440,'127.0.0.1',resolve);});t.after(()=>upstream.close());
const server=await serve(f.runtime);t.after(()=>{server.closeAllConnections();server.close();});
await new Promise((resolve,reject)=>{upstream.once('error',reject);upstream.listen(17440,'127.0.0.1',resolve);});f.after(()=>closeServer(upstream));
const server=await serve(f.runtime);f.after(()=>closeServer(server));
const r=await http(f.runtime,'/test',{headers:{origin:'https://127.0.0.1:17443',cookie:'fixture=value'}});
assert.equal(r.body,'Console upstream');assert.equal(headers.host,'127.0.0.1:17443');assert.equal(headers.origin,'https://127.0.0.1:17443');assert.equal(headers.cookie,'fixture=value');assert.equal(headers['x-auth-subject'],undefined);
});
test('production Console CLI discovers this OP through NODE_EXTRA_CA_CERTS with ordinary TLS validation',async t=>{
const f=setup(t),config=JSON.parse(readFileSync(f.config));config.console=f.output.console;writeFileSync(f.config,JSON.stringify(config));
const server=await serve(f.runtime);t.after(()=>{server.closeAllConnections();server.close();});
const server=await serve(f.runtime);f.after(()=>closeServer(server));
const child=spawn(process.execPath,['packages/webui/src/cli.mjs','serve','--port','17440','--board','http://127.0.0.1:17439','--sso'],{env:{...process.env,MOSAIC_CONFIG:f.config,NODE_EXTRA_CA_CERTS:join(f.runtime,'ca.pem')},stdio:['ignore','pipe','pipe']});
t.after(async()=>{if(child.exitCode===null){child.kill('SIGTERM');await once(child,'exit');}});
f.after(async()=>{await closeChild(child);});
let output='';child.stdout.on('data',c=>output+=c);child.stderr.on('data',c=>output+=c);
await new Promise((resolve,reject)=>{const timer=setTimeout(()=>reject(new Error('Console startup: '+output)),15000);child.stdout.on('data',()=>{if(output.includes('http://127.0.0.1:17440')){clearTimeout(timer);resolve();}});child.once('exit',()=>{clearTimeout(timer);reject(new Error('Console refused: '+output));});});
const health=JSON.parse((await http(f.runtime,'/healthz')).body);assert.equal(health.sso['ui797-local'],'ready');
});
test('authorization codes expire, and tampering with the single subject refuses service',async t=>{
const f=setup(t);let now=Date.now();
const server=await serve(f.runtime,{clock:()=>now});t.after(()=>{server.closeAllConnections();server.close();});
const server=await serve(f.runtime,{clock:()=>now});f.after(()=>closeServer(server));
const verifier='v'.repeat(43),challenge=createHash('sha256').update(verifier).digest('base64url');
const q=new URLSearchParams({client_id:'mosaic-ui797',redirect_uri:'https://127.0.0.1:17443/auth/callback',response_type:'code',scope:'openid',state:'state',nonce:'nonce',code_challenge:challenge,code_challenge_method:'S256'});
const code=new URL((await http(f.runtime,'/oidc/authorize?'+q)).headers.location).searchParams.get('code');now+=60001;
@@ -93,3 +92,25 @@ test('authorization codes expire, and tampering with the single subject refuses
const stateFile=join(f.runtime,'identity.json'),state=JSON.parse(readFileSync(stateFile));state.console.sso.providers[0].subjects=[{sub:'jason',name:'jason'}];writeFileSync(stateFile,JSON.stringify(state));
await assert.rejects(serve(f.runtime),/metadata refused/);
});
test('prepare and persisted serve metadata refuse an otherwise valid unrelated development instance',async t=>{
const f=setup(t),other=join(f.root,'mosaic-unrelated');
renameSync(join(f.root,'mosaic-ui797'),other);
const config=join(other,'config.json');
writeFileSync(join(f.dataRoot,'instance.json'),JSON.stringify({instanceVersion:1,name:'unrelated'}));
assert.throws(()=>prepare({instance:'unrelated',config,owner:'ui797-owner',runtime:join(f.root,'other-id'),tlsPort:17443}),/literal ui797/);
const stateFile=join(f.runtime,'identity.json'),state=JSON.parse(readFileSync(stateFile));
state.instance='unrelated';state.config=config;writeFileSync(stateFile,JSON.stringify(state));
await assert.rejects(serve(f.runtime),/literal ui797/);
});
test('serve revalidates the config directory and current data marker before listening',async t=>{
const f=setup(t);
writeFileSync(join(f.dataRoot,'instance.json'),JSON.stringify({instanceVersion:1,name:'unrelated'}));
await assert.rejects(serve(f.runtime),/marker mismatch/);
writeFileSync(join(f.dataRoot,'instance.json'),JSON.stringify({instanceVersion:1,name:'ui797'}));
const stateFile=join(f.runtime,'identity.json'),state=JSON.parse(readFileSync(stateFile));
const other=join(f.root,'mosaic-unrelated');renameSync(join(f.root,'mosaic-ui797'),other);
state.config=join(other,'config.json');writeFileSync(stateFile,JSON.stringify(state));
await assert.rejects(serve(f.runtime),/matching named test config/);
});
+1 -1
View File
@@ -48,7 +48,7 @@ export function options(argv, allowed) {
return out;
}
export function testInstance(instance, config) {
if (!/^[a-z][a-z0-9]{1,15}$/.test(instance ?? '') || ['dev','prod','production'].includes(instance)) fail('explicit named test instance required');
if (instance !== 'ui797') fail('literal ui797 test instance required');
plainPath(config);
const copies = join(process.env.XDG_DATA_HOME || join(homedir(), '.local/share'), 'mosaic-stack/copies');
if (inside(ROOT, config) || inside(copies, config)) fail('test config and secret slots must be outside source and stack copies');
+44 -3
View File
@@ -1,15 +1,56 @@
// Focused test fixtures only; runtime CLIs never import this file.
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
import { mkdtempSync, mkdirSync, writeFileSync, rmSync,readFileSync,readdirSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
export function fixture(t) {
if (!process.env.TMPDIR?.startsWith('/mnt/storage/scratch/tmp/') || process.env.TMPDIR.includes('_')) throw new Error('set TMPDIR with mktemp -d /mnt/storage/scratch/tmp/XXXXXX');
const root=mkdtempSync(join(tmpdir(),'ui797-'));
t.after(()=>rmSync(root,{recursive:true,force:true}));
const cleanups=[];
t.after(async()=>{
const owned=ownedProcesses(root);
const errors=[];
for(const cleanup of cleanups.reverse()) try { await cleanup(); } catch(e) { errors.push(e); }
// Preserve evidence if resource shutdown failed; never delete beneath a running service.
const survivors=owned.filter(p=>processAlive(p));
if(survivors.length) errors.push(new Error(`owned processes survived fixture shutdown: ${survivors.map(p=>p.pid)}`));
if(errors.length) throw new AggregateError(errors,'fixture shutdown failed');
rmSync(root,{recursive:true,force:true});
});
const configDir=join(root,'mosaic-ui797'),dataRoot=join(root,'data');
mkdirSync(configDir,{mode:0o700});mkdirSync(dataRoot,{mode:0o700});
const config=join(configDir,'config.json');
writeFileSync(config,JSON.stringify({configVersion:1,environment:'development',dataRoot,execution:{backend:'docker',provider:'zai',model:'glm-5.3-flash'}}));
writeFileSync(join(dataRoot,'instance.json'),JSON.stringify({instanceVersion:1,name:'ui797'}));
return {root,config,dataRoot};
return {root,config,dataRoot,after:fn=>cleanups.push(fn)};
}
export async function closeServer(server) {
const closed=new Promise((resolve,reject)=>server.close(e=>e?reject(e):resolve()));
server.closeAllConnections();
await closed;
}
export async function closeChild(child) {
if(child.exitCode!==null || child.signalCode!==null) return;
const closed=new Promise(resolve=>child.once('close',resolve));
child.kill('SIGTERM');
await closed;
}
// Snapshot only this fixture's processes, including reparented Chromium crash handlers.
export function ownedProcesses(root) {
const processes=readdirSync('/proc').filter(n=>/^\d+$/.test(n)).flatMap(pid=>{
try {
const args=readFileSync(`/proc/${pid}/cmdline`,'utf8').split('\0');
const stat=readFileSync(`/proc/${pid}/stat`,'utf8').split(' ');
return [{pid:Number(pid),ppid:Number(stat[3]),start:stat[21],args}];
} catch { return []; }
});
const selected=new Set(processes.filter(p=>p.args.some(a=>a===root || a.startsWith(root+'/') || a.startsWith('--user-data-dir='+root+'/'))).map(p=>p.pid));
for(const p of processes) if(selected.has(p.pid)) for(const a of p.args) if(a.startsWith('--crashpad-handler-pid=')) selected.add(Number(a.split('=')[1]));
let size;
do {size=selected.size;for(const p of processes) if(selected.has(p.ppid))selected.add(p.pid);} while(size!==selected.size);
return processes.filter(p=>selected.has(p.pid));
}
export function processAlive(p) {
try { const stat=readFileSync(`/proc/${p.pid}/stat`,'utf8').split(' ');return stat[21]===p.start && stat[2]!=='Z'; } catch {return false;}
}
+71 -12
View File
@@ -2,6 +2,7 @@
// Persistent CDP pipe client, based on webui/tests/browser.mjs. No injected app script.
import { spawn, fork, spawnSync } from 'node:child_process';
import { createServer, createConnection } from 'node:net';
import {createServer as httpServer,request as httpRequest} from 'node:http';
import { once } from 'node:events';
import { createHash, X509Certificate } from 'node:crypto';
import { appendFileSync, chmodSync, existsSync, readdirSync, readFileSync, unlinkSync } from 'node:fs';
@@ -9,6 +10,7 @@ import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { homedir } from 'node:os';
import { exclusive, fail, isMain, options, plainPath, privateDir, readPrivate } from './support-common.mjs';
const ownedDaemons = new Map();
const HERE = fileURLToPath(import.meta.url);
const INTERACTIVE = new Set(['button','link','textbox','searchbox','combobox','checkbox','radio','switch','tab','menuitem','option','spinbutton','slider']);
const delay = ms => new Promise(r => setTimeout(r, ms));
@@ -19,7 +21,7 @@ export function testURL(value, origin = null) {
if (origin && u.origin !== origin) fail('navigation must stay on the selected test origin');
return u;
}
async function chromium(evidence, ca) {
async function chromium(evidence, ca, proxyPort) {
const cert = new X509Certificate(readPrivate(ca));
if (!cert.ca || Date.parse(cert.validTo) <= Date.now() || Date.parse(cert.validFrom) > Date.now()) fail('valid private test CA required');
const xdg = join(evidence, 'xdg'); privateDir(xdg, true);
@@ -39,7 +41,9 @@ async function chromium(evidence, ca) {
if (legacy) plainPath(legacyNss);
const executable = process.env.CHROMIUM || '/usr/bin/chromium';
const chromeArgs = ['--headless','--no-first-run','--no-default-browser-check','--disable-dev-shm-usage',
'--disable-background-networking','--disable-component-update','--disable-sync','--no-proxy-server','--remote-debugging-pipe',
'--disable-background-networking','--disable-component-update','--disable-sync','--disable-quic',
`--proxy-server=http://127.0.0.1:${proxyPort}`,'--proxy-bypass-list=<-loopback>',
'--force-webrtc-ip-handling-policy=disable_non_proxied_udp','--remote-debugging-pipe',
`--user-data-dir=${profile}`];
const child = spawn(legacy ? '/usr/bin/bwrap' : executable, legacy ? ['--die-with-parent','--bind','/','/','--dev-bind','/dev','/dev','--proc','/proc','--bind',nss,legacyNss,'--',executable,...chromeArgs] : chromeArgs,
{ env:{...process.env,XDG_DATA_HOME:xdg}, stdio: ['ignore','ignore','ignore','pipe','pipe'] });
@@ -66,17 +70,58 @@ async function chromium(evidence, ca) {
const { targetId } = await send('Target.createTarget', { url:'about:blank' });
const { sessionId } = await send('Target.attachToTarget', { targetId, flatten:true });
const call = (method, params) => send(method, params, sessionId);
return { call, send, on, child, async close() {
await send('Browser.close').catch(() => {});
if (child.exitCode === null) await once(child, 'exit');
let closing;
return { call, send, on, child, close() {
return closing ??= (async () => {
const exited=once(child,'close');
await send('Browser.close').catch(() => {});
if (child.exitCode === null && child.signalCode === null) await exited;
})();
} };
}
// External transport boundary: Chromium must use this proxy even for loopback.
// It never resolves/opens a socket for an unapproved destination. CDP is defense in depth.
async function originProxy(origin) {
const allowed=new URL(origin),sockets=new Set();
const track=socket=>{sockets.add(socket);socket.once('close',()=>sockets.delete(socket));return socket;};
const server=httpServer((req,res)=>{
let url;
try {url=testURL(req.url,origin);if(url.origin!==origin || allowed.protocol!=='http:')throw Error();}
catch {res.writeHead(403);res.end();return;}
const upstream=httpRequest({hostname:'127.0.0.1',port:Number(allowed.port),method:req.method,path:url.pathname+url.search,headers:req.headers},reply=>{
res.writeHead(reply.statusCode,reply.headers);reply.pipe(res);
});
upstream.on('socket',track);
upstream.on('error',()=>{if(!res.headersSent)res.writeHead(502);res.end();});
req.on('aborted',()=>upstream.destroy());req.pipe(upstream);
});
server.on('connection',track);
server.on('upgrade',(_req,socket)=>socket.destroy());
server.on('connect',(req,peer,head)=>{
// Only TLS to the selected HTTPS origin may tunnel. WS/other-origin tunnels refuse.
if(allowed.protocol!=='https:' || req.url!==allowed.host){peer.end('HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n');return;}
const upstream=track(createConnection({host:'127.0.0.1',port:Number(allowed.port)}));
upstream.once('connect',()=>{peer.write('HTTP/1.1 200 Connection Established\r\n\r\n');if(head.length)upstream.write(head);peer.pipe(upstream);upstream.pipe(peer);});
upstream.on('error',()=>peer.destroy());peer.on('error',()=>upstream.destroy());
peer.once('close',()=>upstream.destroy());upstream.once('close',()=>peer.destroy());
});
await new Promise((resolve,reject)=>{server.once('error',reject);server.listen(0,'127.0.0.1',resolve);});
let closing;
return {port:server.address().port,close(){return closing??=(async()=>{
const closed=new Promise(resolve=>server.close(resolve));for(const socket of sockets)socket.destroy();await closed;
})();}};
}
export async function daemon(evidence) {
privateDir(evidence);
const state = JSON.parse(readPrivate(join(evidence, 'browser.json'))), origin = testURL(state.origin).origin;
const b = await chromium(evidence, state.ca);
let blocked=false;
const proxy=await originProxy(origin);
let b;
try {
let sequence = 0, generation = 0, refs = new Map(), blocked = false, detail = {};
b = await chromium(evidence, state.ca,proxy.port);
const closeChrome=b.close;
b.close=async()=>{try{await closeChrome();}finally{await proxy.close();}};
let sequence = 0, generation = 0, refs = new Map(), detail = {};
const completed = new Map(), pendingDownloads = new Set();
const downloadsDir = join(evidence,'downloads'); privateDir(downloadsDir, true);
b.on('Browser.downloadWillBegin', e => { pendingDownloads.add(e.guid); });
@@ -99,6 +144,11 @@ export async function daemon(evidence) {
await b.send('Target.setAutoAttach', { autoAttach:true, waitForDebuggerOnStart:true, flatten:true });
await b.call('Page.enable'); await b.call('Accessibility.enable'); await b.call('DOM.enable');
await b.call('Network.enable'); await b.call('Network.setBlockedURLs', { urls:['ws://*','wss://*'] });
// Detection latches the session; the forced proxy enforces BEFORE outbound connection.
// Chromium's own background probes are also refused by the proxy, without poisoning UI actions.
b.on('Network.webSocketCreated',e=>{
try {testURL(e.url.replace(/^ws/,'http'),origin);} catch {blocked=true;}
});
await b.call('Fetch.enable', { patterns:[{ urlPattern:'*', requestStage:'Request' }] });
await b.send('Browser.setDownloadBehavior', { behavior:'allowAndName', downloadPath:downloadsDir, eventsEnabled:true });
await b.call('Emulation.setDeviceMetricsOverride', { width:1280, height:1000, deviceScaleFactor:1, mobile:false });
@@ -238,32 +288,41 @@ export async function daemon(evidence) {
const cleanup = async () => { server.close(); await b.close(); if (existsSync(socket)) unlinkSync(socket); };
for (const signal of ['SIGTERM','SIGINT']) process.once(signal, () => cleanup().finally(() => process.exit(0)));
return { server, cleanup };
} catch (e) { await b.close().catch(() => {}); throw e; }
} catch (e) { await b?.close().catch(() => {}); await proxy.close(); throw e; }
}
export function callBrowser(evidence, command, args=[]) {
export async function callBrowser(evidence, command, args=[]) {
privateDir(evidence); readPrivate(join(evidence,'browser.json'));
return new Promise((resolve,reject) => {
const value=await new Promise((resolve,reject) => {
const peer = createConnection(join(evidence,'browser.sock')); let data = '';
peer.on('error', () => {});
peer.setTimeout(150000, () => peer.destroy(new Error('browser action timed out; inspect receipts before retry')));
peer.on('error',reject); peer.on('connect', () => peer.write(JSON.stringify({ command,args })+'\n'));
peer.on('data',chunk => { data += chunk; }); peer.on('end', () => { try { const r = JSON.parse(data); r.ok ? resolve(r.value) : reject(new Error(r.error)); } catch(e) { reject(e); } });
});
if (command === 'stop' && ownedDaemons.has(evidence)) await ownedDaemons.get(evidence).exited;
return value;
}
export async function start({ evidence, origin, ca }) {
if (testURL(origin).origin !== origin) fail('origin must have no path, query or fragment');
plainPath(ca); readPrivate(ca); plainPath(evidence); privateDir(evidence,true);
exclusive(join(evidence,'browser.json'),JSON.stringify({ version:1,origin,ca })+'\n');
const child = fork(HERE,['--daemon',evidence],{ detached:true,stdio:['ignore','ignore','ignore','ipc'] });
const exited=new Promise(resolve=>child.once('exit',resolve));
ownedDaemons.set(evidence,{child,exited});
exited.then(()=>ownedDaemons.delete(evidence));
try {
await new Promise((resolve,reject) => {
const timer = setTimeout(() => { child.kill('SIGTERM'); reject(new Error('browser daemon startup timed out')); },20000);
child.once('error',reject); child.once('exit', () => { clearTimeout(timer); reject(new Error('browser daemon failed to start')); });
child.once('message',m => { clearTimeout(timer); m.ok ? resolve() : reject(new Error(m.error)); });
});
} catch (e) { child.kill('SIGTERM'); if (child.connected) child.disconnect(); child.unref(); throw e; }
} catch (e) { child.kill('SIGTERM'); if (child.connected) child.disconnect(); await exited; throw e; }
child.disconnect(); child.unref();
return callBrowser(evidence,'start');
try { return await callBrowser(evidence,'start'); }
catch(e) {
await callBrowser(evidence,'stop').catch(async()=>{child.kill('SIGTERM');await exited;});
throw e;
}
}
export async function main(argv, env=process.env) {
const command = argv.shift();
+63 -9
View File
@@ -1,12 +1,12 @@
import test from 'node:test';
import test,{after} from 'node:test';
import assert from 'node:assert/strict';
import { createServer } from 'node:http';
import { readFileSync, readdirSync, existsSync } from 'node:fs';
import { readFileSync, readdirSync, existsSync,writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { spawn } from 'node:child_process';
import { start,callBrowser,testURL } from './ui-browser.mjs';
import { prepare,serve } from './isolated-identity.mjs';
import { fixture } from './test-support.mjs';
import { fixture,closeServer,closeChild,ownedProcesses } from './test-support.mjs';
const pause=ms=>new Promise(r=>setTimeout(r,ms));
test('browser URL guard refuses live, remote, credentials, non-test ports and origin changes',()=>{
for(const u of ['http://127.0.0.1:7332','https://mosaic.woltje.com','http://10.1.1.53:17440','http://localhost:17440','http://u:[email protected]:17440','https://127.0.0.1:17440','http://127.0.0.1:17443'])assert.throws(()=>testURL(u));
@@ -23,9 +23,9 @@ async function setup(t,html) {
if(req.url==='/redirect'){res.writeHead(302,{location:'http://127.0.0.1:7332/live'});res.end();return;}
res.setHeader('content-type','text/html');res.end(html);
});
await new Promise((resolve,reject)=>{server.once('error',reject);server.listen(17440,'127.0.0.1',resolve);});t.after(()=>server.close());
await new Promise((resolve,reject)=>{server.once('error',reject);server.listen(17440,'127.0.0.1',resolve);});f.after(()=>closeServer(server));
const evidence=join(f.root,'browser');
t.after(async()=>{if(existsSync(join(evidence,'browser.sock')))await callBrowser(evidence,'stop').catch(()=>{});});
f.after(async()=>{if(existsSync(join(evidence,'browser.sock')))await callBrowser(evidence,'stop');});
return {...f,identity,evidence,hits};
}
test('real Chromium keeps a session across CLI processes; mouse/keyboard refs, screenshots and actual downloads',async t=>{
@@ -70,9 +70,10 @@ test('private CA browser performs the actual authorization redirect into product
const f=fixture(t),identity=join(f.root,'id'),evidence=join(f.root,'browser');
const output=prepare({instance:'ui797',config:f.config,owner:'ui797-owner',runtime:identity,tlsPort:17443});
const config=JSON.parse(readFileSync(f.config));config.console=output.console;writeFileSync(f.config,JSON.stringify(config));
const idp=await serve(identity);t.after(()=>{idp.closeAllConnections();idp.close();});
const idp=await serve(identity);f.after(()=>closeServer(idp));
const child=spawn(process.execPath,['packages/webui/src/cli.mjs','serve','--port','17440','--board','http://127.0.0.1:17439','--sso'],{env:{...process.env,MOSAIC_CONFIG:f.config,NODE_EXTRA_CA_CERTS:join(identity,'ca.pem')},stdio:['ignore','pipe','pipe']});let text='';child.stdout.on('data',c=>text+=c);child.stderr.on('data',c=>text+=c);
t.after(async()=>{if(existsSync(join(evidence,'browser.sock')))await callBrowser(evidence,'stop').catch(()=>{});if(child.exitCode===null){child.kill('SIGTERM');await new Promise(r=>child.once('exit',r));}});
f.after(()=>closeChild(child));
f.after(async()=>{if(existsSync(join(evidence,'browser.sock')))await callBrowser(evidence,'stop');});
await new Promise((resolve,reject)=>{const timer=setTimeout(()=>reject(new Error(text)),15000);child.stdout.on('data',()=>{if(text.includes('http://127.0.0.1:17440')){clearTimeout(timer);resolve();}});child.once('exit',()=>{clearTimeout(timer);reject(new Error(text));});});
const initial=await start({evidence,origin:'https://127.0.0.1:17443',ca:join(identity,'ca.pem')});
// The sole provider signs in immediately through redirects during start.
@@ -84,8 +85,61 @@ test('private CA browser performs the actual authorization redirect into product
test('a different private CA is refused by Chromium with TLS verification enabled',async t=>{
const f=fixture(t),g=fixture(t),id=join(f.root,'id'),other=join(g.root,'id'),evidence=join(f.root,'browser');
for(const [config,runtime] of [[f.config,id],[g.config,other]])prepare({instance:'ui797',config,owner:'ui797-owner',runtime,tlsPort:17443});
const server=await serve(other);t.after(()=>{server.closeAllConnections();server.close();});
t.after(async()=>{if(existsSync(join(evidence,'browser.sock')))await callBrowser(evidence,'stop');});
const server=await serve(other);f.after(()=>closeServer(server));
f.after(async()=>{if(existsSync(join(evidence,'browser.sock')))await callBrowser(evidence,'stop');});
await assert.rejects(start({evidence,origin:'https://127.0.0.1:17443',ca:join(id,'ca.pem')}),/navigation failed/);
assert.ok(readFileSync(join(evidence,'actions.jsonl'),'utf8').includes('false'));
});
for(const origin of ['http://127.0.0.1:17440','https://127.0.0.1:17443'])test(`Chromium ${origin} blocks WebSocket handshakes before the forbidden listener sees any TCP connection`,async t=>{
let connections=0,upgrades=0;
const outside=createServer((_req,res)=>res.end('forbidden'));
outside.on('connection',()=>connections++);
outside.on('upgrade',(_req,socket)=>{upgrades++;socket.destroy();});
await new Promise((resolve,reject)=>{outside.once('error',reject);outside.listen(17442,'127.0.0.1',resolve);});
t.after(()=>closeServer(outside));
const f=await setup(t,`<!doctype html><h1>safe</h1><button onclick="window.ws=new WebSocket('ws://127.0.0.1:17442/nope');window.wss=new WebSocket('wss://127.0.0.1:17442/nope')">WebSocket</button>`);
if(origin.startsWith('https:')) {const server=await serve(f.identity);f.after(()=>closeServer(server));}
await start({evidence:f.evidence,origin,ca:join(f.identity,'ca.pem')});
await assert.rejects(callBrowser(f.evidence,'click',['--text','WebSocket']),/blocked/);
await pause(300);
assert.equal(connections,0);assert.equal(upgrades,0);
await callBrowser(f.evidence,'stop');
});
test('a failing whole test file shuts down its browser and servers before removing fixture roots',async t=>{
const f=fixture(t),file=join(f.root,'failure.test.mjs');
const url=name=>JSON.stringify(new URL(name,import.meta.url).href);
writeFileSync(file,`import test from 'node:test';
import {createServer} from 'node:http';
import {join} from 'node:path';
import {fixture,closeServer} from ${url('./test-support.mjs')};
import {prepare} from ${url('./isolated-identity.mjs')};
import {start,callBrowser} from ${url('./ui-browser.mjs')};
test('intentional failure after browser startup',async t=>{
const f=fixture(t),identity=join(f.root,'id'),evidence=join(f.root,'browser');
console.log('FIXTURE_ROOT='+f.root);
prepare({instance:'ui797',config:f.config,owner:'ui797-owner',runtime:identity,tlsPort:17443});
const server=createServer((req,res)=>res.end('<h1>failure cleanup</h1>'));
await new Promise(r=>server.listen(17440,'127.0.0.1',r));f.after(()=>closeServer(server));
f.after(()=>callBrowser(evidence,'stop'));
await start({evidence,origin:'http://127.0.0.1:17440',ca:join(identity,'ca.pem')});
throw new Error('expected-cleanup-failure');
});`);
const child=spawn(process.execPath,['--test','--test-reporter=tap',file],{env:{...process.env,NODE_TEST_CONTEXT:undefined},stdio:['ignore','pipe','pipe']});
f.after(()=>closeChild(child));
let output='';child.stdout.on('data',c=>output+=c);child.stderr.on('data',c=>output+=c);
const code=await new Promise(resolve=>child.once('close',resolve));
assert.equal(code,1,output);assert.match(output,/expected-cleanup-failure/);
const root=output.match(/FIXTURE_ROOT=(\S+)/)?.[1];assert.ok(root,output);
assert.equal(existsSync(root),false,output);assert.deepEqual(ownedProcesses(root),[]);
});
after(async()=>{
const {createConnection}=await import('node:net');
for(const port of [17440,17442,17443]) await new Promise((resolve,reject)=>{
const socket=createConnection({host:'127.0.0.1',port});
socket.once('connect',()=>{socket.destroy();reject(new Error(`test listener survived on ${port}`));});
socket.once('error',e=>e.code==='ECONNREFUSED'?resolve():reject(e));
});
});