@clear-capabilities/agentic-security-scanner 0.161.0 → 0.163.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 (182) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/bin/agentic-security.js +98 -2
  3. package/dist/1340.index.js +1871 -0
  4. package/dist/3477.index.js +727 -0
  5. package/dist/3499.index.js +23 -3
  6. package/dist/4113.index.js +59 -8
  7. package/dist/5014.index.js +833 -0
  8. package/dist/5756.index.js +2 -2
  9. package/dist/6257.index.js +1 -1
  10. package/dist/6907.index.js +248 -0
  11. package/dist/6994.index.js +1 -1
  12. package/dist/7033.index.js +33 -5
  13. package/dist/7439.index.js +1822 -0
  14. package/dist/7838.index.js +38 -2
  15. package/dist/8798.index.js +1 -1
  16. package/dist/9207.index.js +5 -0
  17. package/dist/9560.index.js +2336 -27
  18. package/dist/957.index.js +1 -1
  19. package/dist/{1905.index.js → 9999.index.js} +306 -13
  20. package/dist/agentic-security.mjs +12 -12
  21. package/dist/agentic-security.mjs.sha256 +1 -1
  22. package/package.json +39 -7
  23. package/src/capabilities/CLAUDE.md +114 -0
  24. package/src/capabilities/address.js +73 -0
  25. package/src/capabilities/attack-coverage.js +93 -0
  26. package/src/capabilities/decide.js +256 -0
  27. package/src/capabilities/delegate.js +69 -0
  28. package/src/capabilities/egress.js +20 -0
  29. package/src/capabilities/hook-advice.js +50 -0
  30. package/src/capabilities/index.js +32 -0
  31. package/src/capabilities/manifest.js +344 -0
  32. package/src/capabilities/outbound.js +189 -0
  33. package/src/capabilities/paths.js +57 -0
  34. package/src/capabilities/probes.js +211 -0
  35. package/src/capabilities/proxy.js +182 -0
  36. package/src/capabilities/reasons.js +67 -0
  37. package/src/capabilities/receipts.js +356 -0
  38. package/src/capabilities/records.js +58 -0
  39. package/src/capabilities/recovery.js +367 -0
  40. package/src/capabilities/report.js +81 -0
  41. package/src/capabilities/runner.js +236 -0
  42. package/src/capabilities/secrets.js +26 -0
  43. package/src/capabilities/tool-gate.js +119 -0
  44. package/src/capabilities/tool-registry.js +60 -0
  45. package/src/dataflow/CLAUDE.md +29 -0
  46. package/src/dataflow/catalog.js +21 -0
  47. package/src/dataflow/engine.js +20 -0
  48. package/src/dataflow/guard-dominance.js +352 -0
  49. package/src/discovery/CLAUDE.md +18 -0
  50. package/src/discovery/index.js +43 -1
  51. package/src/discovery/lenses.js +18 -0
  52. package/src/discovery/memory.js +7 -5
  53. package/src/engine.js +80 -15
  54. package/src/fix/apply-fix-service.js +33 -3
  55. package/src/integrations/index.js +5 -2
  56. package/src/ir/CLAUDE.md +14 -0
  57. package/src/ir/implicit-handler-params.js +74 -0
  58. package/src/ir/parser-cs.js +20 -0
  59. package/src/ir/parser-go.js +30 -13
  60. package/src/ir/parser-java.js +11 -0
  61. package/src/language/nix-eval-isolation.js +33 -5
  62. package/src/lineage/CLAUDE.md +31 -0
  63. package/src/lineage/deployment/adapter-cloud.js +202 -0
  64. package/src/lineage/deployment/adapter-compose.js +90 -0
  65. package/src/lineage/deployment/adapter-kit.js +142 -0
  66. package/src/lineage/deployment/adapter-kubernetes.js +247 -0
  67. package/src/lineage/deployment/attack-paths.js +443 -0
  68. package/src/lineage/deployment/boundaries-run.js +223 -0
  69. package/src/lineage/deployment/boundary-graph.js +567 -0
  70. package/src/lineage/deployment/cross-repo.js +253 -0
  71. package/src/lineage/deployment/drift.js +383 -0
  72. package/src/lineage/deployment/ingest.js +221 -0
  73. package/src/lineage/deployment/projection.js +244 -0
  74. package/src/lineage/deployment/trace-correlation.js +541 -0
  75. package/src/lineage/source-registry.js +19 -0
  76. package/src/mcp/CLAUDE.md +14 -3
  77. package/src/mcp/invariant-tools.js +64 -0
  78. package/src/mcp/portfolio-tools.js +79 -0
  79. package/src/mcp/redact.js +15 -0
  80. package/src/mcp/server.js +24 -3
  81. package/src/mcp/tools.js +30 -5
  82. package/src/posture/CLAUDE.md +505 -0
  83. package/src/posture/accuracy-scorecard.js +95 -4
  84. package/src/posture/assurance/baseline-inventory.js +218 -0
  85. package/src/posture/assurance/baseline.js +322 -0
  86. package/src/posture/assurance/bounded-io.js +145 -0
  87. package/src/posture/assurance/config.js +299 -0
  88. package/src/posture/assurance/contracts.js +315 -0
  89. package/src/posture/assurance/identity.js +62 -0
  90. package/src/posture/assurance/migrations.js +259 -0
  91. package/src/posture/assurance/rollback.js +214 -0
  92. package/src/posture/assurance/rollout.js +200 -0
  93. package/src/posture/assurance/schema-kit.js +111 -0
  94. package/src/posture/assurance/verification-record.js +258 -0
  95. package/src/posture/attestation.js +5 -0
  96. package/src/posture/autopilot.js +28 -1
  97. package/src/posture/business-logic.js +26 -4
  98. package/src/posture/evaluation/bakeoff.js +148 -0
  99. package/src/posture/evaluation/custody.js +200 -0
  100. package/src/posture/evaluation/deployment-ablation.js +416 -0
  101. package/src/posture/evaluation/dev-cases.js +65 -0
  102. package/src/posture/evaluation/economics.js +85 -0
  103. package/src/posture/evaluation/gates.js +161 -0
  104. package/src/posture/evaluation/grouping.js +101 -0
  105. package/src/posture/evaluation/interval.js +132 -0
  106. package/src/posture/evaluation/invariant-ablation.js +325 -0
  107. package/src/posture/evaluation/labels.js +200 -0
  108. package/src/posture/evaluation/protocol.js +308 -0
  109. package/src/posture/evaluation/release-gate.js +209 -0
  110. package/src/posture/evaluation/report.js +158 -0
  111. package/src/posture/evaluation/runner.js +273 -0
  112. package/src/posture/evaluation/scan-child.js +80 -0
  113. package/src/posture/evaluation/score.js +174 -0
  114. package/src/posture/evaluation/synthetic.js +90 -0
  115. package/src/posture/evaluation/why-missed.js +219 -0
  116. package/src/posture/evidence-bundle.js +93 -7
  117. package/src/posture/execution-proof.js +11 -1
  118. package/src/posture/fix-verify-loop.js +20 -6
  119. package/src/posture/fix-verify.js +37 -2
  120. package/src/posture/fleet.js +11 -2
  121. package/src/posture/invariants/cli.js +95 -0
  122. package/src/posture/invariants/coverage.js +164 -0
  123. package/src/posture/invariants/export.js +178 -0
  124. package/src/posture/invariants/expressions.js +46 -0
  125. package/src/posture/invariants/lifecycle.js +289 -0
  126. package/src/posture/invariants/project-input.js +55 -0
  127. package/src/posture/invariants/repair.js +340 -0
  128. package/src/posture/invariants/run.js +45 -0
  129. package/src/posture/invariants/scenarios.js +311 -0
  130. package/src/posture/invariants/schema.js +167 -0
  131. package/src/posture/invariants/shrink.js +407 -0
  132. package/src/posture/invariants/state-assertions.js +165 -0
  133. package/src/posture/logic-claims.js +12 -0
  134. package/src/posture/model-routing.js +40 -0
  135. package/src/posture/oracles/adapters.js +531 -0
  136. package/src/posture/oracles/conformance.js +276 -0
  137. package/src/posture/oracles/oracle.js +453 -0
  138. package/src/posture/oracles/registry.js +76 -0
  139. package/src/posture/oracles/scenario-classes.js +223 -0
  140. package/src/posture/portfolio/backend.js +279 -0
  141. package/src/posture/portfolio/bundle.js +314 -0
  142. package/src/posture/portfolio/cli.js +142 -0
  143. package/src/posture/portfolio/manifest.js +263 -0
  144. package/src/posture/portfolio/progress.js +267 -0
  145. package/src/posture/portfolio/resume.js +130 -0
  146. package/src/posture/portfolio/retention.js +207 -0
  147. package/src/posture/portfolio/scheduler.js +488 -0
  148. package/src/posture/portfolio/signing.js +155 -0
  149. package/src/posture/portfolio/wording.js +59 -0
  150. package/src/posture/portfolio/work-units.js +405 -0
  151. package/src/posture/replay/replay.js +250 -0
  152. package/src/posture/routing/adapter.js +140 -0
  153. package/src/posture/routing/calibration.js +278 -0
  154. package/src/posture/routing/canary.js +141 -0
  155. package/src/posture/routing/control.js +35 -0
  156. package/src/posture/routing/decide.js +211 -0
  157. package/src/posture/routing/drift.js +137 -0
  158. package/src/posture/routing/economics.js +185 -0
  159. package/src/posture/routing/feedback.js +207 -0
  160. package/src/posture/routing/index.js +31 -0
  161. package/src/posture/routing/outcomes.js +281 -0
  162. package/src/posture/routing/promotion.js +291 -0
  163. package/src/posture/routing/receipts.js +59 -0
  164. package/src/posture/routing/report.js +142 -0
  165. package/src/posture/routing/shadow.js +53 -0
  166. package/src/posture/specification-mining.js +26 -0
  167. package/src/posture/verification/advisory-state.js +71 -0
  168. package/src/posture/verification/emit.js +257 -0
  169. package/src/posture/verification/hypothesis-promotion.js +211 -0
  170. package/src/posture/verification/patch-negative.js +226 -0
  171. package/src/posture/verification/patch-promotion.js +113 -0
  172. package/src/posture/verification/projection.js +183 -0
  173. package/src/posture/verification/repair-records.js +124 -0
  174. package/src/posture/verifier.js +16 -0
  175. package/src/report/index.js +100 -1
  176. package/src/sandbox/CLAUDE.md +51 -0
  177. package/src/sandbox/backend-namespace.js +7 -0
  178. package/src/sandbox/backend-userspace.js +151 -34
  179. package/src/sandbox/control-probes.js +215 -0
  180. package/src/sandbox/supervise.js +201 -0
  181. package/src/sandbox/trust-boundary.js +113 -0
  182. package/src/sandbox/trust-domains.js +171 -0
