@clear-capabilities/agentic-security-scanner 0.161.0 → 0.162.0

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.
Files changed (57) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/bin/agentic-security.js +11 -2
  3. package/dist/4113.index.js +59 -8
  4. package/dist/5613.index.js +500 -0
  5. package/dist/6239.index.js +469 -0
  6. package/dist/7838.index.js +38 -2
  7. package/dist/8798.index.js +1 -1
  8. package/dist/9207.index.js +5 -0
  9. package/dist/9560.index.js +36 -22
  10. package/dist/{1905.index.js → 9999.index.js} +265 -8
  11. package/dist/agentic-security.mjs +2 -2
  12. package/dist/agentic-security.mjs.sha256 +1 -1
  13. package/package.json +9 -4
  14. package/src/discovery/CLAUDE.md +9 -0
  15. package/src/discovery/index.js +28 -0
  16. package/src/discovery/memory.js +7 -5
  17. package/src/fix/apply-fix-service.js +33 -3
  18. package/src/mcp/CLAUDE.md +9 -1
  19. package/src/mcp/tools.js +17 -4
  20. package/src/posture/CLAUDE.md +174 -0
  21. package/src/posture/assurance/baseline-inventory.js +218 -0
  22. package/src/posture/assurance/baseline.js +322 -0
  23. package/src/posture/assurance/bounded-io.js +145 -0
  24. package/src/posture/assurance/config.js +299 -0
  25. package/src/posture/assurance/contracts.js +315 -0
  26. package/src/posture/assurance/identity.js +62 -0
  27. package/src/posture/assurance/migrations.js +259 -0
  28. package/src/posture/assurance/schema-kit.js +111 -0
  29. package/src/posture/assurance/verification-record.js +258 -0
  30. package/src/posture/attestation.js +5 -0
  31. package/src/posture/autopilot.js +28 -1
  32. package/src/posture/evidence-bundle.js +93 -7
  33. package/src/posture/execution-proof.js +11 -1
  34. package/src/posture/fix-verify-loop.js +20 -6
  35. package/src/posture/fix-verify.js +37 -2
  36. package/src/posture/oracles/adapters.js +360 -0
  37. package/src/posture/oracles/conformance.js +275 -0
  38. package/src/posture/oracles/oracle.js +433 -0
  39. package/src/posture/oracles/registry.js +76 -0
  40. package/src/posture/oracles/scenario-classes.js +223 -0
  41. package/src/posture/replay/replay.js +249 -0
  42. package/src/posture/verification/advisory-state.js +71 -0
  43. package/src/posture/verification/emit.js +257 -0
  44. package/src/posture/verification/hypothesis-promotion.js +211 -0
  45. package/src/posture/verification/patch-negative.js +214 -0
  46. package/src/posture/verification/patch-promotion.js +105 -0
  47. package/src/posture/verification/projection.js +183 -0
  48. package/src/posture/verification/repair-records.js +124 -0
  49. package/src/posture/verifier.js +16 -0
  50. package/src/report/index.js +49 -0
  51. package/src/sandbox/CLAUDE.md +41 -0
  52. package/src/sandbox/backend-namespace.js +7 -0
  53. package/src/sandbox/backend-userspace.js +68 -29
  54. package/src/sandbox/control-probes.js +215 -0
  55. package/src/sandbox/supervise.js +201 -0
  56. package/src/sandbox/trust-boundary.js +113 -0
  57. package/src/sandbox/trust-domains.js +171 -0
