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 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-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-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 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-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. session/owner rules. A missing bus, business or ancestry proof still refuses.
No human/Console binding shortcut or TLS-verification disable flag is used. 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. 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. Certificates expire after two days; do not overwrite identity material on restart.
### Step-by-step real browser CLI for tester agents ### 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 `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 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 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. `stop`. The driver never contacts 7332 or mosaic.woltje.com.
The browser trusts the test CA through a private NSS database under its evidence 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", "start": "2026-11-06T02:00:00.000Z",
"end": "2026-11-06T03:00:00.000Z", "end": "2026-11-06T03:00:00.000Z",
"caps": [ "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-opus-5-5","tokens":10000000},
{"provider":"proxy","route":"anthropic","model":"claude-sonnet-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 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 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 The test bus store must exist before initialization. Run the supervisor outside
model-writable settings/data in a normally supervised owner service: 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 Each provider/route/model has a separate append-only ledger, durable across
rounds/restarts. Campaign bytes, root identity and store identity are pinned; rounds/restarts. Campaign bytes, root identity and store identity are pinned;
changed caps/epoch, rewritten/disappeared usage and damaged ledger files stop. 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 Initialization is exclusive. There is no reset, raise or resume command. A stale
`check.lock` after interruption requires owner diagnosis; never auto-reclaim it. `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. A changed target config refuses rather than halting an unverified data root.
At any cap, deadline or accounting refusal, the supervisor fsyncs `stop.json` 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 { spawnSync } from 'node:child_process';
import { turnRecords } from '../../bus/src/usage.mjs'; import { turnRecords } from '../../bus/src/usage.mjs';
import { exclusive, fail, isMain, options, outsideRuntime, plainPath, privateDir, readPrivate, ROOT, testInstance } from './support-common.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 kinds = ['input','output','cacheWrite','cacheRead'];
const digest = x => createHash('sha256').update(typeof x === 'string' ? x : JSON.stringify(x)).digest('hex'); 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]); 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) { function campaignAt(path) {
privateDir(dirname(path)); privateDir(dirname(path));
const text = readPrivate(path), c = JSON.parse(text); 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'); !Array.isArray(c.caps) || !c.caps.length || c.caps.length > 20) fail('campaign-invalid');
const keys = new Set(); const keys = new Set();
for (const cap of c.caps) { for (const cap of c.caps) {
if (Object.keys(cap).sort().join(',') !== 'model,provider,route,tokens' || !['proxy','anthropic','zai','openai'].includes(cap.provider) || 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 ?? '') || !['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'); !approvedBinding(cap) || !Number.isSafeInteger(cap.tokens) || cap.tokens < 1 || cap.tokens > ceiling(cap) || keys.has(modelIdentity(cap))) fail('campaign-cap-invalid');
keys.add(identity(cap)); keys.add(modelIdentity(cap));
} }
return { c, hash:digest(text) };
} }
function binding(c) { function binding(c) {
const { system } = testInstance(c.instance,c.config); 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'); if (p.version !== 1 || !p.binding || !p.campaign) fail('ledger-manifest-invalid');
return p; 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; let p, integrityFailure=false;
try { p=loadLedger(ledger); } catch { try { p=loadLedger(ledger); } catch {
// With a damaged manifest, only the owner campaign and validated TEST marker may identify // 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. // 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); checkBinding(p.binding,c); outsideRuntime(ledger,{dataRoot:p.binding.dataRoot},c.config);
outsideRuntime(campaign,{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 { 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'); const stopFile=join(ledger,'stop.json');
if (existsSync(stopFile)) { if (existsSync(stopFile)) {
reason='stop-already-latched'; reason='stop-already-latched';
try { JSON.parse(readPrivate(stopFile)); } catch { reason='stop-latch-invalid'; } try { JSON.parse(readPrivate(stopFile)); } catch { reason='stop-latch-invalid'; }
} }
try { try {
if (campaignAt(campaign).hash !== p.hash) fail('campaign-changed'); const clock=now(); if (!Number.isFinite(clock)) fail('campaign-clock-invalid');
const clock=now(); if (clock < Date.parse(c.start)) fail('campaign-not-started'); if (clock>=Date.parse(c.end)) fail('campaign-ended'); 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) { if (!reason) {
const records=await read({...p.binding,start:c.start,end:c.end}), observed=new Set(); 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) { 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'); 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'); 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 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); const hash=digest(r), previous=a.entries.get(key);
if (previous && previous.digest !== hash) fail('usage-rewritten'); 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) { if (!previous) {
const e={key,digest:hash,tokens}; writeJournal(join(ledger,filename(a.cap)),e); a.entries.set(key,e); a.total+=tokens; const e={key,digest:hash,tokens}; writeJournal(join(ledger,filename(a.cap)),e); a.entries.set(key,e);
if (!Number.isSafeInteger(a.total)) fail('ledger-overflow');
} }
} }
for (const a of accounts) for (const key of a.entries.keys()) if (!observed.has(key)) fail('usage-disappeared'); 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})); 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'; } } catch(e) { reason=/^[a-z-]{1,80}$/.test(e.message)?e.message:'accounting-failed'; }
if (reason) { 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:true,reason,totals,halt:result};
} }
return {stopped:false,totals}; return {stopped:false,totals};
} finally { rmdirSync(lock); } } finally { if (locked) rmdirSync(lock); }
} }
export function status({ campaign,ledger }) { 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); 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})); 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 }; 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 test from 'node:test';
import assert from 'node:assert/strict'; 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 { 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 { fixture } from './test-support.mjs';
import { Store } from '../../bus/src/store.mjs'; import { Store } from '../../bus/src/store.mjs';
const NOW=Date.parse('2026-11-06T02:30:00.000Z'); 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}); 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) { function setup(t,tokens=5,changes={}) {
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.root,'campaign.json'),ledger=join(f.root,'ledger'); 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}]}; 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}); writeFileSync(campaign,JSON.stringify(c),{mode:0o600});initialize({campaign,ledger});
const halts=[],halt=b=>{halts.push(b);return {halted:true,quiescent:true,generation:'test'};}; 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}}; 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); 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=>{ 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}); 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/); 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/); 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'); 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 { createHash, verify, createPublicKey } from 'node:crypto';
import { request } from 'node:https'; import { request } from 'node:https';
import { createServer } from 'node:http'; 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 { join } from 'node:path';
import { spawn } from 'node:child_process'; import { spawn } from 'node:child_process';
import { once } from 'node:events';
import { prepare,serve,SUBJECT } from './isolated-identity.mjs'; 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'; import { readSlot } from '../../webui/src/sso.mjs';
function setup(t) { function setup(t) {
const f=fixture(t),runtime=join(f.root,'identity'); 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)); assert.throws(()=>prepare(defaults));
}); });
test('real HTTPS authorization redirects, PKCE and nonce bind a single-use bounded RS256 token',async t=>{ 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 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 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'}); 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=>{ test('fixed TLS forwarder preserves normal Console Host, cookies and origin without injected identity',async t=>{
const f=setup(t);let headers; const f=setup(t);let headers;
const upstream=createServer((req,res)=>{headers=req.headers;res.end('Console upstream');}); 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()); 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);t.after(()=>{server.closeAllConnections();server.close();}); 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'}}); 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); 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=>{ 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 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']}); 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); 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));});}); 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'); 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=>{ test('authorization codes expire, and tampering with the single subject refuses service',async t=>{
const f=setup(t);let now=Date.now(); 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 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 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; 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)); 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/); 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; return out;
} }
export function testInstance(instance, config) { 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); plainPath(config);
const copies = join(process.env.XDG_DATA_HOME || join(homedir(), '.local/share'), 'mosaic-stack/copies'); 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'); 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. // 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 { tmpdir } from 'node:os';
import { join } from 'node:path'; import { join } from 'node:path';
export function fixture(t) { 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'); 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-')); 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'); const configDir=join(root,'mosaic-ui797'),dataRoot=join(root,'data');
mkdirSync(configDir,{mode:0o700});mkdirSync(dataRoot,{mode:0o700}); mkdirSync(configDir,{mode:0o700});mkdirSync(dataRoot,{mode:0o700});
const config=join(configDir,'config.json'); 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(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'})); 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. // Persistent CDP pipe client, based on webui/tests/browser.mjs. No injected app script.
import { spawn, fork, spawnSync } from 'node:child_process'; import { spawn, fork, spawnSync } from 'node:child_process';
import { createServer, createConnection } from 'node:net'; import { createServer, createConnection } from 'node:net';
import {createServer as httpServer,request as httpRequest} from 'node:http';
import { once } from 'node:events'; import { once } from 'node:events';
import { createHash, X509Certificate } from 'node:crypto'; import { createHash, X509Certificate } from 'node:crypto';
import { appendFileSync, chmodSync, existsSync, readdirSync, readFileSync, unlinkSync } from 'node:fs'; 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 { fileURLToPath } from 'node:url';
import { homedir } from 'node:os'; import { homedir } from 'node:os';
import { exclusive, fail, isMain, options, plainPath, privateDir, readPrivate } from './support-common.mjs'; import { exclusive, fail, isMain, options, plainPath, privateDir, readPrivate } from './support-common.mjs';
const ownedDaemons = new Map();
const HERE = fileURLToPath(import.meta.url); const HERE = fileURLToPath(import.meta.url);
const INTERACTIVE = new Set(['button','link','textbox','searchbox','combobox','checkbox','radio','switch','tab','menuitem','option','spinbutton','slider']); 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)); 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'); if (origin && u.origin !== origin) fail('navigation must stay on the selected test origin');
return u; return u;
} }
async function chromium(evidence, ca) { async function chromium(evidence, ca, proxyPort) {
const cert = new X509Certificate(readPrivate(ca)); 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'); 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); const xdg = join(evidence, 'xdg'); privateDir(xdg, true);
@@ -39,7 +41,9 @@ async function chromium(evidence, ca) {
if (legacy) plainPath(legacyNss); if (legacy) plainPath(legacyNss);
const executable = process.env.CHROMIUM || '/usr/bin/chromium'; const executable = process.env.CHROMIUM || '/usr/bin/chromium';
const chromeArgs = ['--headless','--no-first-run','--no-default-browser-check','--disable-dev-shm-usage', 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}`]; `--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, 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'] }); { 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 { targetId } = await send('Target.createTarget', { url:'about:blank' });
const { sessionId } = await send('Target.attachToTarget', { targetId, flatten:true }); const { sessionId } = await send('Target.attachToTarget', { targetId, flatten:true });
const call = (method, params) => send(method, params, sessionId); const call = (method, params) => send(method, params, sessionId);
return { call, send, on, child, async close() { let closing;
await send('Browser.close').catch(() => {}); return { call, send, on, child, close() {
if (child.exitCode === null) await once(child, 'exit'); 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) { export async function daemon(evidence) {
privateDir(evidence); privateDir(evidence);
const state = JSON.parse(readPrivate(join(evidence, 'browser.json'))), origin = testURL(state.origin).origin; 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 { 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 completed = new Map(), pendingDownloads = new Set();
const downloadsDir = join(evidence,'downloads'); privateDir(downloadsDir, true); const downloadsDir = join(evidence,'downloads'); privateDir(downloadsDir, true);
b.on('Browser.downloadWillBegin', e => { pendingDownloads.add(e.guid); }); 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.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('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://*'] }); 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.call('Fetch.enable', { patterns:[{ urlPattern:'*', requestStage:'Request' }] });
await b.send('Browser.setDownloadBehavior', { behavior:'allowAndName', downloadPath:downloadsDir, eventsEnabled:true }); await b.send('Browser.setDownloadBehavior', { behavior:'allowAndName', downloadPath:downloadsDir, eventsEnabled:true });
await b.call('Emulation.setDeviceMetricsOverride', { width:1280, height:1000, deviceScaleFactor:1, mobile:false }); 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); }; 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))); for (const signal of ['SIGTERM','SIGINT']) process.once(signal, () => cleanup().finally(() => process.exit(0)));
return { server, cleanup }; 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')); 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 = ''; const peer = createConnection(join(evidence,'browser.sock')); let data = '';
peer.on('error', () => {}); peer.on('error', () => {});
peer.setTimeout(150000, () => peer.destroy(new Error('browser action timed out; inspect receipts before retry'))); 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('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); } }); 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 }) { export async function start({ evidence, origin, ca }) {
if (testURL(origin).origin !== origin) fail('origin must have no path, query or fragment'); if (testURL(origin).origin !== origin) fail('origin must have no path, query or fragment');
plainPath(ca); readPrivate(ca); plainPath(evidence); privateDir(evidence,true); plainPath(ca); readPrivate(ca); plainPath(evidence); privateDir(evidence,true);
exclusive(join(evidence,'browser.json'),JSON.stringify({ version:1,origin,ca })+'\n'); 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 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 { try {
await new Promise((resolve,reject) => { await new Promise((resolve,reject) => {
const timer = setTimeout(() => { child.kill('SIGTERM'); reject(new Error('browser daemon startup timed out')); },20000); 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('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)); }); 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(); 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) { export async function main(argv, env=process.env) {
const command = argv.shift(); 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 assert from 'node:assert/strict';
import { createServer } from 'node:http'; 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 { join } from 'node:path';
import { spawn } from 'node:child_process'; import { spawn } from 'node:child_process';
import { start,callBrowser,testURL } from './ui-browser.mjs'; import { start,callBrowser,testURL } from './ui-browser.mjs';
import { prepare,serve } from './isolated-identity.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)); const pause=ms=>new Promise(r=>setTimeout(r,ms));
test('browser URL guard refuses live, remote, credentials, non-test ports and origin changes',()=>{ 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)); 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;} 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); 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'); 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}; return {...f,identity,evidence,hits};
} }
test('real Chromium keeps a session across CLI processes; mouse/keyboard refs, screenshots and actual downloads',async t=>{ 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 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 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 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); 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));});}); 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')}); 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. // 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=>{ 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'); 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}); 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();}); const server=await serve(other);f.after(()=>closeServer(server));
t.after(async()=>{if(existsSync(join(evidence,'browser.sock')))await callBrowser(evidence,'stop');}); 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/); 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')); 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));
});
});