@@ -0,0 +1,200 @@
1
+ // Staged rollout policy for the assurance features (REL-002.AC01).
2
+ //
3
+ // Every feature in config.js is off by default. This module is the policy that says
4
+ // how a feature moves from "exists in the code" to "an operator may rely on it":
5
+ //
6
+ // offline-fixtures -> shadow-canary -> opt-in-supported
7
+ //
8
+ // offline-fixtures The feature runs against recorded fixtures in tests only. No real target.
9
+ // shadow-canary It may run beside the existing behaviour on real input, but its result is
10
+ // RECORDED, never used: no verdict, exit code or report line changes. A canary is a
11
+ // named, bounded subset an operator chose.
12
+ // opt-in-supported An operator enabled it (config.js), the platform is supported, and every gate for
13
+ // the stage is met. Only here may a result change an outcome.
14
+ //
15
+ // A stage is entered only by `promote`, one step at a time, and only when every documented gate of
16
+ // the stage being entered has evidence that matches the gate's own declared reference. There is no
17
+ // way to skip a stage, to promote a feature the kill switch has stopped, or to promote on a gate
18
+ // that is merely "not failing". Nothing here runs a suite or measures anything: it judges evidence a
19
+ // caller supplies, and a gate that names a suite only accepts evidence for THAT suite.
20
+ //
21
+ // The ledger is append-only history plus a current position. `rollback` (rollback.js) moves the
22
+ // position back to the last known-good entry and appends; it never rewrites or removes history.
23
+
24
+ import { FEATURES, featureStatus } from './config.js';
25
+
26
+ const ROLLOUT_LEDGER_SCHEMA = 'agentic-security/rollout-ledger@1';
27
+ export const STAGES = Object.freeze(['offline-fixtures', 'shadow-canary', 'opt-in-supported']);
28
+
29
+ export const STAGE_MEANING = Object.freeze({
30
+ 'offline-fixtures': 'Runs against recorded fixtures in tests only; no real target is touched.',
31
+ 'shadow-canary': 'May run beside existing behaviour on real input or a named canary subset; results are recorded and never used, so no verdict, exit code or report line changes.',
32
+ 'opt-in-supported': 'An operator enabled it, the platform is supported and every gate is met; only here may a result change an outcome.',
33
+ });
34
+
35
+ // Gate evidence kinds. `suite` names a closure step (scripts/release-closure.mjs) whose recorded result is
36
+ // the evidence; `measurement` and `review` name an artifact a person or a bench produced.
37
+ const suite = (id, closureStep, statement) => ({ id, kind: 'suite', closureStep, statement });
38
+ const measure = (id, statement) => ({ id, kind: 'measurement', statement });
39
+ const review = (id, statement) => ({ id, kind: 'review', statement });
40
+
41
+ // The suite that proves each feature's offline behaviour.
42
+ const FEATURE_SUITE = Object.freeze({
43
+ 'verification-oracles': 'verification',
44
+ 'patch-negative-verification': 'verification',
45
+ 'deployment-boundaries': 'deployment',
46
+ 'invariant-scenarios': 'invariants',
47
+ 'capability-enforcement': 'capabilities',
48
+ 'model-routing': 'routing',
49
+ 'portfolio-assurance': 'portfolio',
50
+ });
51
+
52
+ // What each feature must show before opt-in. Feature specific, and quoted from the PRD's own targets
53
+ // (section 5) rather than invented here.
54
+ const OPT_IN_SPECIFIC = Object.freeze({
55
+ 'verification-oracles': [measure('oracle-conformance', 'Every advertised adapter passes positive, negative, inconclusive and tamper controls (verification-conformance-gate); unavailable prerequisites are reported unsupported')],
56
+ 'patch-negative-verification': [measure('repair-evidence-triple', 'Every advertised verified fix carries original-positive, patched-negative and functional-regression evidence bound to the exact environment, patch and oracle')],
57
+ 'deployment-boundaries': [measure('paired-ablation', 'A frozen contextual ablation shows additional confirmed defects or fewer false positives with unchanged scoring, and no lost baseline defect')],
58
+ 'invariant-scenarios': [review('contract-approval-path', 'Invariant contracts are approved by an authorized reviewer, never by the generator that proposed them')],
59
+ 'capability-enforcement': [measure('backend-probes', 'Every mandatory canary-secret and escape fixture is blocked on the advertised backend with zero canary leakage; macOS stays supervise-only')],
60
+ 'model-routing': [measure('promotion-thresholds', 'At least 200 paired adjudicated tasks overall and 30 per promoted stratum, lower 95% quality bound above -0.02, median cost 20% lower or quality 0.05 higher at no higher median cost, p95 latency within 1.2x')],
61
+ 'portfolio-assurance': [measure('interrupted-run-convergence', 'Interrupted, duplicate-delivery and incremental runs converge to a fresh scoped result with no double-counted progress')],
62
+ });
63
+
64
+ /** The documented gates for entering `stage` with `feature`. Pure and frozen. */
65
+ export function gatesFor(feature, stage) {
66
+ if (!FEATURES[feature] || !STAGES.includes(stage)) return null;
67
+ const own = FEATURE_SUITE[feature];
68
+ if (stage === 'offline-fixtures') {
69
+ return [
70
+ suite('fixtures-green', own, `The ${own} suite passes against recorded fixtures`),
71
+ suite('default-off-unchanged', 'foundation', 'With the feature disabled, scan and report behaviour is unchanged (configuration and kill-switch tests)'),
72
+ ];
73
+ }
74
+ if (stage === 'shadow-canary') {
75
+ return [
76
+ suite('fixtures-green', own, `The ${own} suite still passes at the commit being promoted`),
77
+ measure('shadow-no-outcome-change', 'A recorded shadow run on real input changed no verdict, exit code or report line'),
78
+ suite('rollback-rehearsed', 'release-closure-suite', 'Rollback was rehearsed and restored known-good behaviour without erasing evidence'),
79
+ ];
80
+ }
81
+ return [
82
+ suite('fixtures-green', own, `The ${own} suite passes at the commit being promoted`),
83
+ suite('compatibility-holds', 'compat-nix', 'Existing Haskell/Nix and core-language behaviour is unchanged'),
84
+ suite('documentation-current', 'documentation-suite', 'Scope, examples and the policy card for this feature are published and drift-checked'),
85
+ review('canary-record', 'A shadow-canary record exists with no unresolved incident'),
86
+ ...(OPT_IN_SPECIFIC[feature] || []),
87
+ ];
88
+ }
89
+
90
+ /** Every feature's documented gates, for documentation and drift checks. */
91
+ export function describeRollout() {
92
+ return Object.keys(FEATURES).map((feature) => ({
93
+ feature, risk: FEATURES[feature].risk, platforms: [...FEATURES[feature].platforms],
94
+ stages: STAGES.map((stage) => ({ stage, meaning: STAGE_MEANING[stage], gates: gatesFor(feature, stage) })),
95
+ }));
96
+ }
97
+
98
+ // ---------------------------------------------------------------- ledger
99
+
100
+ const DIGEST = /^(?:sha256:)?[0-9a-f]{64}$/;
101
+
102
+ export function newLedger() {
103
+ const features = {};
104
+ for (const id of Object.keys(FEATURES)) {
105
+ features[id] = { stage: 'offline-fixtures', policyVersion: 1, history: [{ seq: 0, event: 'init', from: null, to: 'offline-fixtures', policyVersion: 1, knownGood: true, evidence: null }] };
106
+ }
107
+ return { schema: ROLLOUT_LEDGER_SCHEMA, features };
108
+ }
109
+
110
+ const clone = (x) => JSON.parse(JSON.stringify(x));
111
+
112
+ /**
113
+ * Judge supplied evidence against the gates for entering `target`.
114
+ * evidence: { [gateId]: { met: true, ref, digest } }. `ref` must equal the gate's own reference
115
+ * (a suite gate accepts only its closure step id); `digest` must be a sha256.
116
+ * -> { ok, unmet: [{ gate, reason }] }
117
+ */
118
+ export function evaluateGates(feature, target, evidence = {}) {
119
+ const gates = gatesFor(feature, target);
120
+ if (!gates) return { ok: false, unmet: [{ gate: null, reason: `unknown feature '${feature}' or stage '${target}'` }] };
121
+ const unmet = [];
122
+ for (const g of gates) {
123
+ const e = evidence?.[g.id];
124
+ if (!e || e.met !== true) { unmet.push({ gate: g.id, reason: 'no evidence that the gate is met' }); continue; }
125
+ if (typeof e.digest !== 'string' || !DIGEST.test(e.digest)) { unmet.push({ gate: g.id, reason: 'evidence carries no sha256 digest' }); continue; }
126
+ if (g.kind === 'suite' && e.ref !== g.closureStep) { unmet.push({ gate: g.id, reason: `evidence is for '${e.ref}', not the required suite '${g.closureStep}'` }); continue; }
127
+ if (g.kind !== 'suite' && (typeof e.ref !== 'string' || !e.ref.trim())) { unmet.push({ gate: g.id, reason: 'evidence names no artifact' }); continue; }
128
+ }
129
+ return { ok: unmet.length === 0, unmet };
130
+ }
131
+
132
+ /**
133
+ * Build gate evidence from recorded closure steps (id, state, log.sha256). Only a step in state
134
+ * `pass` produces evidence; an unsupported, incomplete or failed step produces none.
135
+ */
136
+ export function evidenceFromSteps(steps = []) {
137
+ const out = {};
138
+ for (const s of steps) {
139
+ if (s?.state !== 'pass' || !/^[0-9a-f]{64}$/.test(s.log?.sha256 || '')) continue;
140
+ out[`step:${s.id}`] = { met: true, ref: s.id, digest: s.log.sha256 };
141
+ }
142
+ return out;
143
+ }
144
+
145
+ /** Fill suite-gate evidence for `feature`/`target` from closure-step evidence; other kinds come from `extra`. */
146
+ export function assembleEvidence(feature, target, stepEvidence = {}, extra = {}) {
147
+ const out = { ...extra };
148
+ for (const g of gatesFor(feature, target) || []) {
149
+ if (g.kind === 'suite' && stepEvidence[`step:${g.closureStep}`]) out[g.id] = stepEvidence[`step:${g.closureStep}`];
150
+ }
151
+ return out;
152
+ }
153
+
154
+ /**
155
+ * Move one feature up one stage. Returns a NEW ledger; the input is never mutated.
156
+ * Refuses: an unknown feature, skipping a stage, standing still, any unmet gate, and a feature the
157
+ * kill switch has stopped (config given) or an unsupported platform for opt-in.
158
+ */
159
+ export function promote(ledger, feature, evidence, { config = null } = {}) {
160
+ const cur = ledger?.features?.[feature];
161
+ if (!cur) return { ok: false, ledger, blockers: [{ gate: null, reason: `unknown feature '${feature}'` }] };
162
+ const at = STAGES.indexOf(cur.stage);
163
+ if (at === STAGES.length - 1) return { ok: false, ledger, blockers: [{ gate: null, reason: 'already at the final stage' }] };
164
+ const target = STAGES[at + 1];
165
+ const blockers = [];
166
+ if (config) {
167
+ const st = featureStatus(config, feature);
168
+ if (st.code === 'kill-switch') blockers.push({ gate: null, reason: `the kill switch is set (${st.reason}); a stopped feature is not promoted` });
169
+ if (target === 'opt-in-supported' && st.code === 'platform-unsupported') blockers.push({ gate: null, reason: st.reason });
170
+ }
171
+ const g = evaluateGates(feature, target, evidence);
172
+ blockers.push(...g.unmet);
173
+ if (blockers.length) return { ok: false, ledger, blockers };
174
+ const next = clone(ledger);
175
+ const f = next.features[feature];
176
+ // Monotonic over the whole history: a version number is never reused after a rollback, so a record
177
+ // produced under a rolled-back policy can never be mistaken for one produced under a later policy.
178
+ const policyVersion = Math.max(...cur.history.map((h) => h.policyVersion)) + 1;
179
+ f.history.push({ seq: f.history.length, event: 'promote', from: cur.stage, to: target, policyVersion, knownGood: true, evidence: clone(evidence) });
180
+ f.stage = target; f.policyVersion = policyVersion;
181
+ return { ok: true, ledger: next, blockers: [] };
182
+ }
183
+
184
+ /**
185
+ * How a feature behaves given its configuration AND its stage. Below opt-in a feature that an operator
186
+ * enabled still only runs in the stage's mode, and says so.
187
+ * 'off' disabled, killed, invalid or unsupported: inert
188
+ * 'fixtures' offline-fixtures: the code path is fixtures-only; a real run is refused
189
+ * 'shadow' shadow-canary: run, record, never change an outcome
190
+ * 'active' opt-in-supported: results may change an outcome
191
+ */
192
+ export function effectiveMode(config, ledger, feature) {
193
+ const st = featureStatus(config, feature);
194
+ if (st.status !== 'ok') return { mode: 'off', stage: ledger?.features?.[feature]?.stage ?? null, reason: st.reason, code: st.code };
195
+ const stage = ledger?.features?.[feature]?.stage;
196
+ if (!stage) return { mode: 'off', stage: null, reason: 'the feature has no rollout record, so it has not passed any gate', code: 'no-rollout-record' };
197
+ if (stage === 'offline-fixtures') return { mode: 'fixtures', stage, reason: 'enabled, but the feature has not passed the shadow-canary gates; it runs against fixtures only', code: 'below-stage' };
198
+ if (stage === 'shadow-canary') return { mode: 'shadow', stage, reason: 'enabled, but at shadow-canary: results are recorded and never change an outcome', code: 'below-stage' };
199
+ return { mode: 'active', stage, reason: 'enabled at opt-in-supported', code: null };
200
+ }
@@ -0,0 +1,111 @@
1
+ // Small validation primitives shared by the assurance evidence contracts
2
+ // (CORE-002). Pure: no fs, no clock, no network. Every validator returns
3
+ // `{ ok, errors }` where each error is `{ code, path, message }`; nothing throws
4
+ // on bad input, because a validator that throws on hostile input is a denial of
5
+ // service on whatever called it.
6
+
7
+ export const ERROR_CODES = Object.freeze([
8
+ 'NOT_AN_OBJECT', 'SCHEMA_MISMATCH', 'UNSUPPORTED_MAJOR', 'BAD_VERSION', 'MISSING_FIELD', 'UNKNOWN_FIELD',
9
+ 'BAD_TYPE', 'UNKNOWN_ENUM', 'BAD_DIGEST', 'BAD_ID', 'ID_MISMATCH', 'DUPLICATE_ID', 'DANGLING_REF', 'RULE_VIOLATION',
10
+ ]);
11
+
12
+ export const SUPPORTED_MAJOR = 1;
13
+ export const SCHEMA_VERSION = '1.0.0';
14
+
15
+ const SEMVER = /^(\d+)\.(\d+)\.(\d+)$/;
16
+ const DIGEST = /^sha256:[0-9a-f]{64}$/;
17
+ const COMMIT = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
18
+
19
+ export function makeCtx() {
20
+ const errors = [];
21
+ return {
22
+ errors,
23
+ err(code, path, message) { errors.push({ code, path, message }); },
24
+ };
25
+ }
26
+
27
+ export function isPlainObject(v) {
28
+ return v !== null && typeof v === 'object' && !Array.isArray(v);
29
+ }
30
+
31
+ export function isDigest(v) { return typeof v === 'string' && DIGEST.test(v); }
32
+ export function isCommit(v) { return typeof v === 'string' && COMMIT.test(v); }
33
+ export function isNonEmptyString(v) { return typeof v === 'string' && v.trim().length > 0; }
34
+
35
+ /** Header check: `schema` must match and the major version must be supported. */
36
+ export function checkHeader(ctx, rec, schemaName) {
37
+ if (rec.schema !== schemaName) {
38
+ ctx.err('SCHEMA_MISMATCH', 'schema', `expected schema '${schemaName}', got ${JSON.stringify(rec.schema)}`);
39
+ return false;
40
+ }
41
+ const m = typeof rec.schemaVersion === 'string' ? SEMVER.exec(rec.schemaVersion) : null;
42
+ if (!m) {
43
+ ctx.err('BAD_VERSION', 'schemaVersion', `schemaVersion must be a semantic version, got ${JSON.stringify(rec.schemaVersion)}`);
44
+ return false;
45
+ }
46
+ if (Number(m[1]) !== SUPPORTED_MAJOR) {
47
+ ctx.err('UNSUPPORTED_MAJOR', 'schemaVersion', `major version ${m[1]} is not supported (supported: ${SUPPORTED_MAJOR})`);
48
+ return false;
49
+ }
50
+ return true;
51
+ }
52
+
53
+ /** Closed world: any field outside `allowed` is rejected, never silently carried. */
54
+ export function checkFields(ctx, rec, allowed, required) {
55
+ for (const k of Object.keys(rec)) {
56
+ if (!allowed.includes(k)) ctx.err('UNKNOWN_FIELD', k, `field '${k}' is not part of this schema`);
57
+ }
58
+ for (const k of required) {
59
+ if (rec[k] === undefined) ctx.err('MISSING_FIELD', k, `required field '${k}' is missing`);
60
+ }
61
+ // `createdAt` is informational and never enters an id; it must still be a string.
62
+ if (rec.createdAt !== undefined && typeof rec.createdAt !== 'string') ctx.err('BAD_TYPE', 'createdAt', 'must be a string');
63
+ }
64
+
65
+ export function checkEnum(ctx, path, value, allowed) {
66
+ if (typeof value !== 'string' || !allowed.includes(value)) {
67
+ ctx.err('UNKNOWN_ENUM', path, `${JSON.stringify(value)} is not one of: ${allowed.join(', ')}`);
68
+ return false;
69
+ }
70
+ return true;
71
+ }
72
+
73
+ export function checkDigest(ctx, path, value, { nullable = false } = {}) {
74
+ if (value === null && nullable) return true;
75
+ if (!isDigest(value)) { ctx.err('BAD_DIGEST', path, `must be 'sha256:' followed by 64 lowercase hex characters`); return false; }
76
+ return true;
77
+ }
78
+
79
+ export function checkCommit(ctx, path, value, { nullable = true } = {}) {
80
+ if (value === null && nullable) return true;
81
+ if (!isCommit(value)) { ctx.err('BAD_TYPE', path, 'must be a 40 or 64 character lowercase hex commit id'); return false; }
82
+ return true;
83
+ }
84
+
85
+ export function checkString(ctx, path, value) {
86
+ if (!isNonEmptyString(value)) { ctx.err('BAD_TYPE', path, 'must be a non-empty string'); return false; }
87
+ return true;
88
+ }
89
+
90
+ export function checkId(ctx, rec, expectedId) {
91
+ const prefix = expectedId.split(':')[0];
92
+ if (typeof rec.id !== 'string' || !rec.id.startsWith(`${prefix}:`)) {
93
+ ctx.err('BAD_ID', 'id', `id must start with '${prefix}:'`);
94
+ } else if (rec.id !== expectedId) {
95
+ ctx.err('ID_MISMATCH', 'id', `id does not match the record's semantic content (expected ${expectedId})`);
96
+ }
97
+ }
98
+
99
+ export function result(ctx) {
100
+ return { ok: ctx.errors.length === 0, errors: ctx.errors };
101
+ }
102
+
103
+ /** Reject non-object input with one typed error instead of throwing downstream. */
104
+ export function guardObject(rec) {
105
+ const ctx = makeCtx();
106
+ if (!isPlainObject(rec)) {
107
+ ctx.err('NOT_AN_OBJECT', '', 'record must be a plain object');
108
+ return { ctx, ok: false };
109
+ }
110
+ return { ctx, ok: true };
111
+ }
@@ -0,0 +1,258 @@
1
+ // The one verification record (CORE-002; X-201 builds every surface on it).
2
+ //
3
+ // Shape: hypothesis id, commit, detector origin, oracle, attempt, outcome,
4
+ // reason, evidence items + references, applicable scope, and a SEPARATE repair
5
+ // status.
6
+ //
7
+ // Structural honesty rules, enforced by the validator (not by convention):
8
+ // 1. The six outcomes are distinct: not-run, unsupported, inconclusive, error,
9
+ // refuted, confirmed. Nothing here collapses them to a boolean.
10
+ // 2. Evidence kinds are distinct: observation, inference, independent
11
+ // adjudication, trusted runtime proof. Each kind constrains who may have
12
+ // produced it: a model can only ever produce an inference; trusted runtime
13
+ // proof only comes from a trusted runner; an adjudication only from a human
14
+ // or an independent verifier.
15
+ // 3. `confirmed` needs referenced evidence of runtime proof or independent
16
+ // adjudication, so a model-generated verdict alone cannot yield it, and the
17
+ // record carries a `confirmationLevel` that must equal the level derived from
18
+ // its evidence ('runtime-confirmed' needs trusted-runtime-proof AND a runtime
19
+ // oracle). A forged level fails the derivation check.
20
+ // 4. `refuted` needs an applicable (non-model) oracle, valid preconditions and
21
+ // proving evidence: a missing taint path is not refutation of an
22
+ // authorization or workflow claim.
23
+ // 5. Repair status is a different axis from the outcome. `replay-verified`
24
+ // needs the id of the patched-negative verification record.
25
+
26
+ import {
27
+ SCHEMA_VERSION, makeCtx, result, guardObject, checkHeader, checkFields, checkEnum, checkDigest,
28
+ checkCommit, checkString, checkId, isPlainObject,
29
+ } from './schema-kit.js';
30
+ import { semanticId, ID_PREFIXES } from './identity.js';
31
+
32
+ export const VERIFICATION_SCHEMA = 'agentic-security/verification-record';
33
+
34
+ export const VERIFICATION_OUTCOMES = Object.freeze(['not-run', 'unsupported', 'inconclusive', 'error', 'refuted', 'confirmed']);
35
+ export const REPAIR_STATUSES = Object.freeze(['none', 'proposed', 'applied', 'replay-verified', 'rolled-back']);
36
+ export const EVIDENCE_KINDS = Object.freeze(['observation', 'inference', 'independent-adjudication', 'trusted-runtime-proof']);
37
+ export const EVIDENCE_PRODUCERS = Object.freeze(['model', 'tool', 'human', 'independent-verifier', 'trusted-runner']);
38
+ export const ORACLE_KINDS = Object.freeze(['runtime-replay', 'differential', 'static-proof', 'model', 'human', 'none']);
39
+ export const CONFIRMATION_LEVELS = Object.freeze(['runtime-confirmed', 'adjudicated', 'none']);
40
+
41
+ const RUNTIME_ORACLES = ['runtime-replay', 'differential'];
42
+ const APPLICABLE_FOR_REFUTATION = ['runtime-replay', 'differential', 'static-proof', 'human'];
43
+ const NO_EXECUTION_OUTCOMES = ['not-run', 'unsupported'];
44
+
45
+ const ALLOWED = [
46
+ 'schema', 'schemaVersion', 'id', 'hypothesisId', 'commit', 'detectorOrigin', 'oracle', 'attempt', 'outcome', 'reason',
47
+ 'evidence', 'evidenceRefs', 'scope', 'preconditions', 'confirmationLevel', 'repair', 'migration', 'createdAt',
48
+ ];
49
+ const REQUIRED = [
50
+ 'schema', 'schemaVersion', 'id', 'hypothesisId', 'commit', 'detectorOrigin', 'oracle', 'attempt', 'outcome', 'reason',
51
+ 'evidence', 'evidenceRefs', 'scope', 'preconditions', 'confirmationLevel', 'repair',
52
+ ];
53
+
54
+ // Fields that define WHAT was claimed and how it was checked. `createdAt` (a
55
+ // clock) and `migration` (how the record was represented) are deliberately not
56
+ // here, so neither can move an id.
57
+ export const VERIFICATION_ID_FIELDS = Object.freeze([
58
+ 'hypothesisId', 'commit', 'detectorOrigin', 'oracle', 'attempt', 'outcome', 'reason', 'evidence', 'evidenceRefs',
59
+ 'scope', 'preconditions', 'confirmationLevel', 'repair',
60
+ ]);
61
+
62
+ export function verificationRecordId(record) {
63
+ return semanticId(ID_PREFIXES.verification, record, VERIFICATION_ID_FIELDS);
64
+ }
65
+
66
+ /** The identity of the CLAIM (which hypothesis, at which commit, by which oracle, on which attempt), excluding its result. */
67
+ export function verificationClaimKey(record) {
68
+ return semanticId('vclaim', record, ['hypothesisId', 'commit', 'oracle', 'attempt']);
69
+ }
70
+
71
+ function referencedEvidence(record) {
72
+ const refs = new Set(Array.isArray(record.evidenceRefs) ? record.evidenceRefs : []);
73
+ return (Array.isArray(record.evidence) ? record.evidence : []).filter(e => isPlainObject(e) && refs.has(e.id));
74
+ }
75
+
76
+ /** The confirmation level the evidence actually supports. Never read from the record's own claim. */
77
+ export function deriveConfirmationLevel(record) {
78
+ if (!record || record.outcome !== 'confirmed') return 'none';
79
+ const ev = referencedEvidence(record);
80
+ const runtime = ev.some(e => e.kind === 'trusted-runtime-proof' && e.producer === 'trusted-runner');
81
+ if (runtime && RUNTIME_ORACLES.includes(record.oracle?.kind)) return 'runtime-confirmed';
82
+ const adjudicated = ev.some(e => e.kind === 'independent-adjudication' && ['human', 'independent-verifier'].includes(e.producer));
83
+ return adjudicated ? 'adjudicated' : 'none';
84
+ }
85
+
86
+ function validateEvidenceItem(ctx, e, i) {
87
+ const p = `evidence[${i}]`;
88
+ if (!isPlainObject(e)) { ctx.err('BAD_TYPE', p, 'evidence item must be an object'); return; }
89
+ for (const k of Object.keys(e)) {
90
+ if (!['id', 'kind', 'producer', 'digest', 'source'].includes(k)) ctx.err('UNKNOWN_FIELD', `${p}.${k}`, `field '${k}' is not part of an evidence item`);
91
+ }
92
+ checkString(ctx, `${p}.id`, e.id);
93
+ const kindOk = checkEnum(ctx, `${p}.kind`, e.kind, EVIDENCE_KINDS);
94
+ const prodOk = checkEnum(ctx, `${p}.producer`, e.producer, EVIDENCE_PRODUCERS);
95
+ checkDigest(ctx, `${p}.digest`, e.digest);
96
+ if (e.source !== undefined && e.source !== null) checkString(ctx, `${p}.source`, e.source);
97
+ if (!kindOk || !prodOk) return;
98
+ if (e.producer === 'model' && e.kind !== 'inference') {
99
+ ctx.err('RULE_VIOLATION', p, `a model-produced item can only be 'inference', not '${e.kind}'`);
100
+ }
101
+ if (e.kind === 'trusted-runtime-proof' && e.producer !== 'trusted-runner') {
102
+ ctx.err('RULE_VIOLATION', p, `'trusted-runtime-proof' can only be produced by a 'trusted-runner', not '${e.producer}'`);
103
+ }
104
+ if (e.kind === 'independent-adjudication' && !['human', 'independent-verifier'].includes(e.producer)) {
105
+ ctx.err('RULE_VIOLATION', p, `'independent-adjudication' can only be produced by a 'human' or 'independent-verifier', not '${e.producer}'`);
106
+ }
107
+ if (e.producer === 'trusted-runner' && !['trusted-runtime-proof', 'observation'].includes(e.kind)) {
108
+ ctx.err('RULE_VIOLATION', p, `a 'trusted-runner' produces 'trusted-runtime-proof' or 'observation', not '${e.kind}'`);
109
+ }
110
+ }
111
+
112
+ export function validateVerificationRecord(record) {
113
+ const g = guardObject(record);
114
+ const ctx = g.ctx;
115
+ if (!g.ok) return result(ctx);
116
+ if (!checkHeader(ctx, record, VERIFICATION_SCHEMA)) return result(ctx);
117
+ checkFields(ctx, record, ALLOWED, REQUIRED);
118
+
119
+ checkString(ctx, 'hypothesisId', record.hypothesisId);
120
+ checkCommit(ctx, 'commit', record.commit);
121
+
122
+ const origin = record.detectorOrigin;
123
+ if (!isPlainObject(origin)) ctx.err('BAD_TYPE', 'detectorOrigin', 'must be an object with a detector name');
124
+ else {
125
+ for (const k of Object.keys(origin)) if (!['detector', 'family', 'parser'].includes(k)) ctx.err('UNKNOWN_FIELD', `detectorOrigin.${k}`, 'not part of detectorOrigin');
126
+ checkString(ctx, 'detectorOrigin.detector', origin.detector);
127
+ }
128
+
129
+ const oracle = record.oracle;
130
+ let oracleKind = null;
131
+ if (oracle === null) oracleKind = 'none';
132
+ else if (!isPlainObject(oracle)) ctx.err('BAD_TYPE', 'oracle', 'must be an object or null');
133
+ else {
134
+ for (const k of Object.keys(oracle)) if (!['id', 'kind', 'version'].includes(k)) ctx.err('UNKNOWN_FIELD', `oracle.${k}`, 'not part of oracle');
135
+ checkString(ctx, 'oracle.id', oracle.id);
136
+ if (checkEnum(ctx, 'oracle.kind', oracle.kind, ORACLE_KINDS)) oracleKind = oracle.kind;
137
+ }
138
+
139
+ if (!Number.isInteger(record.attempt) || record.attempt < 0) ctx.err('BAD_TYPE', 'attempt', 'must be a non-negative integer');
140
+ const outcomeOk = checkEnum(ctx, 'outcome', record.outcome, VERIFICATION_OUTCOMES);
141
+ checkString(ctx, 'reason', record.reason);
142
+
143
+ // evidence items
144
+ const ids = new Set();
145
+ if (!Array.isArray(record.evidence)) ctx.err('BAD_TYPE', 'evidence', 'must be an array');
146
+ else {
147
+ record.evidence.forEach((e, i) => {
148
+ validateEvidenceItem(ctx, e, i);
149
+ if (isPlainObject(e) && typeof e.id === 'string') {
150
+ if (ids.has(e.id)) ctx.err('DUPLICATE_ID', `evidence[${i}].id`, `duplicate evidence id '${e.id}'`);
151
+ ids.add(e.id);
152
+ }
153
+ });
154
+ }
155
+ if (!Array.isArray(record.evidenceRefs)) ctx.err('BAD_TYPE', 'evidenceRefs', 'must be an array of evidence ids');
156
+ else {
157
+ const seen = new Set();
158
+ record.evidenceRefs.forEach((r, i) => {
159
+ if (!ids.has(r)) ctx.err('DANGLING_REF', `evidenceRefs[${i}]`, `evidence reference '${r}' does not resolve to an evidence item in this record`);
160
+ if (seen.has(r)) ctx.err('DUPLICATE_ID', `evidenceRefs[${i}]`, `evidence reference '${r}' listed twice`);
161
+ seen.add(r);
162
+ });
163
+ }
164
+
165
+ // scope
166
+ const scope = record.scope;
167
+ if (!isPlainObject(scope)) ctx.err('BAD_TYPE', 'scope', 'must be an object');
168
+ else {
169
+ for (const k of Object.keys(scope)) if (!['description', 'platform', 'backend'].includes(k)) ctx.err('UNKNOWN_FIELD', `scope.${k}`, 'not part of scope');
170
+ checkString(ctx, 'scope.description', scope.description);
171
+ checkString(ctx, 'scope.platform', scope.platform);
172
+ if (scope.backend !== undefined && scope.backend !== null) checkString(ctx, 'scope.backend', scope.backend);
173
+ }
174
+
175
+ // preconditions
176
+ const pre = record.preconditions;
177
+ if (!isPlainObject(pre) || typeof pre.valid !== 'boolean') ctx.err('BAD_TYPE', 'preconditions', 'must be an object with a boolean `valid`');
178
+
179
+ // repair: a separate axis
180
+ const repair = record.repair;
181
+ if (!isPlainObject(repair)) ctx.err('BAD_TYPE', 'repair', 'must be an object');
182
+ else {
183
+ for (const k of Object.keys(repair)) if (!['status', 'patchDigest', 'replayRecordId'].includes(k)) ctx.err('UNKNOWN_FIELD', `repair.${k}`, 'not part of repair');
184
+ if (checkEnum(ctx, 'repair.status', repair.status, REPAIR_STATUSES)) {
185
+ if (['applied', 'replay-verified', 'rolled-back'].includes(repair.status)) {
186
+ if (repair.patchDigest === undefined || repair.patchDigest === null) ctx.err('RULE_VIOLATION', 'repair.patchDigest', `repair status '${repair.status}' requires a patchDigest`);
187
+ }
188
+ if (repair.patchDigest !== undefined && repair.patchDigest !== null) checkDigest(ctx, 'repair.patchDigest', repair.patchDigest);
189
+ if (repair.status === 'replay-verified' && !(typeof repair.replayRecordId === 'string' && repair.replayRecordId.startsWith('vrec:'))) {
190
+ ctx.err('RULE_VIOLATION', 'repair.replayRecordId', `repair status 'replay-verified' requires the id of the patched-negative verification record`);
191
+ }
192
+ }
193
+ }
194
+
195
+ // confirmation level must be what the evidence supports
196
+ const levelOk = checkEnum(ctx, 'confirmationLevel', record.confirmationLevel, CONFIRMATION_LEVELS);
197
+ if (levelOk) {
198
+ const derived = deriveConfirmationLevel(record);
199
+ if (record.confirmationLevel !== derived) {
200
+ ctx.err('RULE_VIOLATION', 'confirmationLevel', `claimed '${record.confirmationLevel}' but the referenced evidence supports '${derived}'`);
201
+ }
202
+ }
203
+
204
+ // outcome rules
205
+ if (outcomeOk) {
206
+ const refEv = referencedEvidence(record);
207
+ const hasProof = refEv.some(e => ['trusted-runtime-proof', 'independent-adjudication'].includes(e.kind));
208
+ const attempt = record.attempt;
209
+ if (!NO_EXECUTION_OUTCOMES.includes(record.outcome) && Number.isInteger(attempt) && attempt < 1) {
210
+ ctx.err('RULE_VIOLATION', 'attempt', `outcome '${record.outcome}' requires attempt >= 1`);
211
+ }
212
+ if (NO_EXECUTION_OUTCOMES.includes(record.outcome)) {
213
+ if (Number.isInteger(attempt) && attempt !== 0) ctx.err('RULE_VIOLATION', 'attempt', `outcome '${record.outcome}' means nothing ran, so attempt must be 0`);
214
+ if (refEv.some(e => e.kind === 'trusted-runtime-proof')) ctx.err('RULE_VIOLATION', 'evidence', `outcome '${record.outcome}' cannot cite runtime proof: nothing executed`);
215
+ }
216
+ if (['confirmed', 'refuted'].includes(record.outcome)) {
217
+ if (record.commit === null) ctx.err('RULE_VIOLATION', 'commit', `outcome '${record.outcome}' must be bound to an exact commit`);
218
+ if (oracleKind === 'none') ctx.err('RULE_VIOLATION', 'oracle', `outcome '${record.outcome}' requires an oracle`);
219
+ if (oracleKind === 'model') ctx.err('RULE_VIOLATION', 'oracle', `a model oracle can never yield '${record.outcome}'`);
220
+ if (!hasProof) ctx.err('RULE_VIOLATION', 'evidenceRefs', `outcome '${record.outcome}' requires referenced trusted-runtime-proof or independent-adjudication evidence`);
221
+ }
222
+ if (record.outcome === 'confirmed' && record.confirmationLevel === 'none') {
223
+ ctx.err('RULE_VIOLATION', 'confirmationLevel', `a 'confirmed' record must carry a confirmation level above 'none'`);
224
+ }
225
+ if (record.outcome === 'refuted') {
226
+ if (oracleKind && !APPLICABLE_FOR_REFUTATION.includes(oracleKind)) ctx.err('RULE_VIOLATION', 'oracle', `oracle kind '${oracleKind}' is not applicable for refutation`);
227
+ if (!(isPlainObject(pre) && pre.valid === true)) ctx.err('RULE_VIOLATION', 'preconditions', `'refuted' requires valid preconditions: a check that could not run is not a refutation`);
228
+ }
229
+ }
230
+
231
+ checkId(ctx, record, verificationRecordId(record));
232
+ return result(ctx);
233
+ }
234
+
235
+ /** Build a complete, valid-by-construction record from the semantic fields; derives the level and the id. */
236
+ export function buildVerificationRecord(fields) {
237
+ const rec = {
238
+ schema: VERIFICATION_SCHEMA,
239
+ schemaVersion: SCHEMA_VERSION,
240
+ hypothesisId: fields.hypothesisId,
241
+ commit: fields.commit ?? null,
242
+ detectorOrigin: fields.detectorOrigin,
243
+ oracle: fields.oracle ?? null,
244
+ attempt: fields.attempt ?? 0,
245
+ outcome: fields.outcome,
246
+ reason: fields.reason,
247
+ evidence: fields.evidence ?? [],
248
+ evidenceRefs: fields.evidenceRefs ?? (fields.evidence ?? []).map(e => e.id),
249
+ scope: fields.scope,
250
+ preconditions: fields.preconditions ?? { valid: false },
251
+ repair: fields.repair ?? { status: 'none' },
252
+ };
253
+ rec.confirmationLevel = deriveConfirmationLevel(rec);
254
+ if (fields.migration) rec.migration = fields.migration;
255
+ if (fields.createdAt) rec.createdAt = fields.createdAt;
256
+ rec.id = verificationRecordId(rec);
257
+ return rec;
258
+ }
@@ -143,6 +143,11 @@ export function computeRunAttestation({
143
143
  try {
144
144
  att.signature = signLastScan(digest);
145
145
  att.signatureScope = 'per-install-hmac';
146
+ // CORE-003.AC03: say plainly who vouches. A symmetric install HMAC can be
147
+ // produced by anyone who can verify it, so it is self-issued by
148
+ // construction and can never be third-party certification.
149
+ att.trustBasis = 'self-issued-local-key';
150
+ att.independentlyCertified = false;
146
151
  } catch { /* signing is best-effort; an unsigned attestation is still valid */ }
147
152
  }
148
153
  return att;