diff --git a/BUILD-LOG.md b/BUILD-LOG.md index 05b94ecfe..f0ab0c33f 100644 --- a/BUILD-LOG.md +++ b/BUILD-LOG.md @@ -6202,3 +6202,5 @@ After: personally authored three-file skills/ms-ux-improvement coordinator packa 2026-11-06 | #797 ui797 support R3 P1 corrections verified | Before: 19e6cb0f1 still emitted WebRTC STUN/TURN UDP despite its flag, and unavailable ledger directories bypassed halt and terminated serve. After: mandatory Bubblewrap network/PID namespaces with an isolated, up loopback; a single namespace TCP relay over a 0600 Unix socket to the existing origin-only proxy, preserving private NSS/TLS validation, control socket permissions and awaited browser-before-proxy cleanup. Real Chromium regressions from both origins create nine ICE agents against six independent IPv4/IPv6/127.0.0.2 STUN/TURN UDP listeners and three TCP listeners; zero datagrams/connections, existing WebSocket/HTTP/SSO/CA negatives retained. Campaign initialization now exclusively pins owner/config/binding/ledger evidence beside the campaign, independently of the ledger volume; missing/renamed/0500 storage still attempts supported CLI halt, explicitly reports nondurable latch/receipt, preserves evidence and never recreates an epoch. Serve survives failures and reasserts each tick/Resume, retaining its stop across storage recovery; invalid/foreign/changed targets refuse. Older intact ledger format preserved. Final sequential whole files: identity 8, browser 10, budget 43 = 61 pass / 0 fail / 0 skip / 0 cancelled. All R1/R2 probes rerun: zero forbidden TCP/UDP, all four storage variants count one halt, original latch preserved, stale-lock/corruption/cap/cutoff/identity negatives retained, SIGTERM/SIGKILL/startup-failure survivors zero. Original R1 general probe exits 1 at its correctly refused split-route setup. Independent final sweep: zero owned processes/listeners at fixed and 43 recorded endpoints; scratch sweep parser corrected before its terminal run. Exactly packages/cli/live plus this single entry and one SESSIONS line; linked-worktree queue refused, no queue/forge writes, push, stash, model call, .53 contact, host network/DNS/trust changes or unrelated-session cleanup. Local implementation ready for independent exact-head review; owner-service successful quiescence remains a later acceptance gate. Evidence/report: /mnt/storage/scratch/mosaic-lead/mvp3/ui797-support-fix-r3-evidence/ and ui797-support-fix-r3-report.md. 2026-11-06 UTC | #797 ui797 support R4 review corrections verified | Before: unchanged 905d313cd reproduces 13 host pathname D-Bus connections/11 AUTH EXTERNAL requests, four damaged-sidecar cases with zero halt attempts, and a coherent epoch edit accepting 4 of 8 real stored tokens against cap 5. After: empty Bubblewrap mount view with read-only system/browser dependencies, minimal dev/proc, private tmpfs tmp/run/home, browser-owned writable evidence/NSS mounts and explicit environment; actual Chromium D-Bus traffic zero on HTTP/HTTPS, diagnostic pathname/abstract/TCP attempts fail, UDP listener sees zero, synthetic 0600 file/config/runtime paths hidden. Private NSS trust and unchanged production Console OIDC sign-in pass. Version-2 ledger independently pins config bytes; sidecar read/parse/mode/deletion and semantic corruption stop with explicit evidenceErrors and counted supported halts, preserving damage. Serve retains verified target/config/epoch evidence and revalidates marker/root/store/config before targeting and immediately before halt. Normal-path independent campaign hashes/epochs must agree; real coherent-epoch-edit and zero pin hash halt without journal reset. Modern sidecar deletion stops; legacy version-1 requires intact independent config-byte proof and an unpinned legacy ledger refuses, without migration/reinitialization. README requires Bubblewrap/user/network/PID namespaces on every browser host and states identity is HTTPS 17443/oidc behind the Console TLS forwarder, never 17442. Sequential whole files budget 89, identity 8, browser 13, request-lifecycle-live 2 = 112 pass / 0 fail / 0 skip / 0 cancelled. Both unchanged reviewer probes first fail semantically on baseline, then all six controls pass; three further retained probes pass, 31 network records/12 denied CONNECTs and both WebRTC origins retain zero forbidden traffic. External-reader positive control, changed-path 64-hex pin search, ten syntax checks and whitespace pass. Development missing import and explicit PWD assertion corrected; evidence retained. Cleanup finds zero processes from this run, no fixed test listeners and empty/removed TMPDIR; 29 older other-session daemons untouched after overbroad sweep was narrowed to this run. Five packages/cli/live paths plus this entry and one SESSIONS line only; original log bytes preserved. Queue read refused linked worktree; no queue/forge writes, push, stash, model spend, real secrets, .53/live-service/host-trust changes or attribution trailers. Local implementation ready for independent exact-commit review; owner-service quiescence remains outside this author charter. Report/evidence: /mnt/storage/scratch/mosaic-lead/mvp3/ui797-support-fix-r4-report.md and ui797-support-fix-r4-evidence/. + +2026-11-06 UTC | #797 ui797 support R5 conflicting-ledger correction verified | Before: fce831c05 let a valid pin and a v2 ledger disagree on target and still halted the ledger's binding; the reviewer's unchanged target-conflict probe sent 8 halts to the wrong business/config (business-only corruption and a foreign ledger, check and serve startup). After: checkTarget runs corroborate() on every tick. A ledger target directs a halt only when the owner campaign, the owner pin, or the live binding of the owner campaign names the same target; otherwise the owner pin replaces it only when it matches the owner campaign hash, the live config/marker/root/store and the live config digest (evidence 'ledger-target-changed'); anything else refuses with campaign-instance-changed. Retained-target comparison stays as the second guard. Four new regressions (check, serve startup, running service, no corroboration) fail 4/4 on r4 source and pass after. Probe after fix: 0 wrong halts in all six cases. Sequential whole files budget 93, identity 8, browser 13, request-lifecycle-live 2 = 116 pass / 0 fail / 0 skip. No push, stash, .53, live service or model spend. Evidence: /mnt/storage/scratch/mosaic-lead/mvp3/ui797-support-r5-fix-evidence/. diff --git a/docs/SESSIONS.md b/docs/SESSIONS.md index a88e707c0..38c7e42cd 100644 --- a/docs/SESSIONS.md +++ b/docs/SESSIONS.md @@ -2069,3 +2069,4 @@ are never rewritten or removed; corrections are new entries. 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. 2026-11-06 | ui797-support-fix-r3 implementation sub-agent | #797 two P1 corrections above 19e6cb0f1 on feat/ui797-support | Mandatory browser network namespace/private proxy relay and independently bound unavailable-ledger halt with persistent serve reassertion; final sequential whole-file suites 61 pass, 0 fail/skip/cancel; all R1/R2 probes rerun, zero forbidden TCP/UDP, storage halts counted and latch preserved, zero owned processes/listeners. One BUILD-LOG entry and packages/cli/live changes only; local commit authorized, independent review pending. Queue refused linked worktree; no push/stash/forge/model/.53/host network-DNS-trust or unrelated-session action. Report: /mnt/storage/scratch/mosaic-lead/mvp3/ui797-support-fix-r3-report.md. 2026-11-06 UTC | ui797-support-fix-r4 implementation sub-agent | #797 review corrections on feat/ui797-support above 905d313cd | Minimal browser mount/environment boundary; independent config-byte proof and retained binding keep halt on sidecar damage; normal-path campaign hash/epoch agreement; unconditional namespaces and HTTPS 17443/oidc docs. Baseline failures retained; six controls corrected; whole files 112 pass, 0 fail/skip/cancel; five retained probes exit 0, zero forbidden traffic, no owned survivors/listeners, TMPDIR removed. Five support paths, one appended BUILD entry and this line; prefix bytes verified. Queue refused linked worktree; no queue/forge/push/stash/model/secrets/.53/live or unrelated-session action. Local author candidate for independent review. Report: /mnt/storage/scratch/mosaic-lead/mvp3/ui797-support-fix-r4-report.md. +2026-11-06 UTC | MVP3 lead #797 (Opus) | #797 support R4 P1 fix on feat/ui797-support above fce831c05 | Conflicting ledger targets now need owner corroboration or are replaced by the owner-verified pin, else refused; 4 regressions fail on r4 and pass after; whole files 116 pass/0 fail; reviewer probe 0 wrong halts. Local commit; independent r5 review next. No push/stash/.53/live action. diff --git a/packages/cli/live/budget-supervisor.mjs b/packages/cli/live/budget-supervisor.mjs index 58512b3db..89111d8d7 100644 --- a/packages/cli/live/budget-supervisor.mjs +++ b/packages/cli/live/budget-supervisor.mjs @@ -137,6 +137,23 @@ function loadLedger(ledger) { (p.version===2 && !/^[0-9a-f]{64}$/.test(p.configHash))) fail('ledger-manifest-invalid'); return p; } +// A ledger's target may direct a halt only when independent evidence names the +// same target: the owner campaign itself, the owner's pin, or the instance the +// current owner campaign resolves to. A conflicting ledger never authorizes its +// own target. The owner-verified pin replaces it only when the pin agrees with +// the owner campaign and the live config/marker/root/store; anything else refuses. +function corroborate(p,pin,{campaign,ledger}) { + const same=(a,b)=>JSON.stringify(a)===JSON.stringify(b); + let owner=null, live=null, liveHash=null; + try { owner=campaignAt(campaign); live=binding(owner.c); liveHash=digest(readFileSync(owner.c.config,'utf8')); } catch { /* No owner evidence. */ } + if(owner && owner.hash===p.hash && same(owner.c,p.campaign)) return p; + if(pin && pin.ledger===ledger && same(pin.binding,p.binding) && pin.configHash===p.configHash) return p; + if(live && same(live,p.binding)) return p; + if(owner && pin && pin.ledger===ledger && pin.hash===owner.hash && (!pin.campaign || same(pin.campaign,owner.c)) && + same(pin.binding,live) && pin.configHash===liveHash) + return {campaign:owner.c,hash:pin.hash,configHash:pin.configHash,binding:pin.binding}; + fail('campaign-instance-changed'); +} function checkTarget({campaign,ledger}, retained=null) { let p, pin, reason=null, writable=true; const evidenceErrors=[]; @@ -159,6 +176,10 @@ function checkTarget({campaign,ledger}, retained=null) { p={campaign:pin.campaign??c,hash:pin.hash,configHash:pin.configHash,binding:pin.binding}; } } + if(p.configHash) { + const resolved=corroborate(p,pin,{campaign,ledger}); + if(resolved!==p) { p=resolved; evidenceErrors.push('ledger-target-changed'); } + } const c=p.campaign; // 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); diff --git a/packages/cli/live/budget-supervisor.test.mjs b/packages/cli/live/budget-supervisor.test.mjs index 2946cc08f..1ce037b8d 100644 --- a/packages/cli/live/budget-supervisor.test.mjs +++ b/packages/cli/live/budget-supervisor.test.mjs @@ -448,3 +448,50 @@ test('semantic sidecar target damage stops using independent config/marker/root/ assert.equal(f.halts.length,1);assert.equal(f.halts[0].business,f.c.business);assert.equal(readFileSync(path,'utf8'),damaged); } }); + +// A conflicting ledger never authorizes its own target (support r4 P1). +function conflictingLedger(t,kind) { + const f=setup(t,100),path=join(f.ledger,'campaign.json'); + const g=kind==='foreign'?setup(t,100,{business:'unrelated-business'}):null; + const alter=()=>{ + if(g) { writeFileSync(path,readFileSync(join(g.ledger,'campaign.json'))); return; } + const p=JSON.parse(readFileSync(path));p.binding.business='unrelated-business';p.campaign.business='unrelated-business';writeFileSync(path,JSON.stringify(p)); + }; + const wrong=()=>f.halts.filter(b=>b.business!==f.c.business||b.config!==f.config||b.dataRoot!==f.dataRoot); + return {f,g,alter,wrong}; +} +test('a business-only or foreign ledger is replaced by the owner-verified target in check',async t=>{ + for(const kind of ['business-only','foreign']) { + const {f,alter,wrong}=conflictingLedger(t,kind);alter(); + const result=await check(f.opts,{now:()=>NOW,read:()=>[row()],halt:f.halt}); + assert.equal(result.stopped,true);assert.ok(result.evidenceErrors.includes('ledger-target-changed')); + assert.equal(f.halts.length,1);assert.deepEqual(wrong(),[]); + } +}); +test('serve startup never retains a conflicting ledger target',async t=>{ + for(const kind of ['business-only','foreign']) { + const {f,alter,wrong}=conflictingLedger(t,kind),controller=new AbortController(),reports=[];alter(); + await serveBudget(f.opts,{interval:1,signal:controller.signal,now:()=>NOW,read:()=>[row()],halt:f.halt,report:r=>{reports.push(r);if(reports.length===3)controller.abort();}}); + assert.equal(reports.length,3);assert.ok(f.halts.length>=1);assert.deepEqual(wrong(),[]); + for(const r of reports) assert.ok(r.evidenceErrors.includes('ledger-target-changed')); + } +}); +test('a running service halts only its retained owner target when the ledger changes under it',async t=>{ + for(const kind of ['business-only','foreign']) { + const {f,alter,wrong}=conflictingLedger(t,kind),controller=new AbortController(),reports=[]; + await serveBudget(f.opts,{interval:1,signal:controller.signal,now:()=>NOW,read:()=>[row()],halt:f.halt,report:r=>{ + reports.push(r);if(reports.length===1)alter();if(reports.length===3)controller.abort(); + }}); + assert.equal(reports[0].stopped,false); + for(const r of reports.slice(1)) { assert.equal(r.stopped,true);assert.ok(r.evidenceErrors.includes('ledger-target-changed')); } + assert.ok(f.halts.length>=1);assert.deepEqual(wrong(),[]); + } +}); +test('a conflicting ledger without corroborating owner evidence refuses every halt',async t=>{ + for(const kind of ['business-only','foreign']) { + const {f,alter}=conflictingLedger(t,kind);alter(); + const pin=f.campaign+'.binding.json',damaged=JSON.parse(readFileSync(pin));damaged.configHash='0'.repeat(64);writeFileSync(pin,JSON.stringify(damaged)); + await assert.rejects(check(f.opts,{now:()=>NOW,read:()=>[row()],halt:f.halt}),/campaign-instance-changed/); + assert.equal(f.halts.length,0); + } +});