forked from mosaicstack/stack
fix(test): preserve sandbox refusal provenance
This commit is contained in:
@@ -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.
|
- 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.
|
- 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.
|
- `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. 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.
|
- 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. Pipelines #2179/#2180/#2181 exposed two runner refusal forms: namespace denial as `spawnSync bwrap` with `error.code=EPERM`, and a test image without Bubblewrap as `error.code=ENOENT`. The replay diagnostic now preserves spawn errors; the refusal detector recognizes only exact `spawnSync bwrap` provenance for `EPERM`/`EACCES`/`ENOENT`, plus Bubblewrap's known namespace-refusal text. Focused negative assertions reject both unrelated `spawnSync git EPERM` and verifier output that merely says `bwrap ENOENT`.
|
||||||
- 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.
|
- 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
|
## Documentation checklist
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ export async function replayCommit(root, commit) {
|
|||||||
if (install.status !== 0 || install.error || install.signal) {
|
if (install.status !== 0 || install.error || install.signal) {
|
||||||
return {
|
return {
|
||||||
...install,
|
...install,
|
||||||
stderr: `historical frozen dependency install failed: ${install.stderr || install.stdout || ''}`,
|
stderr: `historical frozen dependency install failed: ${install.error?.message || install.stderr || install.stdout || ''}`,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const authoritativeChanges = await authoritativeTreeChanges(
|
const authoritativeChanges = await authoritativeTreeChanges(
|
||||||
|
|||||||
@@ -17,10 +17,11 @@ const fixtureRoot = path.join(process.cwd(), '.mosaic-test-work', `gate-history-
|
|||||||
function sandboxUnavailable(result) {
|
function sandboxUnavailable(result) {
|
||||||
const detail = `${result.stdout ?? ''}${result.stderr ?? ''}${result.error?.message ?? ''}`;
|
const detail = `${result.stdout ?? ''}${result.stderr ?? ''}${result.error?.message ?? ''}`;
|
||||||
const bubblewrapSpawnDenied =
|
const bubblewrapSpawnDenied =
|
||||||
result.error?.code === 'EPERM' && /spawnSync bwrap/i.test(result.error?.message ?? '');
|
['EPERM', 'EACCES', 'ENOENT'].includes(result.error?.code) &&
|
||||||
|
/spawnSync bwrap/i.test(result.error?.message ?? '');
|
||||||
if (
|
if (
|
||||||
!bubblewrapSpawnDenied &&
|
!bubblewrapSpawnDenied &&
|
||||||
!/bwrap.*(?:EPERM|Operation not permitted|Creating new namespace failed)/i.test(detail)
|
!/bwrap:.*(?:Operation not permitted|Creating new namespace failed)/i.test(detail)
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -50,13 +51,15 @@ test.after(async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('sandbox refusal classification requires Bubblewrap provenance', () => {
|
test('sandbox refusal classification requires Bubblewrap provenance', () => {
|
||||||
assert.equal(
|
for (const code of ['EPERM', 'EACCES', 'ENOENT']) {
|
||||||
sandboxUnavailable({
|
assert.equal(
|
||||||
status: null,
|
sandboxUnavailable({
|
||||||
error: { code: 'EPERM', message: 'spawnSync bwrap EPERM' },
|
status: null,
|
||||||
}),
|
error: { code, message: `spawnSync bwrap ${code}` },
|
||||||
true,
|
}),
|
||||||
);
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
assert.equal(
|
assert.equal(
|
||||||
sandboxUnavailable({
|
sandboxUnavailable({
|
||||||
status: null,
|
status: null,
|
||||||
@@ -64,6 +67,10 @@ test('sandbox refusal classification requires Bubblewrap provenance', () => {
|
|||||||
}),
|
}),
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
assert.equal(
|
||||||
|
sandboxUnavailable({ status: 1, stderr: 'historical verifier said bwrap ENOENT' }),
|
||||||
|
false,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('prospective history reads each commit own manifest rather than the current tree', async () => {
|
test('prospective history reads each commit own manifest rather than the current tree', async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user