From abaed0c10317afe4aff6c7b4a2abd90a5e40c7ae Mon Sep 17 00:00:00 2001 From: coder-mos1 Date: Fri, 31 Jul 2026 22:57:47 -0500 Subject: [PATCH] test(ci): classify Bubblewrap EPERM exactly --- docs/scratchpads/1029-rm-02-gate-registry.md | 4 ++-- scripts/gate-history.test.mjs | 24 +++++++++++++++++++- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/docs/scratchpads/1029-rm-02-gate-registry.md b/docs/scratchpads/1029-rm-02-gate-registry.md index b71dc671..ab4d1441 100644 --- a/docs/scratchpads/1029-rm-02-gate-registry.md +++ b/docs/scratchpads/1029-rm-02-gate-registry.md @@ -39,7 +39,7 @@ Deliver the seven-gate registry and RED-first anti-inert verifier on `feat/rm-02 - CI wiring RED: package script and unconditional Woodpecker step tests both failed before wiring. - History RED: history test failed with missing module before own-tree manifest selection/provider classification was implemented. - `pnpm gate:verify`: exit 0; seven gates each reported `META-NEGATIVE-CONTROL ... observed red`; queue source/deployed drift control observed red; six queue behavior deltas printed as `DEFECT (owner: RM-03)`. -- Focused Node tests: 34/34 pass after review hardening (24 verifier/wiring plus 10 history/provider tests). +- Focused Node tests: 35/35 pass after review hardening (24 verifier/wiring plus 11 history/provider tests). - `pnpm typecheck`: pass (45/45 Turbo tasks). - `pnpm lint`: pass (25/25 Turbo tasks). - `pnpm format:check`: pass. @@ -61,7 +61,7 @@ The queue guard's `get_state_from_status_json` runs `python3 - <<'PY'` while pro - Initial PR pipeline #2177 exposed Woodpecker's shallow boundary: the activation parent object was present but marked shallow, so `merge-base --is-ancestor` correctly refused to infer ancestry. The unconditional gate step now unshallows before ancestry/provenance checks; its wiring test was observed RED before the CI fix. - Pipeline #2178 then proved the unprivileged Docker runner cannot establish Bubblewrap namespaces. A privileged experiment remained uncommitted and was rejected after Codex correctly rated it CRITICAL: PR-controlled code executes before an in-repository sandbox and could directly use the granted capability. - `mos-remediation` and `rev-974` independently ruled Option C. RM02-REQ-10 now retains its original text, restatement, and reason: PR CI verifies only the current tree, unprivileged and fail-closed; isolated own-tree replay is deferred to RM-60/#1031's external pre-execution authority, cross-referenced with RM-59. Future protected post-merge replay is detection with quarantine/revert, never pre-merge prevention. -- RED-first boundary test proved the old path executed an inert intermediate verifier. The revised path states adjacent `DOES`/`DOES NOT` claims, validates historical manifest provenance without executing it, and infers no replay success. Direct sandbox tests remain hard-fail; unprivileged CI asserts terminal refusal instead of treating replay as success. +- RED-first boundary test proved the old path executed an inert intermediate verifier. The revised path states adjacent `DOES`/`DOES NOT` claims, validates historical manifest provenance without executing it, and infers no replay success. Direct sandbox tests remain hard-fail; unprivileged CI asserts terminal refusal instead of treating replay as success. Pipeline #2179 showed this runner reports namespace denial as `spawnSync bwrap` with `error.code=EPERM`, `status=null`, and no stderr; the refusal detector now recognizes only that Bubblewrap-provenance hard-fail form and rejects unrelated command EPERM results. - Option C security review reported no findings. Code review rejected an initial unrelated typecheck binding for the new security criterion. It was replaced with a dedicated registered `privileged-pr-gate` case: the fixture injects a privilege key into the gate step, the wiring control rejects it for that exact reason, and `gate:verify` observes the boundary negative control. Follow-up hardening uses a closed exact gate-step construction, rejects privilege across the entire pipeline, rejects non-canonical/merged YAML keys, and pins the unrestricted PR/main trigger block; quoted/escaped/alias/merge/duplicate/filter bypass tests pass. Final Codex code review approved with no findings. ## Documentation checklist diff --git a/scripts/gate-history.test.mjs b/scripts/gate-history.test.mjs index 364eb3aa..87d11e83 100644 --- a/scripts/gate-history.test.mjs +++ b/scripts/gate-history.test.mjs @@ -16,7 +16,12 @@ const fixtureRoot = path.join(process.cwd(), '.mosaic-test-work', `gate-history- function sandboxUnavailable(result) { const detail = `${result.stdout ?? ''}${result.stderr ?? ''}${result.error?.message ?? ''}`; - if (!/bwrap:.*(?:Operation not permitted|Creating new namespace failed)/i.test(detail)) { + const bubblewrapSpawnDenied = + result.error?.code === 'EPERM' && /spawnSync bwrap/i.test(result.error?.message ?? ''); + if ( + !bubblewrapSpawnDenied && + !/bwrap.*(?:EPERM|Operation not permitted|Creating new namespace failed)/i.test(detail) + ) { return false; } assert.notEqual(result.status, 0, 'sandbox unavailability must remain terminal nonzero'); @@ -44,6 +49,23 @@ test.after(async () => { await rm(fixtureRoot, { recursive: true, force: true }); }); +test('sandbox refusal classification requires Bubblewrap provenance', () => { + assert.equal( + sandboxUnavailable({ + status: null, + error: { code: 'EPERM', message: 'spawnSync bwrap EPERM' }, + }), + true, + ); + assert.equal( + sandboxUnavailable({ + status: null, + error: { code: 'EPERM', message: 'spawnSync git EPERM' }, + }), + false, + ); +}); + test('prospective history reads each commit own manifest rather than the current tree', async () => { await rm(fixtureRoot, { recursive: true, force: true }); await mkdir(fixtureRoot, { recursive: true });