fix(quality): record current-tree trust boundary
ci/woodpecker/pr/ci Pipeline failed

This commit is contained in:
2026-07-31 22:41:30 -05:00
parent bd603da4b0
commit ada0cbe60e
12 changed files with 248 additions and 43 deletions
+16 -9
View File
@@ -275,9 +275,18 @@ export async function verifyHistory({ root, manifest }) {
return { failures, observations };
}
const onMain = isMainCommit(root, head);
// RM-02 execution boundary (RM-60, cross-reference RM-59), kept adjacent in both directions:
// DOES: run every registered current-tree gate and declared inerting mutation on PR CI,
// unprivileged and fail-closed.
// DOES NOT: execute a commit's own verifier in an isolated PR replay. PR-controlled code would
// otherwise need the namespace capability intended to contain that same code. That external
// trust boundary must be runner/provider-owned before any PR executable or config is evaluated.
observations.push(
`RM-02 EXECUTION BOUNDARY ${head}: DOES: verify the current tree and declared inerting mutations on every PR, unprivileged and fail-closed; DOES NOT: execute isolated per-commit verifier replay in repository-controlled CI; owner RM-60, cross-reference RM-59`,
);
if (!onMain) {
observations.push(
`PROVIDER ASSERTION DEFERRED ${head}: commit is not yet on main; prospective own-tree replay still runs, while retained merge evidence starts after merge`,
`PROVIDER ASSERTION DEFERRED ${head}: commit is not yet on main; retained provider evidence starts after merge and no replay success is inferred`,
);
}
@@ -302,20 +311,18 @@ export async function verifyHistory({ root, manifest }) {
);
continue;
}
const replay = await replayCommit(root, commit);
if (replay.status !== 0 || replay.error || replay.signal) {
failures.push(
`${commit}: own-tree gate replay failed with exit ${String(replay.status)}${replay.signal ? ` signal ${replay.signal}` : ''}${replay.error ? ` error ${replay.error.message}` : ''}: ${(replay.stderr || replay.stdout || '').trim().slice(0, 500)}`,
);
continue;
}
observations.push(`TREE REPLAY ${commit}: own-tree gate verifier exited 0`);
observations.push(
`INTERMEDIATE REPLAY DEFERRED ${commit}: isolated own-tree execution is not performed by repository-controlled CI; owner RM-60, cross-reference RM-59; no success is inferred`,
);
if (!onMain) {
observations.push(
`PROVIDER EVIDENCE ${commit}: DEFERRED until the commit is on main; no success is inferred`,
);
continue;
}
observations.push(
`POST-MERGE DETECTION BOUNDARY ${commit}: protected isolated replay awaits RM-60; when available, a failure requires quarantine/revert and is detection, not pre-merge prevention`,
);
if (evidence.state === 'terminal-failure') {
failures.push(
`${commit}: retained provider evidence is not terminal-success (${evidence.detail})`,
+26 -2
View File
@@ -14,6 +14,15 @@ import {
const fixtureRoot = path.join(process.cwd(), '.mosaic-test-work', `gate-history-${process.pid}`);
function sandboxUnavailable(result) {
const detail = `${result.stdout ?? ''}${result.stderr ?? ''}${result.error?.message ?? ''}`;
if (!/bwrap:.*(?:Operation not permitted|Creating new namespace failed)/i.test(detail)) {
return false;
}
assert.notEqual(result.status, 0, 'sandbox unavailability must remain terminal nonzero');
return true;
}
function git(root, ...args) {
const result = spawnSync('git', args, { cwd: root, encoding: 'utf8' });
assert.equal(result.status, 0, result.stderr);
@@ -79,6 +88,7 @@ test('historical replay executes each selected commit verifier from that commit
const inertResult = await replayCommit(root, inert);
const fixedResult = await replayCommit(root, fixed);
if (sandboxUnavailable(inertResult) || sandboxUnavailable(fixedResult)) return;
assert.notEqual(inertResult.status, 0);
assert.match(inertResult.stderr, /OLD TREE INERT/);
assert.equal(fixedResult.status, 0);
@@ -117,6 +127,7 @@ test('historical install lifecycle cannot replace an authoritative verifier', as
const result = await replayCommit(root, commit);
assert.notEqual(result.status, 0);
if (sandboxUnavailable(result)) return;
assert.match(result.stderr, /authoritative archived file changed.*scripts\/gate-verify\.mjs/i);
assert.doesNotMatch(result.stdout, /FORGED SUCCESS/);
});
@@ -141,6 +152,7 @@ test('historical verifier receives no current-process secret environment', async
process.env.REPLAY_SENTINEL = 'must-not-cross-boundary';
try {
const result = await replayCommit(root, commit);
if (sandboxUnavailable(result)) return;
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /SECRETLESS/);
assert.doesNotMatch(
@@ -174,6 +186,7 @@ test('historical replay cannot observe a sibling process in the runner PID names
git(root, 'commit', '-m', 'pid-isolated replay fixture');
const commit = git(root, 'rev-parse', 'HEAD');
const result = await replayCommit(root, commit);
if (sandboxUnavailable(result)) return;
assert.equal(result.status, 0, result.stderr);
assert.match(result.stdout, /PIDLESS/);
assert.doesNotMatch(`${result.stdout}${result.stderr}`, /HOST PID VISIBLE/);
@@ -182,7 +195,7 @@ test('historical replay cannot observe a sibling process in the runner PID names
}
});
test('feature-branch history replays an inert intermediate commit before the healthy head', async () => {
test('PR verification states the RM-60 boundary without executing an intermediate verifier', async () => {
const root = `${fixtureRoot}-feature`;
await rm(root, { recursive: true, force: true });
await mkdir(root, { recursive: true });
@@ -217,7 +230,18 @@ test('feature-branch history replays an inert intermediate commit before the hea
root,
manifest: { schemaVersion: 1, activationCommit: activation },
});
assert.ok(result.failures.some((failure) => /INTERMEDIATE INERT/.test(failure)));
assert.deepEqual(result.failures, []);
assert.ok(
result.observations.some(
(observation) => /DOES:.*current tree.*DOES NOT:.*isolated.*RM-60.*RM-59/i.test(observation),
),
);
assert.ok(
result.observations.some(
(observation) => /INTERMEDIATE REPLAY DEFERRED.*RM-60.*no success is inferred/i.test(observation),
),
);
assert.ok(result.observations.every((observation) => !/INTERMEDIATE INERT/.test(observation)));
} finally {
if (previousBranch === undefined) delete process.env.CI_COMMIT_BRANCH;
else process.env.CI_COMMIT_BRANCH = previousBranch;
+19
View File
@@ -151,6 +151,25 @@ async function applyFixture(root, fixture = {}) {
const target = await safeSandboxWrite(root, entry.path, entry.content, 'fixture write');
if (entry.mode !== undefined) await chmod(target, entry.mode);
}
for (const entry of fixture.replaceFiles ?? []) {
const target = await sandboxPath(root, entry.path, 'fixture replace');
if ((await lstat(target)).isSymbolicLink()) {
throw new Error(`fixture replace: target is a symbolic link (${entry.path})`);
}
const source = await readFile(target, 'utf8');
const occurrences = source.split(entry.find).length - 1;
if (occurrences !== 1) {
throw new Error(
`fixture replace: stale or ambiguous match for ${entry.path} (${occurrences} matches)`,
);
}
await safeSandboxWrite(
root,
entry.path,
source.replace(entry.find, entry.replace),
'fixture replace',
);
}
for (const relativePath of fixture.removePaths ?? []) {
await rm(await sandboxPath(root, relativePath, 'fixture remove'), {
recursive: true,
+91 -9
View File
@@ -3,20 +3,102 @@ import { readFile } from 'node:fs/promises';
import test from 'node:test';
const root = process.cwd();
const expectedTriggers = `when:
# PR + manual CI run on any branch — the pull_request pipeline is the merge gate.
# push CI is restricted to protected branches (main) so a feature-branch push no
# longer fires a redundant SECOND pipeline alongside its PR pipeline. This ~halves
# CI load on the storage-constrained runner with zero loss of gating (branch
# protection requires no push/ci status context; main still gets full push CI).
- event: [pull_request, manual]
- event: push
branch: main`;
const expectedGateStep = ` image: *node_image
# Woodpecker's shallow marker makes merge-base reject even present parents;
# full history is required for activation ancestry and manifest provenance.
commands:
- *enable_pnpm
- apk add --no-cache bubblewrap
- if [ -f .git/shallow ]; then git fetch --unshallow --no-tags origin; fi
- pnpm gate:verify
depends_on:
- install
- sanitization
- upgrade-guard`;
export function assertUnprivilegedGateStep(pipeline) {
assert.doesNotMatch(
pipeline,
/privileged/i,
'no pull-request pipeline step may declare privilege',
);
for (const line of pipeline.split('\n')) {
const candidate = line.trimStart().replace(/^-\s+/, '');
if (/^(?:["'!<].*|[A-Za-z_][A-Za-z0-9_-]*\s+):(?:\s|$)/.test(candidate)) {
assert.fail(`non-canonical or merged YAML key is forbidden: ${candidate}`);
}
}
const triggerMatches = [...pipeline.matchAll(/^when:\n([\s\S]*?)(?=\n\n)/gm)];
assert.equal(triggerMatches.length, 1, 'exactly one top-level trigger is required');
assert.equal(
`when:\n${triggerMatches[0][1].trimEnd()}`,
expectedTriggers,
'top-level triggers must match closed PR/main construction',
);
const matches = [
...pipeline.matchAll(/\n gate-verify:\n([\s\S]*?)(?=\n [a-z][a-z0-9-]+:|\nservices:|$)/g),
];
assert.equal(matches.length, 1, 'exactly one gate-verify step is required');
// Closed textual construction by design: accepting arbitrary YAML syntax here
// would require a duplicate-key-preserving parser. Exact equality rejects all
// extra keys, quoted/escaped key spellings, aliases, and mapping merges.
assert.equal(matches[0][1].trimEnd(), expectedGateStep, 'gate-verify step must match closed unprivileged construction');
}
test('package.json exposes the canonical gate:verify command', async () => {
const packageJson = JSON.parse(await readFile(`${root}/package.json`, 'utf8'));
assert.equal(packageJson.scripts['gate:verify'], 'node scripts/gate-verify.mjs');
});
test('Woodpecker runs gate verification on every pipeline without a path filter', async () => {
test('Woodpecker runs the closed unprivileged gate construction on every pipeline', async () => {
const pipeline = await readFile(`${root}/.woodpecker/ci.yml`, 'utf8');
assert.match(pipeline, /\n gate-verify:\n/);
const step =
pipeline.match(/\n gate-verify:\n([\s\S]*?)(?=\n [a-z][a-z0-9-]+:|\nservices:)/)?.[1] ?? '';
assert.match(
step,
/commands:\n - \*enable_pnpm\n - apk add --no-cache bubblewrap\n - if \[ -f \.git\/shallow \]; then git fetch --unshallow --no-tags origin; fi\n - pnpm gate:verify\n/,
);
assert.doesNotMatch(step, /\bwhen:|\bpath:/);
assertUnprivilegedGateStep(pipeline);
});
test('gate wiring rejects privilege syntax, merges, duplicate keys, and trigger narrowing', async () => {
const pipeline = await readFile(`${root}/.woodpecker/ci.yml`, 'utf8');
const additions = [
' privileged: *enabled\n',
' "privileged": true\n',
" 'privileged': true\n",
' privileged : true\n',
' "priv\\u0069leged": true\n',
' <<: *privileged-step\n',
' "<<": *privileged-step\n',
];
for (const addition of additions) {
const changed = pipeline.replace(' gate-verify:\n image:', ` gate-verify:\n${addition} image:`);
assert.throws(() => assertUnprivilegedGateStep(changed));
}
const privilegedInstall = pipeline.replace(
' install:\n image:',
' install:\n privileged: true\n image:',
);
const duplicate = `${pipeline}\n gate-verify:\n image: *node_image\n`;
const noPullRequest = pipeline.replace(
' - event: [pull_request, manual]',
' - event: manual',
);
const filteredPullRequest = pipeline.replace(
' - event: [pull_request, manual]',
' - event: [pull_request, manual]\n path: [scripts/**]',
);
assert.throws(() => assertUnprivilegedGateStep(privilegedInstall), /privilege/i);
assert.throws(() => assertUnprivilegedGateStep(duplicate), /exactly one gate-verify/);
assert.throws(() => assertUnprivilegedGateStep(noPullRequest), /closed PR\/main construction/);
assert.throws(
() => assertUnprivilegedGateStep(filteredPullRequest),
/closed PR\/main construction/,
);
});