harness-everything 0.36.2 → 0.36.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +17 -0
- package/ci/fixtures/doc-obligations/read-boundary/boundary-controls.json +211 -0
- package/ci/fixtures/doc-obligations/read-boundary/boundary-policy.json +325 -0
- package/ci/fixtures/doc-obligations/read-boundary/cases.json +14058 -0
- package/ci/fixtures/doc-obligations/read-boundary/expected.json +1 -0
- package/ci/fixtures/doc-obligations/read-boundary/identity-cases.json +119 -0
- package/ci/fixtures/doc-obligations/read-boundary/identity-expected.json +1 -0
- package/ci/fixtures/doc-obligations/read-boundary/scope-cases.json +208 -0
- package/ci/fixtures/doc-obligations/read-boundary/scope-expected.json +1 -0
- package/ci/fixtures/doc-obligations/reconciliation-prep/cases.json +2751 -0
- package/ci/fixtures/doc-obligations/reconciliation-prep/flood-entrypoint.js +5 -0
- package/ci/fixtures/doc-obligations/reconciliation-prep/hang-entrypoint.js +5 -0
- package/ci/fixtures/doc-obligations/reconciliation-prep/pending-red.json +648 -0
- package/ci/fixtures/doc-obligations/reconciliation-prep/provider-contract.json +100 -0
- package/ci/fixtures/doc-obligations/reconciliation-prep/trust-contract.json +188 -0
- package/ci/lib/doc-evidence-boundary-check.js +1240 -0
- package/ci/lib/doc-evidence-fixtures.js +459 -0
- package/ci/lib/doc-evidence-trace.js +171 -0
- package/ci/lib/doc-reconciliation-prep-fixtures.js +502 -0
- package/ci/lib/snapshot-stdin.js +27 -0
- package/ci/mechanism-2aw-project-docs-config.test.js +450 -10
- package/ci/mechanism-325-3b-preparation.test.js +382 -0
- package/ci/mechanism-341-boundary-enforcement.test.js +660 -0
- package/ci/mechanism-341-capture-module.test.js +357 -0
- package/ci/mechanism-341-mutation-runner.test.js +413 -0
- package/ci/mechanism-341-read-boundary.test.js +388 -0
- package/ci/mechanism-341-snapshot-stdin.test.js +40 -0
- package/ci/mutation-check.js +805 -45
- package/ci/mutation-loader.js +544 -57
- package/ci/prep/doc-reconciliation-red.js +407 -0
- package/ci/stress-341-boundary-control.js +85 -0
- package/docs/doc-evidence-baseline-binding.md +105 -19
- package/docs/doc-evidence-boundary.md +135 -0
- package/docs/doc-workflow-reconciliation.md +224 -0
- package/docs/document-lifecycle.md +67 -8
- package/harness-everything/SKILL.md +1 -1
- package/harness-everything/scripts/doc-evidence-capture.js +331 -0
- package/harness-everything/scripts/doc-evidence.js +72 -223
- package/harness-everything/scripts/project-docs-resolver.js +30 -13
- package/multi-agent-workspace/SKILL.md +1 -1
- package/multi-agent-workspace/scripts/project-docs-resolver.js +30 -13
- package/opencode-plugin/plugin.json +1 -1
- package/package.json +5 -3
- package/plugins/harness-everything/.codex-plugin/plugin.json +1 -1
- package/plugins/harness-everything/plugin.json +1 -1
- package/plugins/harness-everything/skills/harness-everything/SKILL.md +1 -1
- package/plugins/harness-everything/skills/harness-everything/scripts/doc-evidence-capture.js +331 -0
- package/plugins/harness-everything/skills/harness-everything/scripts/doc-evidence.js +72 -223
- package/plugins/harness-everything/skills/harness-everything/scripts/project-docs-resolver.js +30 -13
- package/plugins/harness-everything/skills/multi-agent-workspace/SKILL.md +1 -1
- package/plugins/harness-everything/skills/multi-agent-workspace/scripts/project-docs-resolver.js +30 -13
- package/plugins/harness-everything/skills/to-spec/SKILL.md +1 -1
- package/plugins/harness-everything/skills/to-spec/scripts/check-project-docs.js +136 -4
- package/plugins/harness-everything/skills/to-spec/scripts/project-docs-resolver.js +30 -13
- package/sbom.cdx.json +43 -7
- package/to-spec/SKILL.md +1 -1
- package/to-spec/scripts/check-project-docs.js +136 -4
- package/to-spec/scripts/project-docs-resolver.js +30 -13
|
@@ -0,0 +1,660 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// #341 Phase 0C enforcement suite.
|
|
4
|
+
// C1 (isolation): the frozen isolation policy is run against the exact canonical and packaged source pairs, and the
|
|
5
|
+
// reviewed weakened-boundary transforms are applied in memory. Every negative asserts its pinned ruleId; runtime or
|
|
6
|
+
// load failures do not count as enforcement. COMP-C2-BOUND N05/N06 are deliberate C1 survivors.
|
|
7
|
+
// C2 (ast, c2.md Requests 1-3): the same transforms are rejected by the pinned AST supplement, with complete
|
|
8
|
+
// structure coverage. Positive controls cover local rename, format and comment changes. Fail-closed controls cover
|
|
9
|
+
// invalid syntax and parser-version mismatch. AST-path sensitivity controls disable only the AST target-drift or
|
|
10
|
+
// program-drift sink in the actually loaded checker source. Fingerprints are never regenerated or accepted here.
|
|
11
|
+
|
|
12
|
+
const assert = require('node:assert/strict');
|
|
13
|
+
const crypto = require('node:crypto');
|
|
14
|
+
const fs = require('node:fs');
|
|
15
|
+
const os = require('node:os');
|
|
16
|
+
const path = require('node:path');
|
|
17
|
+
const vm = require('node:vm');
|
|
18
|
+
const { spawnSync } = require('node:child_process');
|
|
19
|
+
const { spawnWithSnapshotInput } = require('./lib/snapshot-stdin');
|
|
20
|
+
const { createRequire } = require('node:module');
|
|
21
|
+
const { checkBoundary } = require('./lib/doc-evidence-boundary-check');
|
|
22
|
+
|
|
23
|
+
const ROOT = path.resolve(__dirname, '..');
|
|
24
|
+
const FIXTURE_DIR = path.join(__dirname, 'fixtures', 'doc-obligations', 'read-boundary');
|
|
25
|
+
const POLICY_PATH = path.join(FIXTURE_DIR, 'boundary-policy.json');
|
|
26
|
+
const CONTROLS_PATH = path.join(FIXTURE_DIR, 'boundary-controls.json');
|
|
27
|
+
const CHECKER_PATH = path.join(__dirname, 'lib', 'doc-evidence-boundary-check.js');
|
|
28
|
+
const policyText = fs.readFileSync(POLICY_PATH, 'utf8');
|
|
29
|
+
const policy = JSON.parse(policyText);
|
|
30
|
+
const controls = JSON.parse(fs.readFileSync(CONTROLS_PATH, 'utf8'));
|
|
31
|
+
// C2 (AST) covers only controls with an AST route. N02c is isolation-only: its AST rejection would be a numbering side effect.
|
|
32
|
+
const astControls = controls.controls.filter(control => !control.routes || control.routes.includes('ast'));
|
|
33
|
+
// Code-level expectations (isolation route). Several codes share one ruleId, so an alias or return rule can be removed
|
|
34
|
+
// without changing the rule-level outcome; these assertions pin the specific emitter each control must trigger.
|
|
35
|
+
// AST-route pinned target expected for controls whose AST rejection must come from a named target (c2-fix).
|
|
36
|
+
const INTENDED_AST_TARGETS = {
|
|
37
|
+
N02c: { contract: 'ast-target-context-destructure', location: '@evaluateObligation-context-destructure' }
|
|
38
|
+
};
|
|
39
|
+
const INTENDED_CODES = {
|
|
40
|
+
N01: ['CONTEXT_ALIAS_OR_COMPUTED_USE'],
|
|
41
|
+
N02: ['CONTEXT_ALIAS_OR_COMPUTED_USE'],
|
|
42
|
+
N01d: ['CONTEXT_ALIAS_OR_COMPUTED_USE'],
|
|
43
|
+
N02d: ['CONTEXT_ALIAS_OR_COMPUTED_USE'],
|
|
44
|
+
N02c: ['CONTEXT_CONSUMER_SURFACE'],
|
|
45
|
+
N03: ['RETURNED_VIEW_ESCAPE'],
|
|
46
|
+
N04: ['RETURNED_VIEW_ESCAPE']
|
|
47
|
+
};
|
|
48
|
+
const checkerRequire = createRequire(CHECKER_PATH);
|
|
49
|
+
|
|
50
|
+
const EVENT_FILE = process.env.HARNESS_ASSERTION_EVENT_FILE;
|
|
51
|
+
const SELECTED_ASSERTION = process.env.HARNESS_ASSERTION_ID;
|
|
52
|
+
const SELECTED_CASE = process.env.HARNESS_CASE_ID
|
|
53
|
+
|| (SELECTED_ASSERTION ? SELECTED_ASSERTION.slice(0, SELECTED_ASSERTION.indexOf('.')) : undefined);
|
|
54
|
+
const claimedAssertions = new Set();
|
|
55
|
+
const claimedCases = new Set();
|
|
56
|
+
const failures = [];
|
|
57
|
+
|
|
58
|
+
const FAIL_CLOSED_CODES = ['SOURCE_PAIR_MISSING', 'AST_POLICY_INVALID', 'AST_PARSER_UNAVAILABLE', 'AST_PARSE_FAILED',
|
|
59
|
+
'AST_NODE_UNSUPPORTED', 'AST_TARGET_INCOMPLETE', 'CHECKER_FAIL_CLOSED', 'CHECKER_EXECUTION_FAILED',
|
|
60
|
+
'CHECKER_EXECUTION_TIMEOUT', 'MODULE_LOAD_FAILED', 'MODULE_IMPORT_DENIED'];
|
|
61
|
+
|
|
62
|
+
const LOOP_CONTROL_CHILD_TIMEOUT_MS = 5000;
|
|
63
|
+
|
|
64
|
+
function emit(event) {
|
|
65
|
+
if (!EVENT_FILE) return;
|
|
66
|
+
fs.appendFileSync(EVENT_FILE, `${JSON.stringify({ schemaVersion: 1, ...event })}\n`, 'utf8');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function assertion(caseId, contract, run) {
|
|
70
|
+
const assertionId = `${caseId}.${contract}`;
|
|
71
|
+
claimedCases.add(caseId);
|
|
72
|
+
claimedAssertions.add(assertionId);
|
|
73
|
+
if (SELECTED_CASE && SELECTED_CASE !== caseId) return;
|
|
74
|
+
if (SELECTED_ASSERTION && SELECTED_ASSERTION !== assertionId) return;
|
|
75
|
+
try {
|
|
76
|
+
run();
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (!(error instanceof assert.AssertionError)) throw error;
|
|
79
|
+
failures.push(assertionId);
|
|
80
|
+
emit({ event: 'assertion-failure', caseId, assertionId, errorCode: 'ERR_ASSERTION',
|
|
81
|
+
message: String(error.message).split('\n')[0] });
|
|
82
|
+
console.error(`FAIL ${assertionId}: ${String(error.message).split('\n')[0]}`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
emit({ event: 'assertion-pass', caseId, assertionId });
|
|
86
|
+
console.log(`PASS ${assertionId}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function sha256(file) {
|
|
90
|
+
return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function sourcePair(distribution, files) {
|
|
94
|
+
const selected = policy.distributions[distribution];
|
|
95
|
+
if (!selected || files.evaluator !== selected.evaluator || files.capture !== selected.capture) {
|
|
96
|
+
throw new Error(`fixture paths do not match frozen policy for ${distribution}`);
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
evaluator: fs.readFileSync(path.join(ROOT, selected.evaluator), 'utf8'),
|
|
100
|
+
capture: fs.readFileSync(path.join(ROOT, selected.capture), 'utf8')
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function applyControl(control, original) {
|
|
105
|
+
const sources = { ...original };
|
|
106
|
+
for (const transform of control.transforms) {
|
|
107
|
+
if (!['evaluator', 'capture'].includes(transform.file)
|
|
108
|
+
|| typeof transform.from !== 'string' || typeof transform.to !== 'string') {
|
|
109
|
+
throw new Error(`${control.id}: malformed transform`);
|
|
110
|
+
}
|
|
111
|
+
const occurrences = sources[transform.file].split(transform.from).length - 1;
|
|
112
|
+
if (occurrences !== 1) {
|
|
113
|
+
throw new Error(`${control.id}: ${transform.file} anchor occurred ${occurrences} times, expected once`);
|
|
114
|
+
}
|
|
115
|
+
sources[transform.file] = sources[transform.file].replace(transform.from, transform.to);
|
|
116
|
+
}
|
|
117
|
+
if (sources.evaluator === original.evaluator && sources.capture === original.capture) {
|
|
118
|
+
throw new Error(`${control.id}: neither source was weakened`);
|
|
119
|
+
}
|
|
120
|
+
for (const kind of ['evaluator', 'capture']) {
|
|
121
|
+
new vm.Script(sources[kind], { filename: `${control.id}/${kind}.js` });
|
|
122
|
+
}
|
|
123
|
+
return sources;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function insertInfiniteLoop(source, functionName) {
|
|
127
|
+
const signature = new RegExp(`function\\s+${functionName}\\s*\\([^)]*\\)\\s*\\{`, 'g'); // portability-lint: allow RegExp source, not a path
|
|
128
|
+
const matches = [...source.matchAll(signature)];
|
|
129
|
+
assert.equal(matches.length, 1, `${functionName}: expected exactly one function entrypoint`);
|
|
130
|
+
const weakened = source.replace(signature, match => `${match}\n while (true) {}`);
|
|
131
|
+
new vm.Script(weakened, { filename: `timeout-control/${functionName}.js` });
|
|
132
|
+
return weakened;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function boundedLoopControlResult(input, label) {
|
|
136
|
+
const startedAt = Date.now();
|
|
137
|
+
const child = spawnWithSnapshotInput(process.execPath, [CHECKER_PATH, '--c1-test-fast-isolation'], {
|
|
138
|
+
input: JSON.stringify(input),
|
|
139
|
+
encoding: 'utf8',
|
|
140
|
+
timeout: LOOP_CONTROL_CHILD_TIMEOUT_MS,
|
|
141
|
+
killSignal: 'SIGKILL',
|
|
142
|
+
maxBuffer: 1024 * 1024
|
|
143
|
+
});
|
|
144
|
+
const elapsedMs = Date.now() - startedAt;
|
|
145
|
+
assert.equal(child.error && child.error.code, undefined,
|
|
146
|
+
`${label}: the bounded test child must finish before ${LOOP_CONTROL_CHILD_TIMEOUT_MS}ms`);
|
|
147
|
+
assert.equal(child.signal, null, `${label}: the bounded test child was not terminated`);
|
|
148
|
+
assert.equal(child.status, 0, `${label}: child exit ${child.status}; stderr=${child.stderr}`);
|
|
149
|
+
const result = JSON.parse(child.stdout);
|
|
150
|
+
assert.equal(result.coverage.complete, false, `${label}: timeout leaves C1 coverage incomplete`);
|
|
151
|
+
assert.deepEqual(result.diagnostics.map(item => item.code), ['CHECKER_EXECUTION_TIMEOUT'],
|
|
152
|
+
`${label}: the argv-gated short-deadline path returns only the deterministic fail-closed diagnostic`);
|
|
153
|
+
return { result, elapsedMs };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function appendAfterCaptureExports(source, injectedLines) {
|
|
157
|
+
const anchor = 'module.exports = { createInvocationCapture };';
|
|
158
|
+
assert.equal(source.split(anchor).length - 1, 1,
|
|
159
|
+
'capture source must have one exact module export anchor');
|
|
160
|
+
return source.replace(anchor, [anchor, ...injectedLines].join('\n'));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function insertHostArgumentProbe(source, functionName) {
|
|
164
|
+
const signature = new RegExp(`function\\s+${functionName}\\s*\\([^)]*\\)\\s*\\{`, 'g'); // portability-lint: allow RegExp source, not a path
|
|
165
|
+
assert.equal([...source.matchAll(signature)].length, 1,
|
|
166
|
+
`${functionName}: expected exactly one host-argument probe entrypoint`);
|
|
167
|
+
const probe = [
|
|
168
|
+
' for (const candidate of [arguments[0], arguments[0] && arguments[0].obligations, arguments[0] && arguments[0].artifacts]) {',
|
|
169
|
+
' if (!candidate || (typeof candidate !== "object" && typeof candidate !== "function")) continue;',
|
|
170
|
+
' try {',
|
|
171
|
+
" candidate.constructor.constructor('return process')();",
|
|
172
|
+
" throw new Error('__C1_HOST_ARGUMENT_ESCAPED__');",
|
|
173
|
+
' } catch (error) {',
|
|
174
|
+
" if (error && error.message === '__C1_HOST_ARGUMENT_ESCAPED__') throw error;",
|
|
175
|
+
' }',
|
|
176
|
+
' }'
|
|
177
|
+
].join('\n');
|
|
178
|
+
return source.replace(signature, match => `${match}\n${probe}`);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function insertHostArrayCallbackProbe(source) {
|
|
182
|
+
const anchor = `baselineEntries() {
|
|
183
|
+
return [...acceptedBaselineEntries.values()].map(entry => Object.freeze({ ...entry }));
|
|
184
|
+
},`;
|
|
185
|
+
assert.equal(source.split(anchor).length - 1, 1,
|
|
186
|
+
'capture source must have one exact baselineEntries callback probe anchor');
|
|
187
|
+
return source.replace(anchor, `baselineEntries() {
|
|
188
|
+
const entries = [...acceptedBaselineEntries.values()].map(entry => Object.freeze({ ...entry }));
|
|
189
|
+
const nativeEvery = Array.prototype.every;
|
|
190
|
+
Array.prototype.every = function (callback) {
|
|
191
|
+
try {
|
|
192
|
+
callback.constructor.constructor('return process')();
|
|
193
|
+
throw new Error('__C1_HOST_CALLBACK_ESCAPED__');
|
|
194
|
+
} catch (error) {
|
|
195
|
+
if (error && error.message === '__C1_HOST_CALLBACK_ESCAPED__') throw error;
|
|
196
|
+
}
|
|
197
|
+
return nativeEvery.call(this, callback);
|
|
198
|
+
};
|
|
199
|
+
return entries;
|
|
200
|
+
},`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function insertHostBridgeProbe(source) {
|
|
204
|
+
return appendAfterCaptureExports(source, [
|
|
205
|
+
'try {',
|
|
206
|
+
" const escapedProcess = crypto.createHash.constructor('return process')();",
|
|
207
|
+
" module.exports.__cryptoEscapedHostProcess = typeof escapedProcess.pid === 'number';",
|
|
208
|
+
'} catch {}',
|
|
209
|
+
'try {',
|
|
210
|
+
" const escapedProcess = require.constructor('return process')();",
|
|
211
|
+
" module.exports.__requireEscapedHostProcess = typeof escapedProcess.pid === 'number';",
|
|
212
|
+
'} catch {}'
|
|
213
|
+
]);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function insertSigtermResistantEscape(source) {
|
|
217
|
+
return appendAfterCaptureExports(source, [
|
|
218
|
+
"const hostProcess = crypto.createHash.constructor('return process')();",
|
|
219
|
+
"hostProcess.on('SIGTERM', () => {});",
|
|
220
|
+
"const hostSetInterval = crypto.createHash.constructor('return setInterval')();",
|
|
221
|
+
'hostSetInterval(() => {}, 1000);'
|
|
222
|
+
]);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function boundedDirectControlResult(input, label) {
|
|
226
|
+
const startedAt = Date.now();
|
|
227
|
+
const child = spawnWithSnapshotInput(process.execPath, [CHECKER_PATH, '--c1-isolation-child'], {
|
|
228
|
+
input: JSON.stringify(input),
|
|
229
|
+
encoding: 'utf8',
|
|
230
|
+
timeout: LOOP_CONTROL_CHILD_TIMEOUT_MS,
|
|
231
|
+
killSignal: 'SIGKILL',
|
|
232
|
+
maxBuffer: 1024 * 1024
|
|
233
|
+
});
|
|
234
|
+
const elapsedMs = Date.now() - startedAt;
|
|
235
|
+
assert.equal(child.error && child.error.code, undefined,
|
|
236
|
+
`${label}: the SIGKILL-guarded checker child must finish before ${LOOP_CONTROL_CHILD_TIMEOUT_MS}ms`);
|
|
237
|
+
assert.equal(child.signal, null, `${label}: checker child was not terminated`);
|
|
238
|
+
assert.equal(child.status, 0, `${label}: child exit ${child.status}; stderr=${child.stderr}`);
|
|
239
|
+
return { result: JSON.parse(child.stdout), elapsedMs };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function boundedSigkillControlResult(label) {
|
|
243
|
+
const startedAt = Date.now();
|
|
244
|
+
const child = spawnSync(process.execPath, [CHECKER_PATH, '--c1-timeout-kill-test'], {
|
|
245
|
+
encoding: 'utf8',
|
|
246
|
+
timeout: LOOP_CONTROL_CHILD_TIMEOUT_MS,
|
|
247
|
+
killSignal: 'SIGKILL',
|
|
248
|
+
maxBuffer: 1024 * 1024
|
|
249
|
+
});
|
|
250
|
+
const elapsedMs = Date.now() - startedAt;
|
|
251
|
+
assert.equal(child.error && child.error.code, undefined,
|
|
252
|
+
`${label}: external SIGKILL deadline ${LOOP_CONTROL_CHILD_TIMEOUT_MS}ms must contain the control`);
|
|
253
|
+
assert.equal(child.signal, null, `${label}: timeout wrapper finished before the external deadline`);
|
|
254
|
+
assert.equal(child.status, 0, `${label}: wrapper exit ${child.status}; stderr=${child.stderr}`);
|
|
255
|
+
return { result: JSON.parse(child.stdout), elapsedMs };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function controlStatus(result, intendedRuleId) {
|
|
259
|
+
if (result.diagnostics.some(diagnostic => diagnostic.ruleId === intendedRuleId)) return 'REJECTED';
|
|
260
|
+
if (result.coverage.complete && result.diagnostics.length === 0) return 'SURVIVED';
|
|
261
|
+
return 'UNCHECKED';
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// C1 expectation: the pinned status for each control (N05/N06 are deliberate isolation survivors).
|
|
265
|
+
function assertExpectedControl(control, result) {
|
|
266
|
+
const intended = result.diagnostics.filter(diagnostic => diagnostic.ruleId === control.intendedRuleId);
|
|
267
|
+
const actual = controlStatus(result, control.intendedRuleId);
|
|
268
|
+
assert.equal(actual, control.expectedC1Status,
|
|
269
|
+
`${control.id}: intended ${control.intendedRuleId}; expected ${control.expectedC1Status}, got ${actual}; diagnostics=${JSON.stringify(result.diagnostics)}; missing=${JSON.stringify(result.coverage.missing)}`);
|
|
270
|
+
if (control.expectedC1Status === 'REJECTED') {
|
|
271
|
+
assert.ok(intended.length > 0, `${control.id}: rejection must carry intended rule ${control.intendedRuleId}`);
|
|
272
|
+
assert.ok(!result.diagnostics.some(item => FAIL_CLOSED_CODES.includes(item.code)),
|
|
273
|
+
`${control.id}: a checker/load failure is not enforcement`);
|
|
274
|
+
} else if (control.expectedC1Status === 'SURVIVED') {
|
|
275
|
+
assert.equal(result.coverage.complete, true, `${control.id}: survivor must complete declared C1 coverage`);
|
|
276
|
+
assert.deepEqual(result.diagnostics, [], `${control.id}: survivor has no boundary diagnostics`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// C2 expectation: every N01-N09 control, including the dormant variants, must be rejected by its pinned AST rule with
|
|
281
|
+
// complete structure coverage and no fail-closed condition. A survivor under the AST route is a failure.
|
|
282
|
+
function assertAstComplete(result, label) {
|
|
283
|
+
assert.equal(result.method, 'ast', `${label}: AST route ran`);
|
|
284
|
+
assert.ok(result.coverage.checked.includes('source-pair'), `${label}: source pair checked`);
|
|
285
|
+
assert.ok(result.coverage.checked.includes('ast-structure'), `${label}: AST structure checked`);
|
|
286
|
+
assert.deepEqual(result.coverage.missing, [], `${label}: no declared AST coverage missing`);
|
|
287
|
+
assert.equal(result.coverage.complete, true, `${label}: AST coverage is complete`);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function assertAstRejection(control, result) {
|
|
291
|
+
assertAstComplete(result, control.id);
|
|
292
|
+
assert.ok(!result.diagnostics.some(item => FAIL_CLOSED_CODES.includes(item.code)),
|
|
293
|
+
`${control.id}: a fail-closed AST condition is not enforcement (${JSON.stringify(result.diagnostics.map(item => item.code))})`);
|
|
294
|
+
const intended = result.diagnostics.filter(diagnostic => diagnostic.ruleId === control.intendedRuleId);
|
|
295
|
+
assert.ok(intended.length > 0, `${control.id}: AST rejection must carry intended rule ${control.intendedRuleId}; diagnostics=${JSON.stringify(result.diagnostics)}`);
|
|
296
|
+
if (control.intendedRuleId === 'COMP-C2-BOUND') {
|
|
297
|
+
assert.ok(intended.every(item => item.exceptionId === 'COMP-C2-BOUND'),
|
|
298
|
+
`${control.id}: the exception rule must name its bounded exception ID`);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function loadCheckerFromSource(source) {
|
|
303
|
+
const isolatedModule = { exports: {} };
|
|
304
|
+
const allowed = new Set(['./snapshot-stdin', 'node:child_process', 'node:fs', 'acorn', 'acorn-walk', 'acorn/package.json', 'acorn-walk/package.json']);
|
|
305
|
+
const sandbox = {
|
|
306
|
+
module: isolatedModule,
|
|
307
|
+
exports: isolatedModule.exports,
|
|
308
|
+
require(specifier) {
|
|
309
|
+
if (specifier === 'node:vm') return vm;
|
|
310
|
+
if (specifier === 'node:crypto') return crypto;
|
|
311
|
+
if (allowed.has(specifier)) return checkerRequire(specifier);
|
|
312
|
+
throw new Error(`checker self-test denied import ${String(specifier)}`);
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
vm.runInNewContext(source, sandbox, { filename: 'ci/lib/doc-evidence-boundary-check.js', timeout: 5000 });
|
|
316
|
+
if (typeof isolatedModule.exports.checkBoundary !== 'function') {
|
|
317
|
+
throw new Error('mutated checker source did not export checkBoundary');
|
|
318
|
+
}
|
|
319
|
+
const check = isolatedModule.exports.checkBoundary;
|
|
320
|
+
// The sandbox has its own Array/Object realm. Round-trip results so node:assert compares them as ordinary values;
|
|
321
|
+
// without this, deepStrictEqual([], []) fails across realms and a sensitivity check can pass for the wrong reason.
|
|
322
|
+
return input => JSON.parse(JSON.stringify(check(input)));
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function noDiagnosticsChecker() {
|
|
326
|
+
const source = fs.readFileSync(CHECKER_PATH, 'utf8');
|
|
327
|
+
const activeAdd = ` const add = (code, file, location, ruleId, exceptionId) => {\n diagnostics.push({\n code,\n file: typeof file === 'string' ? file : '<policy>',\n location: typeof location === 'string' ? location : 'input',\n ruleId,\n ...(exceptionId ? { exceptionId } : {})\n });\n };`;
|
|
328
|
+
const disabledAdd = ` let __c1TestAddSinkCalls = 0;\n const add = (code, file, location, ruleId, exceptionId) => {\n void [code, file, location, ruleId, exceptionId];\n __c1TestAddSinkCalls++;\n };`;
|
|
329
|
+
const count = source.split(activeAdd).length - 1;
|
|
330
|
+
assert.equal(count, 1, 'checker-disable self-test must find exactly one diagnostic sink');
|
|
331
|
+
let disabledSource = source.replace(activeAdd, disabledAdd);
|
|
332
|
+
const finalReturn = ` return {\n method,\n coverage: { complete: missing.length === 0, checked, missing: unique(missing) },\n diagnostics\n };\n}\n\nfunction runAstBoundaryCheck`;
|
|
333
|
+
const instrumentedReturn = ` return {\n method,\n coverage: { complete: missing.length === 0, checked, missing: unique(missing), testAddSinkCalls: __c1TestAddSinkCalls },\n diagnostics\n };\n}\n\nfunction runAstBoundaryCheck`;
|
|
334
|
+
assert.equal(disabledSource.split(finalReturn).length - 1, 1,
|
|
335
|
+
'checker-disable self-test must find exactly one C1 result return for its sink sentinel');
|
|
336
|
+
disabledSource = disabledSource.replace(finalReturn, instrumentedReturn);
|
|
337
|
+
|
|
338
|
+
// Run the transformed module as an actual CLI child. This uses the production --c1-isolation-child entrypoint,
|
|
339
|
+
// with real process/filename bindings and the same child boundary as checkBoundary().
|
|
340
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'harness-c1-disabled-checker-'));
|
|
341
|
+
const baselinePath = path.join(tempDir, 'doc-evidence-boundary-check-baseline.js');
|
|
342
|
+
const disabledPath = path.join(tempDir, 'doc-evidence-boundary-check-disabled.js');
|
|
343
|
+
fs.copyFileSync(path.join(__dirname, 'lib', 'snapshot-stdin.js'), path.join(tempDir, 'snapshot-stdin.js'));
|
|
344
|
+
fs.writeFileSync(baselinePath, source, 'utf8');
|
|
345
|
+
fs.writeFileSync(disabledPath, disabledSource, 'utf8');
|
|
346
|
+
return {
|
|
347
|
+
baseline(input) {
|
|
348
|
+
return runCheckerChild(baselinePath, input, 'unchanged baseline');
|
|
349
|
+
},
|
|
350
|
+
run(input) {
|
|
351
|
+
return runCheckerChild(disabledPath, input, 'disabled checker');
|
|
352
|
+
},
|
|
353
|
+
cleanup() {
|
|
354
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function runCheckerChild(checkerPath, input, label) {
|
|
360
|
+
const child = spawnWithSnapshotInput(process.execPath, [checkerPath, '--c1-isolation-child'], {
|
|
361
|
+
input: JSON.stringify(input),
|
|
362
|
+
encoding: 'utf8',
|
|
363
|
+
timeout: LOOP_CONTROL_CHILD_TIMEOUT_MS,
|
|
364
|
+
killSignal: 'SIGKILL',
|
|
365
|
+
maxBuffer: 4 * 1024 * 1024
|
|
366
|
+
});
|
|
367
|
+
assert.equal(child.error && child.error.code, undefined,
|
|
368
|
+
`${label}: real C1 child must finish before ${LOOP_CONTROL_CHILD_TIMEOUT_MS}ms`);
|
|
369
|
+
assert.equal(child.signal, null, `${label}: real C1 child must not be terminated`);
|
|
370
|
+
assert.equal(child.status, 0, `${label}: real C1 child exit ${child.status}; stderr=${child.stderr}`);
|
|
371
|
+
let result;
|
|
372
|
+
assert.doesNotThrow(() => { result = JSON.parse(child.stdout); }, `${label}: child must return JSON`);
|
|
373
|
+
assert.ok(result && typeof result === 'object' && Array.isArray(result.diagnostics),
|
|
374
|
+
`${label}: child must return a boundary result`);
|
|
375
|
+
return result;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Disables one AST drift emission only. AST checkBoundary dispatches directly to runAstBoundaryCheck before it reads
|
|
379
|
+
// process or __filename, so this VM-only source loader remains valid for the AST route.
|
|
380
|
+
function astDriftDisabledChecker(emissionCode) {
|
|
381
|
+
const source = fs.readFileSync(CHECKER_PATH, 'utf8');
|
|
382
|
+
const line = new RegExp(`^\\s*add\\('${emissionCode}'.*$`, 'gm'); // portability-lint: allow regex source, not a file name
|
|
383
|
+
const matches = source.match(line) || [];
|
|
384
|
+
assert.equal(matches.length, 1, `${emissionCode} disable self-test must find exactly one emission`);
|
|
385
|
+
return loadCheckerFromSource(source.replace(line, ' void 0;'));
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
console.log('=== #341 Phase 0C enforcement (C1 isolation + C2 ast) ===');
|
|
389
|
+
assert.equal(policy.status, 'frozen-for-c1', 'boundary policy is frozen for the C1 trial');
|
|
390
|
+
assert.equal(controls.phase, 'C1', 'control inventory targets C1');
|
|
391
|
+
assert.equal(controls.expectedRuleIdsPending, false, 'all intended rule IDs are pinned independently of observed output');
|
|
392
|
+
assert.ok(controls.controls.length > 0, 'control inventory is nonempty');
|
|
393
|
+
for (const control of controls.controls) {
|
|
394
|
+
assert.equal(typeof control.intendedRuleId, 'string', `${control.id} pins an intended rule ID`);
|
|
395
|
+
assert.ok(['REJECTED', 'SURVIVED'].includes(control.expectedC1Status), `${control.id} pins an expected C1 outcome`);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (SELECTED_CASE && !/^C[12]-[A-Za-z0-9-]+$/.test(SELECTED_CASE)) {
|
|
399
|
+
console.error(`infrastructure: invalid HARNESS_CASE_ID ${SELECTED_CASE}`);
|
|
400
|
+
process.exit(2);
|
|
401
|
+
}
|
|
402
|
+
if (SELECTED_ASSERTION && (!SELECTED_CASE || !SELECTED_ASSERTION.startsWith(`${SELECTED_CASE}.`))) {
|
|
403
|
+
console.error(`infrastructure: HARNESS_ASSERTION_ID ${SELECTED_ASSERTION} does not belong to case ${SELECTED_CASE}`);
|
|
404
|
+
process.exit(2);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const policyHashAtStart = sha256(POLICY_PATH);
|
|
408
|
+
|
|
409
|
+
for (const [distribution, files] of Object.entries(controls.distributions)) {
|
|
410
|
+
const original = sourcePair(distribution, files);
|
|
411
|
+
|
|
412
|
+
// ---- C1 isolation ----
|
|
413
|
+
const positiveCase = `C1-positive-${distribution}`;
|
|
414
|
+
assertion(positiveCase, 'boundary.complete-isolation-policy', () => {
|
|
415
|
+
const positive = checkBoundary({ distribution, sources: original, policy, method: 'isolation' });
|
|
416
|
+
assert.equal(positive.coverage.complete, true, `${distribution}: unchanged pair has complete coverage`);
|
|
417
|
+
assert.deepEqual(positive.diagnostics, [], `${distribution}: unchanged pair has no diagnostics`);
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
for (const control of controls.controls) {
|
|
421
|
+
const caseId = `C1-${control.id}-${distribution}`;
|
|
422
|
+
if (SELECTED_CASE && SELECTED_CASE !== caseId) continue;
|
|
423
|
+
const sources = applyControl(control, original);
|
|
424
|
+
const result = checkBoundary({ distribution, sources, policy, method: 'isolation' });
|
|
425
|
+
const ruleName = control.intendedRuleId.replace(/[^A-Za-z0-9-]/g, '-');
|
|
426
|
+
assertion(caseId, `boundary.intended-rule-${ruleName}`, () => assertExpectedControl(control, result));
|
|
427
|
+
for (const code of INTENDED_CODES[control.id] || []) {
|
|
428
|
+
assertion(caseId, `boundary.intended-code-${code}`, () => {
|
|
429
|
+
assert.ok(result.diagnostics.some(item => item.code === code),
|
|
430
|
+
`${control.id}: the ${code} emitter must fire; diagnostics=${JSON.stringify(result.diagnostics.map(item => item.code))}`);
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
const diagnostics = result.diagnostics.map(item => `${item.code}/${item.ruleId}${item.exceptionId ? `/${item.exceptionId}` : ''}`).join(',') || 'none';
|
|
434
|
+
console.log(`EVIDENCE ${caseId} expected=${control.expectedC1Status} actual=${controlStatus(result, control.intendedRuleId)} coverage=${result.coverage.complete ? 'complete' : `missing:${result.coverage.missing.join(',')}`} diagnostics=${diagnostics}`);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Prove N01/N02/N03 rejection assertions are rule-sensitive: a test-only mutation of the actual C1 checker source
|
|
438
|
+
// runs as a real child through --c1-isolation-child. The sentinel distinguishes a reached no-diagnostics sink from
|
|
439
|
+
// any generic fail-closed/infrastructure result.
|
|
440
|
+
for (const id of ['N01', 'N02', 'N03']) {
|
|
441
|
+
const caseId = `C1-checker-disabled-${id}-${distribution}`;
|
|
442
|
+
// Selection must gate setup and subprocesses as well as the final assertion.
|
|
443
|
+
assertion(caseId, 'assertion.child-sink-sensitivity', () => {
|
|
444
|
+
const control = controls.controls.find(item => item.id === id);
|
|
445
|
+
if (!control) throw new Error(`checker-disable self-test missing ${id}`);
|
|
446
|
+
const sources = applyControl(control, original);
|
|
447
|
+
const input = { distribution, sources, policy, method: 'isolation' };
|
|
448
|
+
const disabledChecker = noDiagnosticsChecker();
|
|
449
|
+
try {
|
|
450
|
+
const baseline = disabledChecker.baseline(input);
|
|
451
|
+
assertExpectedControl(control, baseline);
|
|
452
|
+
const disabledResult = disabledChecker.run(input);
|
|
453
|
+
assert.ok(!disabledResult.diagnostics.some(item => FAIL_CLOSED_CODES.includes(item.code)),
|
|
454
|
+
`${id}: fail-closed or infrastructure results cannot count as a sensitivity kill; diagnostics=${JSON.stringify(disabledResult.diagnostics)}`);
|
|
455
|
+
assert.ok(disabledResult.coverage.testAddSinkCalls > 0,
|
|
456
|
+
`${id}: disabled add() sink must be reached in the real child; sentinel=${disabledResult.coverage.testAddSinkCalls}`);
|
|
457
|
+
assert.ok(!disabledResult.diagnostics.some(item => item.ruleId === control.intendedRuleId),
|
|
458
|
+
`${id}: disabled checker must omit intended rule ${control.intendedRuleId}`);
|
|
459
|
+
assert.throws(() => assertExpectedControl(control, disabledResult), error =>
|
|
460
|
+
error instanceof assert.AssertionError
|
|
461
|
+
&& error.message.includes(`intended ${control.intendedRuleId}; expected REJECTED`),
|
|
462
|
+
`${id}: intended-rule assertion must fail because its diagnostic is missing`);
|
|
463
|
+
console.log(`EVIDENCE ${caseId} baseline=REJECTED intended=${control.intendedRuleId} disabled=missing sentinel=${disabledResult.coverage.testAddSinkCalls} infrastructure=none`);
|
|
464
|
+
} finally {
|
|
465
|
+
disabledChecker.cleanup();
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// A selected C1 control must not execute unrelated AST probes.
|
|
471
|
+
if (SELECTED_CASE && !SELECTED_CASE.startsWith('C2-')) continue;
|
|
472
|
+
|
|
473
|
+
// ---- C2 ast (persistent, per luna's Requests 1-3) ----
|
|
474
|
+
const astPositive = checkBoundary({ distribution, sources: original, policy, method: 'ast' });
|
|
475
|
+
assertion(`C2-positive-${distribution}`, 'boundary.ast-complete-unchanged', () => {
|
|
476
|
+
assertAstComplete(astPositive, `${distribution}: unchanged pair`);
|
|
477
|
+
assert.deepEqual(astPositive.diagnostics, [], `${distribution}: unchanged pair has no AST diagnostics`);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
for (const control of astControls) {
|
|
481
|
+
const caseId = `C2-${control.id}-${distribution}`;
|
|
482
|
+
const ruleName = control.intendedRuleId.replace(/[^A-Za-z0-9-]/g, '-');
|
|
483
|
+
assertion(caseId, 'transform.applied-exactly-once', () => {
|
|
484
|
+
const sources = applyControl(control, original);
|
|
485
|
+
assert.ok(sources.evaluator !== original.evaluator || sources.capture !== original.capture,
|
|
486
|
+
`${control.id}: the transform changed the selected pair`);
|
|
487
|
+
});
|
|
488
|
+
const sources = applyControl(control, original);
|
|
489
|
+
const astResult = checkBoundary({ distribution, sources, policy, method: 'ast' });
|
|
490
|
+
assertion(caseId, `boundary.ast-intended-rule-${ruleName}`, () => assertAstRejection(control, astResult));
|
|
491
|
+
if (INTENDED_AST_TARGETS[control.id]) {
|
|
492
|
+
const pinned = INTENDED_AST_TARGETS[control.id];
|
|
493
|
+
assertion(caseId, `boundary.${pinned.contract}`, () => {
|
|
494
|
+
assert.ok(astResult.diagnostics.some(item => item.location === pinned.location && item.ruleId === control.intendedRuleId),
|
|
495
|
+
`${control.id}: the AST rejection must come from pinned target ${pinned.location} under ${control.intendedRuleId}; diagnostics=${JSON.stringify(astResult.diagnostics.map(item => item.location + '/' + item.ruleId))}`);
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
const diagnostics = astResult.diagnostics.map(item => `${item.code}/${item.ruleId}${item.exceptionId ? `/${item.exceptionId}` : ''}`).join(',') || 'none';
|
|
499
|
+
console.log(`EVIDENCE ${caseId} expected=REJECTED actual=${astResult.diagnostics.some(item => item.ruleId === control.intendedRuleId) ? 'REJECTED' : 'NOT-REJECTED'} coverage=${astResult.coverage.complete ? 'complete' : `missing:${astResult.coverage.missing.join(',')}`} diagnostics=${diagnostics}`);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Positive: a unique local rename of a declared capture-module local, and a formatting/comment-only change.
|
|
503
|
+
const capturePresent = original.capture.split('candidateDigestCache').length - 1;
|
|
504
|
+
assert.ok(capturePresent > 0, 'the rename positive control has a local to rename');
|
|
505
|
+
const renamed = { evaluator: original.evaluator, capture: original.capture.split('candidateDigestCache').join('uniqueCandidateDigestCacheAlias') };
|
|
506
|
+
assertion(`C2-positive-rename-${distribution}`, 'boundary.ast-accepts-local-rename', () => {
|
|
507
|
+
const result = checkBoundary({ distribution, sources: renamed, policy, method: 'ast' });
|
|
508
|
+
assertAstComplete(result, `${distribution}: local rename`);
|
|
509
|
+
assert.deepEqual(result.diagnostics, [], `${distribution}: a unique local rename is accepted`);
|
|
510
|
+
});
|
|
511
|
+
const formatted = {
|
|
512
|
+
evaluator: original.evaluator.replace(/\n/g, '\r\n'),
|
|
513
|
+
capture: `${original.capture.replace(/\n/g, '\r\n')}\r\n// C2 positive control: trailing comment, formatting only.\r\n`
|
|
514
|
+
};
|
|
515
|
+
assertion(`C2-positive-format-${distribution}`, 'boundary.ast-accepts-format-and-comments', () => {
|
|
516
|
+
const result = checkBoundary({ distribution, sources: formatted, policy, method: 'ast' });
|
|
517
|
+
assertAstComplete(result, `${distribution}: formatting and comments`);
|
|
518
|
+
assert.deepEqual(result.diagnostics, [], `${distribution}: CRLF and trailing comments are accepted`);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
// Locality (c2-fix): a benign unused local inside one unrelated function must drift only that function's target. The
|
|
522
|
+
// full-program fingerprint still reports the edit. Before c2-fix a single new local moved six unrelated targets.
|
|
523
|
+
const LOCALITY_ANCHOR = 'function isPlainObject(value) {\n return Boolean(value) && typeof value === \'object\' && !Array.isArray(value);\n}';
|
|
524
|
+
assert.equal(original.capture.split(LOCALITY_ANCHOR).length - 1, 1, 'the locality probe has one isPlainObject anchor');
|
|
525
|
+
const localityCapture = original.capture.replace(LOCALITY_ANCHOR, () => LOCALITY_ANCHOR.replace('{\n return', '{\n const unrelatedProbeLocal = 1;\n void unrelatedProbeLocal;\n return'));
|
|
526
|
+
assertion(`C2-locality-${distribution}`, 'boundary.ast-benign-local-stays-local', () => {
|
|
527
|
+
const result = checkBoundary({ distribution, sources: { evaluator: original.evaluator, capture: localityCapture }, policy, method: 'ast' });
|
|
528
|
+
assert.equal(result.coverage.complete, true, `${distribution}: the locality probe keeps complete AST coverage`);
|
|
529
|
+
const targetDrift = result.diagnostics.filter(item => item.code === 'AST_TARGET_DRIFT').map(item => item.location);
|
|
530
|
+
assert.deepEqual(targetDrift, ['isPlainObject'], `${distribution}: only the edited target drifts; saw ${JSON.stringify(targetDrift)}`);
|
|
531
|
+
assert.ok(result.diagnostics.some(item => item.code === 'AST_PROGRAM_DRIFT'), `${distribution}: the full-program fingerprint still reports the edit`);
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
// Fail-closed: invalid syntax, and a parser-version pin that does not match the installed parser.
|
|
535
|
+
const syntaxMarker = 'function checkDocObligations(';
|
|
536
|
+
assert.equal(original.evaluator.split(syntaxMarker).length - 1, 1, 'the syntax fail-closed control has one anchor');
|
|
537
|
+
const badSyntax = { evaluator: original.evaluator.replace(syntaxMarker, 'function checkDocObligations((('), capture: original.capture };
|
|
538
|
+
assertion(`C2-failclosed-syntax-${distribution}`, 'boundary.ast-fails-closed-on-syntax', () => {
|
|
539
|
+
const result = checkBoundary({ distribution, sources: badSyntax, policy, method: 'ast' });
|
|
540
|
+
assert.equal(result.coverage.complete, false, 'invalid syntax is incomplete coverage');
|
|
541
|
+
assert.ok(!result.coverage.checked.includes('ast-structure'), 'invalid syntax never checks AST structure');
|
|
542
|
+
assert.ok(result.diagnostics.some(item => item.code === 'AST_PARSE_FAILED' && item.ruleId === 'AST_PARSE_COMPLETE'),
|
|
543
|
+
'invalid syntax reports AST_PARSE_FAILED under AST_PARSE_COMPLETE');
|
|
544
|
+
});
|
|
545
|
+
const pinMismatch = JSON.parse(JSON.stringify(policy));
|
|
546
|
+
pinMismatch.astSupplement.parser.acorn = '0.0.0';
|
|
547
|
+
assertion(`C2-failclosed-parser-pin-${distribution}`, 'boundary.ast-fails-closed-on-parser-pin', () => {
|
|
548
|
+
const result = checkBoundary({ distribution, sources: original, policy: pinMismatch, method: 'ast' });
|
|
549
|
+
assert.equal(result.coverage.complete, false, 'a parser-version mismatch is incomplete coverage');
|
|
550
|
+
assert.ok(result.diagnostics.some(item => item.code === 'AST_PARSER_UNAVAILABLE' && item.ruleId === 'AST_PARSER_PINNED'),
|
|
551
|
+
'a parser-version mismatch reports AST_PARSER_UNAVAILABLE under AST_PARSER_PINNED');
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
// AST-path sensitivity: disable only the AST target-drift emission in the actually loaded checker source. Every
|
|
555
|
+
// N01-N09 AST rejection must then fail its intended-rule assertion, so the AST rule path is what is being tested.
|
|
556
|
+
const targetDisabled = astDriftDisabledChecker('AST_TARGET_DRIFT');
|
|
557
|
+
for (const control of astControls) {
|
|
558
|
+
const sources = applyControl(control, original);
|
|
559
|
+
const disabledResult = targetDisabled({ distribution, sources, policy, method: 'ast' });
|
|
560
|
+
assertion(`C2-checker-disabled-ast-target-${control.id}-${distribution}`, 'assertion.rejects-disabled-ast-target', () => {
|
|
561
|
+
assert.throws(() => assertAstRejection(control, disabledResult),
|
|
562
|
+
error => error instanceof assert.AssertionError && error.message.includes('AST rejection must carry intended rule'),
|
|
563
|
+
`${control.id}: the AST rejection assertion must fail on the missing intended rule when AST target-drift emission is disabled`);
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Separation: with only the program-drift emission disabled, the target-level catches of N05/N06 must still reject.
|
|
568
|
+
const programDisabled = astDriftDisabledChecker('AST_PROGRAM_DRIFT');
|
|
569
|
+
for (const id of ['N05', 'N06']) {
|
|
570
|
+
const control = controls.controls.find(item => item.id === id);
|
|
571
|
+
if (!control) throw new Error(`AST program-drift self-test missing ${id}`);
|
|
572
|
+
const sources = applyControl(control, original);
|
|
573
|
+
const programResult = programDisabled({ distribution, sources, policy, method: 'ast' });
|
|
574
|
+
assertion(`C2-checker-program-drift-disabled-${id}-${distribution}`, 'assertion.target-drift-still-rejects', () => {
|
|
575
|
+
assertAstRejection(control, programResult);
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// C1 infinite-loop controls use the argv-gated 1,000 ms deadline; production C1 has a 10,000 ms hosted-runner budget.
|
|
581
|
+
const [loopDistribution, loopFiles] = Object.entries(controls.distributions)[0];
|
|
582
|
+
const loopOriginal = sourcePair(loopDistribution, loopFiles);
|
|
583
|
+
for (const control of [
|
|
584
|
+
{ caseId: 'C1-timeout-evaluator-loop', kind: 'evaluator', functionName: 'checkDocObligations' },
|
|
585
|
+
{ caseId: 'C1-timeout-capture-loop', kind: 'capture', functionName: 'createInvocationCapture' }
|
|
586
|
+
]) {
|
|
587
|
+
const sources = { ...loopOriginal, [control.kind]: insertInfiniteLoop(loopOriginal[control.kind], control.functionName) };
|
|
588
|
+
assertion(control.caseId, 'execution-timeout.fails-closed', () => {
|
|
589
|
+
const { elapsedMs } = boundedLoopControlResult({
|
|
590
|
+
distribution: loopDistribution,
|
|
591
|
+
sources,
|
|
592
|
+
policy,
|
|
593
|
+
method: 'isolation'
|
|
594
|
+
}, control.caseId);
|
|
595
|
+
console.log(`EVIDENCE ${control.caseId} result=CHECKER_EXECUTION_TIMEOUT elapsedMs=${elapsedMs}`);
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
assertion('C1-timeout-sigkill-load-bearing', 'sigkill-terminates-sigterm-resistant-child', () => {
|
|
600
|
+
const { result, elapsedMs } = boundedSigkillControlResult('C1-timeout-sigkill-load-bearing');
|
|
601
|
+
assert.equal(result.coverage.complete, false, 'the actual C1 deadline leaves coverage incomplete');
|
|
602
|
+
const diagnosticCodes = result.diagnostics.map(item => item.code);
|
|
603
|
+
assert.deepEqual(diagnosticCodes, ['CHECKER_EXECUTION_TIMEOUT'],
|
|
604
|
+
`SIGKILL timeout is classified as CHECKER_EXECUTION_TIMEOUT; actual=${diagnosticCodes.join(',')}`);
|
|
605
|
+
console.log(`EVIDENCE C1-timeout-sigkill-load-bearing result=CHECKER_EXECUTION_TIMEOUT elapsedMs=${elapsedMs} outerDeadlineMs=${LOOP_CONTROL_CHILD_TIMEOUT_MS}`);
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
assertion('C1-host-bridge-probe', 'host-function-constructors-are-unreachable', () => {
|
|
609
|
+
const sources = {
|
|
610
|
+
evaluator: insertHostArgumentProbe(loopOriginal.evaluator, 'checkDocObligations'),
|
|
611
|
+
capture: insertHostArgumentProbe(
|
|
612
|
+
insertHostBridgeProbe(insertHostArrayCallbackProbe(loopOriginal.capture)), 'createInvocationCapture')
|
|
613
|
+
};
|
|
614
|
+
const { result, elapsedMs } = boundedDirectControlResult({
|
|
615
|
+
distribution: loopDistribution,
|
|
616
|
+
sources,
|
|
617
|
+
policy,
|
|
618
|
+
method: 'isolation'
|
|
619
|
+
}, 'C1-host-bridge-probe');
|
|
620
|
+
console.log(`EVIDENCE C1-host-bridge-probe complete=${result.coverage.complete} diagnostics=${result.diagnostics.map(item => item.code).join(',')} elapsedMs=${elapsedMs}`);
|
|
621
|
+
assert.equal(result.coverage.complete, true, 'blocked bridge probes leave C1 coverage complete');
|
|
622
|
+
assert.deepEqual(result.diagnostics.map(item => item.code), ['MODULE_IMPORT_SURFACE'],
|
|
623
|
+
'only the intentional require-surface mutation is diagnosed');
|
|
624
|
+
assert.ok(!result.diagnostics.some(item => item.code === 'MODULE_EXPORT_SURFACE'),
|
|
625
|
+
'neither exposed crypto nor require yields a host process export');
|
|
626
|
+
console.log(`EVIDENCE C1-host-bridge-probe result=BLOCKED elapsedMs=${elapsedMs}`);
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
assertion('C1-timeout-sigterm-resistant', 'escape-is-rejected-within-external-deadline', () => {
|
|
630
|
+
const sources = { ...loopOriginal, capture: insertSigtermResistantEscape(loopOriginal.capture) };
|
|
631
|
+
const { result, elapsedMs } = boundedDirectControlResult({
|
|
632
|
+
distribution: loopDistribution,
|
|
633
|
+
sources,
|
|
634
|
+
policy,
|
|
635
|
+
method: 'isolation'
|
|
636
|
+
}, 'C1-timeout-sigterm-resistant');
|
|
637
|
+
assert.equal(result.coverage.complete, false, 'the escaped SIGTERM-resistant child is rejected');
|
|
638
|
+
assert.ok(result.diagnostics.some(item => FAIL_CLOSED_CODES.includes(item.code)),
|
|
639
|
+
`the SIGTERM-resistant control must fail closed; diagnostics=${JSON.stringify(result.diagnostics)}`);
|
|
640
|
+
console.log(`EVIDENCE C1-timeout-sigterm-resistant result=${result.diagnostics[0].code} elapsedMs=${elapsedMs}`);
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
// Fingerprints are reviewed inputs. This run must never rewrite the policy or its pinned fingerprints.
|
|
644
|
+
assertion('C2-guard', 'boundary.policy-hash-stable', () => {
|
|
645
|
+
assert.equal(sha256(POLICY_PATH), policyHashAtStart, 'the frozen policy file is unchanged by the suite');
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
if (SELECTED_ASSERTION && !claimedAssertions.has(SELECTED_ASSERTION)) {
|
|
649
|
+
console.error(`infrastructure: unknown HARNESS_ASSERTION_ID ${SELECTED_ASSERTION}`);
|
|
650
|
+
process.exit(2);
|
|
651
|
+
}
|
|
652
|
+
if (SELECTED_CASE && !claimedCases.has(SELECTED_CASE)) {
|
|
653
|
+
console.error(`infrastructure: unknown HARNESS_CASE_ID ${SELECTED_CASE}`);
|
|
654
|
+
process.exit(2);
|
|
655
|
+
}
|
|
656
|
+
if (failures.length) {
|
|
657
|
+
console.error(`FAIL ${failures.length} enforcement assertion(s): ${failures.join(', ')}`);
|
|
658
|
+
process.exit(1);
|
|
659
|
+
}
|
|
660
|
+
console.log('PASS all C1 isolation controls, C2 AST controls, positive/fail-closed controls, and AST-path sensitivity');
|