@@ -0,0 +1,275 @@
1
+ // The oracle conformance suite (X-208).
2
+ //
3
+ // One reusable contract that every registered adapter must satisfy, used three ways: by the test suite (all registered
4
+ // adapters plus deliberately bad fakes), by the release gate (`scripts/verification-conformance-check.mjs`), and by a
5
+ // contributor adding an oracle (docs/guides/verification-oracle-conformance.md). It checks an adapter, it never grants one
6
+ // anything: it imports no registration function and adds no execution permission.
7
+ //
8
+ // Two layers, because they have different costs and different host requirements:
9
+ //
10
+ // STATIC (`checkAdapterContract`, `checkFixturePins`): no execution, any host. An adapter must declare
11
+ // - CLASS SCOPE: a known class, a stated status and note per platform (Linux never `supported`: no Linux enforcement is
12
+ // verified by this build), stated limitations, and an existing fixture directory;
13
+ // - RESOURCE BUDGETS: all five budgets finite, positive and under the hard ceilings below;
14
+ // - NEGATIVE CONTROLS: at least one, each expecting `refuted`, and a negative fixture on disk that is a real file;
15
+ // - AUTHORITATIVE EVIDENCE: verifier-authored `harnessSource`, `prepare` and `interpret` (the verdict is formed by the
16
+ // verifier from what the run left behind, never by the target), and a `logicDigest` that matches its own logic;
17
+ // - PINNED FIXTURES: the positive, negative and inconclusive fixtures and the scenario hash to the values committed in
18
+ // `test/fixtures/oracles/conformance-pins.json`, together with the adapter's logic digest. A changed fixture or changed
19
+ // adapter logic fails until someone re-pins it on purpose.
20
+ //
21
+ // EXECUTION (`runAdapterConformance`): the adapter is run through `runOracle` and so through the trust boundary only.
22
+ // - STATE MAPPING: positive confirms, negative refutes with proven preconditions, inconclusive stays open;
23
+ // - RECEIPTS: the positive run's receipt is one `runOracle` issued (a copy is not), frozen, bound to the record id and the
24
+ // logic digest, with trusted-runtime-proof evidence from the trusted runner and sanitized environment metadata;
25
+ // - TAMPER: a target that rewrites the adapter harness yields `error` and no receipt; a target printing verdict-looking text
26
+ // cannot move a refutation; an unavailable prerequisite is `unsupported`, executes nothing and issues no receipt;
27
+ // - CANCELLATION: an aborted run is `inconclusive`, has no receipt and leaves no harness process behind;
28
+ // - REPLAY: running the positive request again reproduces the outcome and the record id.
29
+ // Where the boundary cannot run, execution is reported `not-run` with the reason; it is never reported as passed.
30
+ import fs from 'node:fs';
31
+ import path from 'node:path';
32
+ import { runOracle, validateOracleSpec, oracleLogicDigest, isIssuedReceipt, ORACLE_CLASSES, HARNESS_FILE, FEATURE_ID } from './oracle.js';
33
+ import { SCENARIO_CLASSES } from './scenario-classes.js';
34
+ import { digestOf } from '../assurance/identity.js';
35
+ import { validateVerificationRecord } from '../assurance/verification-record.js';
36
+ import { resolveAssuranceConfig } from '../assurance/config.js';
37
+ import { isPlainObject } from '../assurance/schema-kit.js';
38
+
39
+ const CONFORMANCE_SCHEMA = 'agentic-security/oracle-conformance';
40
+ export const PINS_FILE = 'test/fixtures/oracles/conformance-pins.json';
41
+
42
+ /** Hard ceilings the gate enforces on an adapter's declared budgets. A bigger budget is a bigger default execution permission. */
43
+ export const BUDGET_CEILINGS = Object.freeze({ timeoutMs: 60_000, graceMs: 10_000, maxOutputBytes: 1_048_576, maxFileBytes: 1_048_576, maxFiles: 64 });
44
+ const FIXTURE_KINDS = ['positive', 'negative', 'inconclusive'];
45
+ const CONFORMANCE_COMMIT = 'd'.repeat(40);
46
+
47
+ const entry = (check, ok, detail) => ({ check, ok: !!ok, detail });
48
+
49
+ // ---------------------------------------------------------------- static
50
+
51
+ /** Static contract problems for one adapter; `ok` only when every check passes. Never throws. */
52
+ export function checkAdapterContract(adapter, { scannerRoot } = {}) {
53
+ const checks = [];
54
+ try {
55
+ if (!isPlainObject(adapter)) return { id: null, ok: false, checks: [entry('spec', false, 'adapter must be an object')] };
56
+ const spec = { ...adapter }; delete spec.logicDigest;
57
+ const specProblems = validateOracleSpec(spec);
58
+ checks.push(entry('spec', specProblems.length === 0, specProblems.join('; ') || 'the adapter satisfies the adapter contract'));
59
+
60
+ // class scope
61
+ const scopeProblems = [];
62
+ if (!ORACLE_CLASSES.includes(adapter.class)) scopeProblems.push(`class '${adapter.class}' is not a known oracle class`);
63
+ if (!isPlainObject(adapter.platforms) || !['darwin', 'linux', 'win32'].every((p) => isPlainObject(adapter.platforms[p]) && adapter.platforms[p].status && adapter.platforms[p].note)) scopeProblems.push('every platform needs a stated status and note');
64
+ if (adapter.platforms?.linux?.status === 'supported') scopeProblems.push('Linux cannot be claimed supported');
65
+ if (!Array.isArray(adapter.limitations) || adapter.limitations.length === 0) scopeProblems.push('limitations must be stated');
66
+ if (!Array.isArray(adapter.prerequisites) || adapter.prerequisites.length === 0) scopeProblems.push('prerequisites must be declared');
67
+ const dir = scannerRoot && adapter.fixtures?.dir ? path.join(scannerRoot, adapter.fixtures.dir) : null;
68
+ if (scannerRoot && !(dir && fs.existsSync(dir))) scopeProblems.push('the fixture directory does not exist');
69
+ checks.push(entry('class-scope', scopeProblems.length === 0, scopeProblems.join('; ') || `class '${adapter.class}' with stated platforms, prerequisites and limitations`));
70
+
71
+ // resource budgets
72
+ const budgetProblems = [];
73
+ if (!isPlainObject(adapter.budgets)) budgetProblems.push('budgets are required');
74
+ else for (const [k, ceiling] of Object.entries(BUDGET_CEILINGS)) {
75
+ const v = adapter.budgets[k];
76
+ if (!Number.isInteger(v) || v <= 0) budgetProblems.push(`budgets.${k} must be a positive integer`);
77
+ else if (v > ceiling) budgetProblems.push(`budgets.${k} (${v}) exceeds the ceiling ${ceiling}`);
78
+ }
79
+ checks.push(entry('resource-budgets', budgetProblems.length === 0, budgetProblems.join('; ') || 'all five budgets are declared and under their ceilings'));
80
+
81
+ // negative controls
82
+ const negProblems = [];
83
+ if (!Array.isArray(adapter.negativeControls) || adapter.negativeControls.length === 0) negProblems.push('at least one negative control is required');
84
+ else if (!adapter.negativeControls.every((n) => isPlainObject(n) && n.id && n.description && n.expectedOutcome === 'refuted')) negProblems.push('each negative control needs an id, a description and expectedOutcome refuted');
85
+ if (scannerRoot && dir && adapter.fixtures?.negative) {
86
+ const f = path.join(dir, adapter.fixtures.negative, 'target.mjs');
87
+ if (!(fs.existsSync(f) && fs.statSync(f).isFile())) negProblems.push('the negative fixture is not a file on disk');
88
+ }
89
+ checks.push(entry('negative-controls', negProblems.length === 0, negProblems.join('; ') || 'a negative control is declared and its fixture exists'));
90
+
91
+ // authoritative evidence receipts
92
+ const evProblems = [];
93
+ if (typeof adapter.harnessSource !== 'string' || !adapter.harnessSource.includes('__INPUT__')) evProblems.push('a verifier-authored harness is required');
94
+ for (const fn of ['prepare', 'interpret']) if (typeof adapter[fn] !== 'function') evProblems.push(`${fn} (verifier-side) is required`);
95
+ if (typeof adapter.logicDigest !== 'string') evProblems.push('no logic digest');
96
+ else if (!specProblems.length && adapter.logicDigest !== oracleLogicDigest(spec)) evProblems.push('the logic digest does not match the adapter logic');
97
+ checks.push(entry('evidence-receipts', evProblems.length === 0, evProblems.join('; ') || 'the verdict is formed verifier-side from observed output and bound by a logic digest'));
98
+ } catch (e) {
99
+ checks.push(entry('contract', false, `the contract check failed: ${String(e?.message || e).slice(0, 160)}`));
100
+ }
101
+ return { id: adapter?.id ?? null, ok: checks.every((c) => c.ok), checks };
102
+ }
103
+
104
+ function readFixture(scannerRoot, adapter, kind) {
105
+ const dir = path.join(scannerRoot, adapter.fixtures.dir);
106
+ return {
107
+ target: fs.readFileSync(path.join(dir, adapter.fixtures[kind], 'target.mjs'), 'utf8'),
108
+ scenario: fs.readFileSync(path.join(dir, 'scenario.json'), 'utf8'),
109
+ };
110
+ }
111
+
112
+ /** The pin record for one adapter, from its fixtures and logic digest. Used to write and to check pins. */
113
+ export function computePins(adapter, scannerRoot) {
114
+ const pins = { logicDigest: adapter.logicDigest, scenario: null, fixtures: {} };
115
+ for (const kind of FIXTURE_KINDS) {
116
+ const f = readFixture(scannerRoot, adapter, kind);
117
+ pins.scenario = digestOf(f.scenario);
118
+ pins.fixtures[kind] = digestOf(f.target);
119
+ }
120
+ return pins;
121
+ }
122
+
123
+ export function loadPins(scannerRoot) {
124
+ try { return JSON.parse(fs.readFileSync(path.join(scannerRoot, PINS_FILE), 'utf8')); } catch { return null; }
125
+ }
126
+
127
+ /** The fixture pins for one adapter against the committed pin file. A missing pin is a failure, not a skip. */
128
+ export function checkFixturePins(adapter, scannerRoot, pins = loadPins(scannerRoot)) {
129
+ const id = adapter?.id;
130
+ const want = pins && isPlainObject(pins.adapters) ? pins.adapters[id] : null;
131
+ if (!want) return entry('pinned-fixtures', false, `no pinned fixtures for '${id}' in ${PINS_FILE}; pin them deliberately (node scripts/verification-conformance-check.mjs --update-pins)`);
132
+ let have;
133
+ try { have = computePins(adapter, scannerRoot); } catch (e) { return entry('pinned-fixtures', false, `the fixtures could not be read: ${String(e?.message || e).slice(0, 120)}`); }
134
+ const moved = [];
135
+ if (want.logicDigest !== have.logicDigest) moved.push('adapter logic digest');
136
+ if (want.scenario !== have.scenario) moved.push('scenario');
137
+ for (const kind of FIXTURE_KINDS) if (want.fixtures?.[kind] !== have.fixtures[kind]) moved.push(`${kind} fixture`);
138
+ return entry('pinned-fixtures', moved.length === 0, moved.length ? `changed since pinned: ${moved.join(', ')}` : 'adapter logic and the three fixtures match their pins');
139
+ }
140
+
141
+ // ---------------------------------------------------------------- execution
142
+
143
+ const enabledConfig = () => resolveAssuranceConfig({ env: {}, overrides: { features: { [FEATURE_ID]: true } } });
144
+
145
+ function requestFor(adapter, scannerRoot, kind, over = {}) {
146
+ const f = readFixture(scannerRoot, adapter, kind);
147
+ return {
148
+ oracleId: adapter.id, hypothesisId: `conformance-${adapter.id}`, commit: CONFORMANCE_COMMIT, entry: 'target.mjs',
149
+ files: { 'target.mjs': f.target }, inputs: JSON.parse(f.scenario),
150
+ // the parser oracle's positive case is ended by the supervisor deadline; keep it short
151
+ ...(adapter.class === 'parser-resource' ? { budgets: { timeoutMs: Math.min(2500, adapter.budgets.timeoutMs) } } : {}), ...over,
152
+ };
153
+ }
154
+
155
+ /**
156
+ * Run the execution contract for one adapter.
157
+ * @param {object} adapter
158
+ * @param {object} o
159
+ * @param {string} o.scannerRoot
160
+ * @param {function} [o.run] the runner (default `runOracle`); a stand-in lets a test model a misbehaving runner
161
+ * @param {object} [o.runOptions] extra options for the runner (`probeEnv`, `deps`)
162
+ * @param {function} [o.harnessProcesses] returns the number of leftover harness processes (cancellation check)
163
+ */
164
+ export async function runAdapterConformance(adapter, { scannerRoot, run = runOracle, runOptions = {}, harnessProcesses = null } = {}) {
165
+ const checks = [];
166
+ const registry = { getOracle: (id) => (id === adapter.id ? adapter : null) };
167
+ const go = (req, extra = {}) => run(req, { config: enabledConfig(), registry, ...runOptions, ...extra });
168
+ try {
169
+ const pos = await go(requestFor(adapter, scannerRoot, 'positive'));
170
+ if (pos.status === 'unsupported' || pos.status === 'blocked' || pos.status === 'disabled') {
171
+ return { id: adapter.id, ok: false, executed: false, checks: [entry('execution-available', false, `the oracle could not run on this host: ${String(pos.reason).slice(0, 200)}`)] };
172
+ }
173
+ // state mapping
174
+ checks.push(entry('state-positive', pos.outcome === 'confirmed' && pos.record?.confirmationLevel === 'runtime-confirmed', `positive fixture settled '${pos.outcome}'`));
175
+ const neg = await go(requestFor(adapter, scannerRoot, 'negative'));
176
+ checks.push(entry('state-negative', neg.outcome === 'refuted' && neg.record?.preconditions?.valid === true, `negative fixture settled '${neg.outcome}' (preconditions ${neg.record?.preconditions?.valid})`));
177
+ const inc = await go(requestFor(adapter, scannerRoot, 'inconclusive'));
178
+ checks.push(entry('state-inconclusive', inc.outcome === 'inconclusive' && inc.record?.preconditions?.valid === false && inc.record?.confirmationLevel === 'none', `inconclusive fixture settled '${inc.outcome}'`));
179
+ checks.push(entry('records-valid', [pos, neg, inc].every((r) => r.record && validateVerificationRecord(r.record).ok), 'every record validates against the version-1 schema'));
180
+
181
+ // authoritative evidence receipt
182
+ const r = pos.receipt;
183
+ const ev = pos.record?.evidence?.find((e) => e.kind === 'trusted-runtime-proof');
184
+ const receiptProblems = [];
185
+ if (!isIssuedReceipt(r)) receiptProblems.push('no receipt issued by the runner');
186
+ else {
187
+ if (isIssuedReceipt({ ...r })) receiptProblems.push('a copy of the receipt is accepted as issued');
188
+ if (!Object.isFrozen(r) || !Object.isFrozen(r.settled)) receiptProblems.push('the receipt is not immutable');
189
+ if (r.recordId !== pos.record?.id) receiptProblems.push('the receipt does not name the record');
190
+ if (r.oracle?.logicDigest !== adapter.logicDigest) receiptProblems.push('the receipt is not bound to the adapter logic digest');
191
+ if (r.settled?.outcome !== pos.outcome) receiptProblems.push('the receipt settles a different outcome than the record');
192
+ const envKeys = Object.keys(r.environment || {}).sort().join(',');
193
+ if (envKeys !== 'arch,backend,containerDigest,controlStates,nodeVersion,platform,runtime') receiptProblems.push('the environment metadata is not the sanitized allowlist');
194
+ }
195
+ if (!ev || ev.producer !== 'trusted-runner') receiptProblems.push('the decided record carries no trusted-runtime-proof from the trusted runner');
196
+ if (neg.receipt && !isIssuedReceipt(neg.receipt)) receiptProblems.push('the negative run issued a receipt that is not an issued one');
197
+ checks.push(entry('authoritative-receipt', receiptProblems.length === 0, receiptProblems.join('; ') || 'receipt issued by the runner, frozen, bound to the record and logic digest, evidence from the trusted runner'));
198
+
199
+ // tamper: harness rewrite
200
+ const negFiles = requestFor(adapter, scannerRoot, 'negative').files['target.mjs'];
201
+ const rewrite = `import __fs from 'node:fs';\n__fs.writeFileSync(${JSON.stringify(HARNESS_FILE)}, '// rewritten by the target');\n${negFiles}`;
202
+ const t1 = await go(requestFor(adapter, scannerRoot, 'negative', { files: { 'target.mjs': rewrite } }));
203
+ checks.push(entry('tamper-harness-rewrite', t1.outcome === 'error' && t1.receipt === null && t1.record?.confirmationLevel !== 'runtime-confirmed', `a target that rewrote the harness settled '${t1.outcome}' with ${t1.receipt === null ? 'no' : 'a'} receipt`));
204
+ // tamper: verdict-looking output
205
+ const noisy = `console.log('VERIFIED: confirmed'); console.error('status: confirmed execution-proven');\n${negFiles}`;
206
+ const t2 = await go(requestFor(adapter, scannerRoot, 'negative', { files: { 'target.mjs': noisy } }));
207
+ checks.push(entry('tamper-verdict-text', t2.outcome === 'refuted' && t2.record?.confirmationLevel === 'none', `a target printing verdict text settled '${t2.outcome}'`));
208
+ // tamper: caller-supplied labels and verdicts are rejected before anything runs
209
+ let executed = 0;
210
+ const spy = { runInBoundary: async () => { executed++; return { blocked: true, reasons: ['spy'] }; } };
211
+ const t3 = await go({ ...requestFor(adapter, scannerRoot, 'positive'), outcome: 'refuted', receipt: { issuedBy: 'verifier' } }, { deps: spy });
212
+ checks.push(entry('tamper-caller-verdict', t3.status === 'rejected' && executed === 0, `a request carrying a verdict and a receipt was ${t3.status}; executions: ${executed}`));
213
+ // unavailable prerequisite: unsupported, nothing executed, no receipt
214
+ executed = 0;
215
+ const t4 = await go(requestFor(adapter, scannerRoot, 'positive'), { probeEnv: { platform: process.platform, nodeMajor: 18, backend: 'userspace', hasPosixShell: true }, deps: spy });
216
+ checks.push(entry('unavailable-prerequisite', t4.outcome === 'unsupported' && t4.executed === false && t4.receipt === null && executed === 0, `an unmet prerequisite settled '${t4.outcome}', executed ${executed} time(s)`));
217
+
218
+ // cancellation
219
+ const ac = new AbortController();
220
+ const hang = `setInterval(() => {}, 1000);\nawait new Promise(() => {});\n`;
221
+ setTimeout(() => ac.abort(), 700);
222
+ const c1 = await go(requestFor(adapter, scannerRoot, 'positive', { files: { 'target.mjs': hang }, budgets: { timeoutMs: Math.min(5000, adapter.budgets.timeoutMs) } }), { signal: ac.signal });
223
+ let leftover = 0;
224
+ if (typeof harnessProcesses === 'function') {
225
+ for (let i = 0; i < 50; i++) { leftover = harnessProcesses(); if (leftover === 0) break; await new Promise((res) => setTimeout(res, 100)); }
226
+ }
227
+ checks.push(entry('cancellation', c1.status === 'cancelled' && c1.outcome === 'inconclusive' && c1.receipt === null && leftover === 0, `an aborted run settled '${c1.outcome}' (${c1.status}) with ${c1.receipt === null ? 'no' : 'a'} receipt; leftover harness processes: ${leftover}`));
228
+
229
+ // replay
230
+ const again = await go(requestFor(adapter, scannerRoot, 'positive'));
231
+ checks.push(entry('replay', again.outcome === pos.outcome && again.record?.id === pos.record?.id, `a second run of the pinned positive fixture settled '${again.outcome}' with ${again.record?.id === pos.record?.id ? 'the same' : 'a different'} record id`));
232
+ } catch (e) {
233
+ checks.push(entry('execution', false, `the execution contract failed to run: ${String(e?.message || e).slice(0, 160)}`));
234
+ }
235
+ return { id: adapter.id, ok: checks.every((c) => c.ok), executed: true, checks };
236
+ }
237
+
238
+ // ---------------------------------------------------------------- whole registry
239
+
240
+ /**
241
+ * The gate over a set of adapters. Static checks and pins always run. Execution runs only when `execute` is true; a host that
242
+ * cannot run the boundary reports `execution: 'not-run'` with the reason, and `requireExecution` turns that into a failure.
243
+ */
244
+ export async function checkConformance(adapters, { scannerRoot, execute = true, requireExecution = false, pins, run, runOptions, harnessProcesses } = {}) {
245
+ const results = [];
246
+ const ids = new Set();
247
+ for (const a of adapters) {
248
+ const staticResult = checkAdapterContract(a, { scannerRoot });
249
+ const checks = [...staticResult.checks];
250
+ if (scannerRoot) checks.push(checkFixturePins(a, scannerRoot, pins));
251
+ if (ids.has(a?.id)) checks.push(entry('unique-id', false, `duplicate adapter id '${a?.id}'`));
252
+ ids.add(a?.id);
253
+ let execution = 'not-run'; let executionReason = 'execution conformance was not requested';
254
+ if (execute && checks.every((c) => c.ok) && scannerRoot) {
255
+ const ex = await runAdapterConformance(a, { scannerRoot, run, runOptions, harnessProcesses });
256
+ if (ex.executed) { execution = ex.ok ? 'passed' : 'failed'; executionReason = null; checks.push(...ex.checks); }
257
+ else { execution = 'not-run'; executionReason = ex.checks[0]?.detail ?? 'unavailable'; }
258
+ } else if (execute && !checks.every((c) => c.ok)) {
259
+ executionReason = 'not attempted: the static contract failed';
260
+ }
261
+ const staticOk = checks.every((c) => c.ok);
262
+ const ok = staticOk && (execution === 'passed' || (execution === 'not-run' && !requireExecution));
263
+ results.push({ id: a?.id ?? null, class: a?.class ?? null, ok, execution, executionReason, checks });
264
+ }
265
+ // every advertised class must be served by a registered adapter
266
+ const missing = ORACLE_CLASSES.filter((c) => !adapters.some((a) => a?.class === c));
267
+ const unmapped = SCENARIO_CLASSES.filter((c) => !adapters.some((a) => a?.id === c.adapterId)).map((c) => c.id);
268
+ const registryProblems = [...missing.map((c) => `no adapter serves class '${c}'`), ...unmapped.map((c) => `non-taint class '${c}' names an adapter that is not registered`)];
269
+ return {
270
+ schema: CONFORMANCE_SCHEMA, schemaVersion: '1.0.0',
271
+ ok: results.every((r) => r.ok) && registryProblems.length === 0,
272
+ adapters: results, registryProblems,
273
+ executionRan: results.some((r) => r.execution === 'passed' || r.execution === 'failed'),
274
+ };
275
+ }