@clear-capabilities/agentic-security-scanner 0.185.0 → 0.186.2

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 (174) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/bin/agentic-security.js +66 -18
  3. package/dist/1128.index.js +25 -21
  4. package/dist/{5817.index.js → 1602.index.js} +378 -29
  5. package/dist/2584.index.js +1 -1
  6. package/dist/4171.index.js +2 -2
  7. package/dist/{0b301d45b231f144a1b4.json → 4d06c8944ae121d7085c.json} +8 -8
  8. package/dist/5752.index.js +466 -0
  9. package/dist/7405.index.js +1 -1
  10. package/dist/8158.index.js +1713 -0
  11. package/dist/8798.index.js +1 -1
  12. package/dist/9382.index.js +2 -2
  13. package/dist/9560.index.js +1 -1
  14. package/dist/9579.index.js +1 -1
  15. package/dist/agentic-security.mjs +29 -29
  16. package/dist/agentic-security.mjs.sha256 +1 -1
  17. package/package.json +8 -8
  18. package/src/dataflow/CLAUDE.md +6 -3
  19. package/src/dataflow/access-paths.js +12 -2
  20. package/src/dataflow/demand-query.js +279 -40
  21. package/src/dataflow/dep-fingerprint.js +1 -1
  22. package/src/dataflow/exploit-prover.js +6 -2
  23. package/src/dataflow/ir-cache.js +268 -0
  24. package/src/dataflow/privacy-taint.js +7 -0
  25. package/src/engine.js +62 -5
  26. package/src/ir/CLAUDE.md +4 -0
  27. package/src/ir/callgraph.js +36 -8
  28. package/src/ir/index.js +18 -4
  29. package/src/ir/parser-js.js +10 -0
  30. package/src/language/CLAUDE.md +13 -0
  31. package/src/language/nix-option-types.js +170 -0
  32. package/src/language/nix-script-taint.js +46 -3
  33. package/src/language/nix-secrets.js +20 -2
  34. package/src/lineage/CLAUDE.md +2 -1
  35. package/src/lineage/smt-orchestrator.js +47 -11
  36. package/src/lineage/smt-z3-backend.js +294 -0
  37. package/src/posture/CLAUDE.md +5 -1
  38. package/src/posture/business-logic.js +16 -7
  39. package/src/posture/dag-commands.js +442 -0
  40. package/src/posture/finding-defaults.js +19 -0
  41. package/src/posture/git-history.js +91 -1
  42. package/src/posture/proof-fabric/cli.js +1 -1
  43. package/src/posture/proof-fabric/container.js +2 -2
  44. package/src/posture/proof-fabric/generate-haskell.js +205 -0
  45. package/src/posture/proof-fabric/generate.js +6 -3
  46. package/src/posture/proof-fabric/reproducer.js +4 -2
  47. package/src/posture/proof-fabric/runner.js +4 -4
  48. package/src/sast/CLAUDE.md +2 -0
  49. package/src/sast/_call-args.js +70 -0
  50. package/src/sast/init-scope.js +87 -0
  51. package/src/sast/listen-bind-address.js +49 -0
  52. package/src/sast/redirect-target.js +106 -0
  53. package/src/sast/reflected-input.js +76 -0
  54. package/src/sast/send-file-root.js +27 -0
  55. package/src/sast/sensitive-data-logging.js +38 -1
  56. package/dist/119.index.js +0 -197
  57. package/dist/128.index.js +0 -129
  58. package/dist/1293.index.js +0 -5689
  59. package/dist/1336.index.js +0 -16331
  60. package/dist/1351.index.js +0 -133
  61. package/dist/1387.index.js +0 -271
  62. package/dist/1437.index.js +0 -23
  63. package/dist/1445.index.js +0 -110
  64. package/dist/1505.index.js +0 -151
  65. package/dist/1557.index.js +0 -293
  66. package/dist/1621.index.js +0 -311
  67. package/dist/1751.index.js +0 -160
  68. package/dist/1765.index.js +0 -727
  69. package/dist/1861.index.js +0 -690
  70. package/dist/1906.index.js +0 -433
  71. package/dist/208.index.js +0 -1631
  72. package/dist/2223.index.js +0 -159
  73. package/dist/2238.index.js +0 -295
  74. package/dist/2530.index.js +0 -152
  75. package/dist/2565.index.js +0 -116
  76. package/dist/2574.index.js +0 -127
  77. package/dist/2620.index.js +0 -345
  78. package/dist/2773.index.js +0 -98
  79. package/dist/2775.index.js +0 -32
  80. package/dist/2966.index.js +0 -171
  81. package/dist/2972.index.js +0 -87
  82. package/dist/2991.index.js +0 -131
  83. package/dist/3063.index.js +0 -833
  84. package/dist/3075.index.js +0 -227
  85. package/dist/3121.index.js +0 -297
  86. package/dist/3314.index.js +0 -263
  87. package/dist/3480.index.js +0 -1822
  88. package/dist/3481.index.js +0 -24
  89. package/dist/3506.index.js +0 -268
  90. package/dist/3533.index.js +0 -591
  91. package/dist/3653.index.js +0 -450
  92. package/dist/3740.index.js +0 -722
  93. package/dist/3822.index.js +0 -131
  94. package/dist/3868.index.js +0 -292
  95. package/dist/3918.index.js +0 -192
  96. package/dist/4090.index.js +0 -288
  97. package/dist/4113.index.js +0 -1286
  98. package/dist/4121.index.js +0 -1190
  99. package/dist/4142.index.js +0 -690
  100. package/dist/4168.index.js +0 -509
  101. package/dist/4281.index.js +0 -993
  102. package/dist/4336.index.js +0 -436
  103. package/dist/4387.index.js +0 -532
  104. package/dist/4413.index.js +0 -302
  105. package/dist/4476.index.js +0 -4510
  106. package/dist/4491.index.js +0 -133
  107. package/dist/4538.index.js +0 -185
  108. package/dist/4684.index.js +0 -319
  109. package/dist/4740.index.js +0 -165
  110. package/dist/4780.index.js +0 -209
  111. package/dist/4802.index.js +0 -225
  112. package/dist/4812.index.js +0 -379
  113. package/dist/4835.index.js +0 -126
  114. package/dist/494.index.js +0 -226
  115. package/dist/4991.index.js +0 -549
  116. package/dist/5199.index.js +0 -305
  117. package/dist/5216.index.js +0 -109
  118. package/dist/5439.index.js +0 -301
  119. package/dist/5443.index.js +0 -97
  120. package/dist/5496.index.js +0 -377
  121. package/dist/5738.index.js +0 -240
  122. package/dist/5801.index.js +0 -588
  123. package/dist/5858.index.js +0 -223
  124. package/dist/5933.index.js +0 -143
  125. package/dist/601.index.js +0 -23
  126. package/dist/6173.index.js +0 -273
  127. package/dist/6291.index.js +0 -1871
  128. package/dist/6312.index.js +0 -666
  129. package/dist/634.index.js +0 -16182
  130. package/dist/644.index.js +0 -87
  131. package/dist/6508.index.js +0 -283
  132. package/dist/6560.index.js +0 -645
  133. package/dist/6576.index.js +0 -422
  134. package/dist/6579.index.js +0 -271
  135. package/dist/6655.index.js +0 -3221
  136. package/dist/6745.index.js +0 -305
  137. package/dist/6818.index.js +0 -21642
  138. package/dist/6932.index.js +0 -1190
  139. package/dist/6972.index.js +0 -167
  140. package/dist/6995.index.js +0 -163
  141. package/dist/7249.index.js +0 -191
  142. package/dist/7372.index.js +0 -1197
  143. package/dist/7546.index.js +0 -541
  144. package/dist/76.index.js +0 -448
  145. package/dist/7879.index.js +0 -163
  146. package/dist/8025.index.js +0 -130
  147. package/dist/8054.index.js +0 -248
  148. package/dist/808.index.js +0 -1330
  149. package/dist/8097.index.js +0 -201
  150. package/dist/8126.index.js +0 -191
  151. package/dist/8178.index.js +0 -78
  152. package/dist/8382.index.js +0 -93
  153. package/dist/8408.index.js +0 -261
  154. package/dist/8540.index.js +0 -174
  155. package/dist/8635.index.js +0 -802
  156. package/dist/8688.index.js +0 -410
  157. package/dist/8699.index.js +0 -779
  158. package/dist/8891.index.js +0 -457
  159. package/dist/8916.index.js +0 -885
  160. package/dist/9146.index.js +0 -170
  161. package/dist/9168.index.js +0 -841
  162. package/dist/9292.index.js +0 -369
  163. package/dist/9422.index.js +0 -866
  164. package/dist/9440.index.js +0 -453
  165. package/dist/9479.index.js +0 -152
  166. package/dist/9481.index.js +0 -220
  167. package/dist/9539.index.js +0 -100
  168. package/dist/9547.index.js +0 -103
  169. package/dist/9692.index.js +0 -90
  170. package/dist/9800.index.js +0 -462
  171. package/dist/9899.index.js +0 -138
  172. package/dist/9981.index.js +0 -206
  173. package/dist/999.index.js +0 -117
  174. package/dist/9999.index.js +0 -1698
@@ -0,0 +1,294 @@
1
+ //
2
+ // smt-z3-backend.js: a real solver for the SMT orchestrator (DAG-04, R04.3 / R04.4).
3
+ //
4
+ // The orchestrator (smt-orchestrator.js) is built against an injectable backend object
5
+ // { name, version?, available?, check(problem, { signal }) -> Promise<{ result, reason? }> }.
6
+ // This module is that backend, on top of the optional `z3-solver` package. Rules it keeps:
7
+ //
8
+ // * LAZY and OPTIONAL. `z3-solver` is imported on the first query, never at module load, and never by a
9
+ // default scan (the whole path is behind `pathEvidence` and `AGENTIC_SECURITY_LINEAGE_SMT=z3`). A package
10
+ // that is not installed, fails to load or fails to initialise makes the backend `unsupported`
11
+ // (`available === false`): never a crash and never an `unsat`.
12
+ // * ONLY A VERIFIED ENCODING MAY SAY `unsat`. `encodeProblem` refuses (result `unknown`, with a reason) any
13
+ // problem it cannot translate faithfully: a variable used with two sorts, a number SMT-LIB cannot write
14
+ // exactly, an unknown literal or operator. An opaque (unmodeled) predicate is encoded as `true`, which can
15
+ // only make the formula LARGER, so `unsat` stays sound; for the same reason a `sat` that leaned on an opaque
16
+ // predicate is reported `unknown`, not as a model.
17
+ // * Numbers are Real, not Int, so a comparison such as `0.5 < x < 1` is not wrongly called infeasible.
18
+ // * BOUNDED. A per-query timeout (`timeoutMs`), a process memory cap (`memoryMb`, Z3's `memory_max_size`),
19
+ // and the orchestrator's own deadline racing the query. A timeout is `timeout`, memory exhaustion and any
20
+ // other solver failure is `unknown`. None of them ever reduces anything.
21
+ // * DETERMINISTIC. Variables are renamed v0..vN in sorted order, assertions keep their order, the solver
22
+ // random seed is fixed, and the SMT-LIB text is the cache key. Only definitive answers (`sat`, `unsat`)
23
+ // are cached; a timeout depends on the machine and is not.
24
+ //
25
+ // `unsat` here means "the encoded conditions cannot hold together", relative to the model stated in
26
+ // path-predicates.js. It is not a proof about the running program, and `sat` never implies exploitability.
27
+
28
+ import crypto from 'node:crypto';
29
+ import { createRequire } from 'node:module';
30
+
31
+ const require = createRequire(import.meta.url);
32
+
33
+ export const Z3_BACKEND_VERSION = 'smt-z3/1';
34
+ export const Z3_DEFAULT_TIMEOUT_MS = 1000;
35
+ export const Z3_DEFAULT_MEMORY_MB = 256;
36
+ const CACHE_MAX = 256;
37
+ const HARD_GRACE_MS = 500;
38
+ // 1e21 and 1e-7 are where String(number) switches to exponent form, which SMT-LIB has no literal for.
39
+ const MAX_PLAIN = 1e15;
40
+
41
+ // ---- encoding ------------------------------------------------------------------------------------------
42
+
43
+ const OPS = { '<': '<', '<=': '<=', '>': '>', '>=': '>=', '==': '=', '===': '=', '!=': 'distinct', '!==': 'distinct' };
44
+
45
+ /** A finite JS number as an SMT-LIB Real literal, or null when it cannot be written exactly in plain decimal. */
46
+ function realLiteral(n) {
47
+ if (typeof n !== 'number' || !Number.isFinite(n) || Math.abs(n) >= MAX_PLAIN) return null;
48
+ const s = String(n);
49
+ if (/e/i.test(s)) return null;
50
+ const abs = Math.abs(n);
51
+ const body = Number.isInteger(abs) ? `${abs}.0` : String(abs);
52
+ return n < 0 ? `(- ${body})` : body;
53
+ }
54
+
55
+ /** A JS string as an SMT-LIB 2.6 string literal: only printable ASCII is written raw. */
56
+ function stringLiteral(s) {
57
+ let out = '';
58
+ for (const ch of String(s)) {
59
+ const cp = ch.codePointAt(0);
60
+ if (ch === '"') out += '""';
61
+ else if (cp >= 0x20 && cp <= 0x7e && ch !== '\\') out += ch;
62
+ else out += `\\u{${cp.toString(16)}}`;
63
+ }
64
+ return `"${out}"`;
65
+ }
66
+
67
+ /**
68
+ * Translate the solver-neutral problem (path-predicates.js `toSolverProblem`) into SMT-LIB 2 text.
69
+ *
70
+ * @returns {{ok:true, text:string, digest:string, unmodeled:number, sorts:object, literals:number, assertions:number}
71
+ * | {ok:false, reason:string}}
72
+ */
73
+ export function encodeProblem(problem) {
74
+ if (!problem || typeof problem !== 'object' || !Array.isArray(problem.assertions) || !Array.isArray(problem.vars)) {
75
+ return { ok: false, reason: 'encoding-unverified:malformed-problem' };
76
+ }
77
+ const sorts = new Map();
78
+ const claim = (v, sort) => {
79
+ if (typeof v !== 'string') return false;
80
+ if (sorts.has(v) && sorts.get(v) !== sort) return false;
81
+ sorts.set(v, sort);
82
+ return true;
83
+ };
84
+ const constants = problem.constants && typeof problem.constants === 'object' ? problem.constants : {};
85
+ let literals = 0; let unmodeled = 0;
86
+ const alt = [];
87
+ for (const a of problem.assertions) {
88
+ if (!a || !Array.isArray(a.alternatives)) return { ok: false, reason: 'encoding-unverified:malformed-assertion' };
89
+ for (const conj of a.alternatives) {
90
+ if (!Array.isArray(conj)) return { ok: false, reason: 'encoding-unverified:malformed-assertion' };
91
+ for (const l of conj) {
92
+ literals += 1;
93
+ if (!l || typeof l !== 'object') return { ok: false, reason: 'encoding-unverified:malformed-literal' };
94
+ if (l.t === 'num') {
95
+ if (!OPS[l.op]) return { ok: false, reason: 'encoding-unverified:unknown-operator' };
96
+ for (const s of [l.left, l.right]) {
97
+ if (!s || typeof s !== 'object') return { ok: false, reason: 'encoding-unverified:malformed-literal' };
98
+ if ('var' in s) { if (!claim(s.var, 'Real')) return { ok: false, reason: 'encoding-unverified:sort-conflict' }; }
99
+ else if (realLiteral(s.const) === null) return { ok: false, reason: 'encoding-unverified:number-not-encodable' };
100
+ }
101
+ } else if (l.t === 'eq') {
102
+ if (typeof l.value !== 'string') return { ok: false, reason: 'encoding-unverified:non-string-equality' };
103
+ if (!claim(l.var, 'String')) return { ok: false, reason: 'encoding-unverified:sort-conflict' };
104
+ } else if (l.t === 'bool') {
105
+ if (!claim(l.var, 'Bool')) return { ok: false, reason: 'encoding-unverified:sort-conflict' };
106
+ } else if (l.t === 'opaque') {
107
+ unmodeled += 1;
108
+ } else {
109
+ return { ok: false, reason: 'encoding-unverified:unknown-literal' };
110
+ }
111
+ }
112
+ }
113
+ }
114
+ const constEntries = [];
115
+ for (const [k, v] of Object.entries(constants)) {
116
+ if (!sorts.has(k)) continue; // a constant for a variable no condition reads constrains nothing
117
+ if (typeof v === 'number') {
118
+ if (!claim(k, 'Real')) return { ok: false, reason: 'encoding-unverified:sort-conflict' };
119
+ if (realLiteral(v) === null) return { ok: false, reason: 'encoding-unverified:number-not-encodable' };
120
+ } else if (typeof v === 'string') {
121
+ if (!claim(k, 'String')) return { ok: false, reason: 'encoding-unverified:sort-conflict' };
122
+ } else return { ok: false, reason: 'encoding-unverified:constant-type' };
123
+ constEntries.push([k, v]);
124
+ }
125
+ // Rename to v0..vN in sorted order: no escaping hazard, and two distinct names can never merge.
126
+ const names = [...sorts.keys()].sort();
127
+ const id = new Map(names.map((n, i) => [n, `v${i}`]));
128
+ const lines = ['(set-logic ALL)'];
129
+ for (const n of names) lines.push(`(declare-const ${id.get(n)} ${sorts.get(n)})`);
130
+ const side = (s) => ('var' in s ? id.get(s.var) : realLiteral(s.const));
131
+ let asserted = 0;
132
+ for (const a of problem.assertions) {
133
+ const alts = a.alternatives.map((conj) => {
134
+ const parts = conj.map((l) => {
135
+ if (l.t === 'num') return `(${OPS[l.op]} ${side(l.left)} ${side(l.right)})`;
136
+ if (l.t === 'eq') return l.equal ? `(= ${id.get(l.var)} ${stringLiteral(l.value)})` : `(distinct ${id.get(l.var)} ${stringLiteral(l.value)})`;
137
+ if (l.t === 'bool') return l.truth ? id.get(l.var) : `(not ${id.get(l.var)})`;
138
+ return 'true'; // opaque: no constraint
139
+ });
140
+ return parts.length === 0 ? 'true' : parts.length === 1 ? parts[0] : `(and ${parts.join(' ')})`;
141
+ });
142
+ lines.push(`(assert ${alts.length === 0 ? 'false' : alts.length === 1 ? alts[0] : `(or ${alts.join(' ')})`})`);
143
+ asserted += 1;
144
+ }
145
+ for (const [k, v] of constEntries.sort((x, y) => (x[0] < y[0] ? -1 : 1))) {
146
+ lines.push(`(assert (= ${id.get(k)} ${typeof v === 'number' ? realLiteral(v) : stringLiteral(v)}))`);
147
+ asserted += 1;
148
+ }
149
+ lines.push('(check-sat)');
150
+ const text = lines.join('\n');
151
+ // Structural self-check of the translation: every assertion of the problem became exactly one assert.
152
+ const emitted = lines.filter((l) => l.startsWith('(assert ')).length;
153
+ if (emitted !== problem.assertions.length + constEntries.length || asserted !== emitted) {
154
+ return { ok: false, reason: 'encoding-unverified:assertion-count' };
155
+ }
156
+ return {
157
+ ok: true, text, unmodeled, literals, assertions: emitted,
158
+ sorts: Object.fromEntries(names.map((n) => [n, sorts.get(n)])),
159
+ digest: crypto.createHash('sha256').update(text).digest('hex').slice(0, 16),
160
+ };
161
+ }
162
+
163
+ // ---- the backend ---------------------------------------------------------------------------------------
164
+
165
+ /**
166
+ * The default loader. Loaded through a createRequire call at first use, the way the optional tree-sitter
167
+ * runtime is: a bundler keeps it external, and a build without the package fails here (reported as
168
+ * unsupported by the caller), never at start-up.
169
+ */
170
+ async function defaultLoader() {
171
+ return require('z3-solver');
172
+ }
173
+
174
+ /**
175
+ * Build a z3 backend.
176
+ *
177
+ * @param {object} [o]
178
+ * @param {number} [o.timeoutMs] per-query solver timeout (Z3 `timeout`)
179
+ * @param {number} [o.memoryMb] process memory cap handed to Z3 (`memory_max_size`)
180
+ * @param {()=>Promise<object>} [o.loader] returns the `z3-solver` module; injected by tests
181
+ */
182
+ export function createZ3Solver({ timeoutMs = Z3_DEFAULT_TIMEOUT_MS, memoryMb = Z3_DEFAULT_MEMORY_MB, loader = defaultLoader } = {}) {
183
+ const state = { initP: null, failed: null, api: null, ctx: null, version: null, queue: Promise.resolve() };
184
+ const cache = new Map();
185
+ const stats = { queries: 0, cacheHits: 0, loads: 0 };
186
+
187
+ function init() {
188
+ if (state.initP) return state.initP;
189
+ state.initP = (async () => {
190
+ stats.loads += 1;
191
+ const mod = await loader();
192
+ const initFn = mod?.init ?? mod?.default?.init;
193
+ if (typeof initFn !== 'function') throw new Error('z3-solver has no init()');
194
+ const api = await initFn();
195
+ try { api.Z3.global_param_set('memory_max_size', String(memoryMb)); } catch { /* an older build without the knob */ }
196
+ state.api = api;
197
+ state.ctx = api.Context('lineage-smt');
198
+ try { state.version = api.getVersionString?.() ?? null; } catch { state.version = null; }
199
+ return state;
200
+ })().catch((e) => {
201
+ state.failed = String(e?.code || e?.name || 'load-error');
202
+ state.initP = null;
203
+ return null;
204
+ });
205
+ return state.initP;
206
+ }
207
+
208
+ async function run(enc, signal) {
209
+ const ctx = state.ctx;
210
+ const solver = new ctx.Solver();
211
+ let abortHook;
212
+ try {
213
+ solver.set('timeout', timeoutMs);
214
+ try { solver.set('random_seed', 0); } catch { /* not a parameter of this build */ }
215
+ solver.fromString(enc.text);
216
+ if (signal) {
217
+ abortHook = () => { try { state.api.Z3.interrupt(ctx.ptr); } catch { /* best effort */ } };
218
+ signal.addEventListener('abort', abortHook, { once: true });
219
+ }
220
+ // Z3 honours `timeout`, but a wedged wasm call must not hold the queue forever: past a grace period the
221
+ // backend gives up on this context and reports itself unavailable from then on.
222
+ let hardTimer;
223
+ const hung = new Promise((resolve) => { hardTimer = setTimeout(() => resolve('hung'), timeoutMs + HARD_GRACE_MS); });
224
+ const verdict = await Promise.race([solver.check(), hung]).finally(() => clearTimeout(hardTimer));
225
+ if (verdict === 'hung') {
226
+ try { state.api.Z3.interrupt(ctx.ptr); } catch { /* best effort */ }
227
+ state.failed = 'hung';
228
+ return { result: 'timeout', reason: 'z3-hung' };
229
+ }
230
+ if (verdict === 'unsat') return { result: 'unsat' };
231
+ if (verdict === 'sat') return { result: 'sat' };
232
+ const why = String(solver.reasonUnknown?.() ?? '');
233
+ if (/timeout/i.test(why)) return { result: 'timeout', reason: 'z3-timeout' };
234
+ if (/memout|memory/i.test(why)) return { result: 'unknown', reason: 'z3-memory' };
235
+ if (/cancel/i.test(why)) return { result: 'unknown', reason: 'z3-cancelled' };
236
+ return { result: 'unknown', reason: 'z3-unknown' };
237
+ } finally {
238
+ if (abortHook && signal) signal.removeEventListener('abort', abortHook);
239
+ }
240
+ }
241
+
242
+ const backend = {
243
+ name: 'z3',
244
+ get version() { return state.version ? `${state.version}+${Z3_BACKEND_VERSION}` : Z3_BACKEND_VERSION; },
245
+ /** False only after a load/initialisation failure; true (unknown yet) before the first query. */
246
+ get available() { return state.failed === null; },
247
+ get loadFailure() { return state.failed; },
248
+ stats() { return { ...stats, cacheSize: cache.size }; },
249
+ /** Never throws. `unsupported` when z3 is missing, `unknown` when the encoding cannot be verified. */
250
+ async check(problem, { signal } = {}) {
251
+ stats.queries += 1;
252
+ const enc = encodeProblem(problem);
253
+ if (!enc.ok) return { result: 'unknown', reason: enc.reason };
254
+ const key = `${timeoutMs}|${enc.text}`;
255
+ const hit = cache.get(key);
256
+ if (hit) { stats.cacheHits += 1; cache.delete(key); cache.set(key, hit); return finish(hit, enc, true); }
257
+ const ready = await init();
258
+ if (!ready || state.failed) return { result: 'unsupported', reason: `z3-unavailable:${state.failed}` };
259
+ // One query at a time: the wasm context is single-threaded.
260
+ const turn = state.queue.then(() => run(enc, signal)).catch(() => ({ result: 'unknown', reason: 'z3-error' }));
261
+ state.queue = turn.then(() => {}, () => {});
262
+ const raw = await turn;
263
+ if (raw.result === 'sat' || raw.result === 'unsat') {
264
+ cache.set(key, raw);
265
+ if (cache.size > CACHE_MAX) cache.delete(cache.keys().next().value);
266
+ }
267
+ return finish(raw, enc, false);
268
+ },
269
+ /** Release the wasm worker threads. The next query re-initialises. */
270
+ async shutdown() {
271
+ const api = state.api;
272
+ state.initP = null; state.api = null; state.ctx = null;
273
+ if (!api) return;
274
+ try { const kt = (await loader())?.killThreads ?? null; if (kt && api.em) kt(api.em); } catch { /* nothing to release */ }
275
+ },
276
+ };
277
+ return backend;
278
+ }
279
+
280
+ function finish(raw, enc, cached) {
281
+ // A model that leaned on an unmodeled predicate says nothing about the real conditions.
282
+ if (raw.result === 'sat' && enc.unmodeled > 0) return { result: 'unknown', reason: 'sat-with-unmodeled-predicates', encodingDigest: enc.digest, cached };
283
+ return { ...raw, encodingDigest: enc.digest, cached };
284
+ }
285
+
286
+ let _shared = null;
287
+ /** The process-wide backend used when AGENTIC_SECURITY_LINEAGE_SMT=z3. */
288
+ export function sharedZ3Solver() {
289
+ if (!_shared) _shared = createZ3Solver();
290
+ return _shared;
291
+ }
292
+ export function smtBackendEnabled(env = process.env) {
293
+ return env.AGENTIC_SECURITY_LINEAGE_SMT === 'z3';
294
+ }
@@ -1030,7 +1030,11 @@ A sibling of `evidence-bundle.js` for one source-to-sink PATH rather than one fi
1030
1030
  - **Proof binding.** An external proof reference must name this bundle's sink node, data element, graph digest and configuration digest; a stale or mismatched one is listed under `exploit.rejected` with reasons and is never re-attached.
1031
1031
  - **Partial stays partial.** Completion fields are required (a missing one is invalid, never "complete"), the bundle is JSON-normalized at build so serialization cannot change it, and replay re-derives the path terminal, the truncation marks and `overall`.
1032
1032
  - **Redaction.** Identities become `idt_` tokens (`lineage/identity-token.js`), trace/session ids become `sref_` references, free text goes through `mcp/redact.js`, unknown input fields are dropped and only counted.
1033
- - **Limits.** Building needs the hop stream, which a scan does not persist; there is no `attest --witness` yet. Source files are bound only when the caller supplies their text (the rest are listed `unboundSourceFiles` and replay warns). The graph-edge to provenance-edge relation is the flow binding (`graphBinding.flowId`), checked for existence, not re-derived. Cross-service replay depends on DAG-08.
1033
+ - **Limits.** Building needs the hop stream, which a scan does not persist; `witness build` (`dag-commands.js`) therefore re-runs the field analysis rather than reading a stored stream. Source files are bound only when the caller supplies their text (the rest are listed `unboundSourceFiles` and replay warns). The graph-edge to provenance-edge relation is the flow binding (`graphBinding.flowId`), checked for existence, not re-derived. Cross-service replay depends on DAG-08.
1034
+
1035
+ ## Opt-in DAG commands: `dag-commands.js` (DAG-01, 02, 06, 08, 09, 10)
1036
+
1037
+ CLI entry points for capabilities that were library-only: `dataflow view` (typed graph view plus SCC condensation), `dataflow contracts` (boundary contracts via `runBoundaryTaint`), `dataflow sink-query` (demand reverse taint), the completion contract of `dataflow plan-cuts` (`planContract`; the command body is still in `bin/agentic-security.js`), and `witness build|verify`. Dispatched lazily from `bin/agentic-security.js`, so a default scan never imports it and its output is unchanged. Every report carries a `completion` record in the shared vocabulary and `negativeClaimAllowed` is false unless the engine's own envelope earned it. Exit codes: 0 report produced, 1 nothing usable or verification failed, 2 usage. Suite: `test/cli/dag-commands.test.js` (in `test:lineage`). User docs: `docs/security-graph/graph-precision.md` (Commands section).
1034
1038
 
1035
1039
  ## Gotchas
1036
1040
 
@@ -154,19 +154,28 @@ function collectStateSets(fileContents) {
154
154
  }
155
155
 
156
156
  function extractStateMachine(fileContents) {
157
- // Then look for direct writes like `.status = "<not in set>"` and flag.
157
+ // Then look for direct writes like `.status = "<not in set>"` and flag. A write is
158
+ // compared only against the status sets declared in the SAME file: a set in one
159
+ // module says nothing about what another module's `.status` may hold, and matching
160
+ // every `.status =` in the repository against the first set found anywhere flags
161
+ // unrelated objects. Test files are skipped (they assign arbitrary states on purpose).
158
162
  const stateSets = collectStateSets(fileContents);
159
163
  if (!stateSets.length) return [];
164
+ const byFile = new Map();
165
+ for (const s of stateSets) {
166
+ if (!byFile.has(s.file)) byFile.set(s.file, []);
167
+ byFile.get(s.file).push(s);
168
+ }
160
169
  const findings = [];
161
- for (const [fp, c] of Object.entries(fileContents || {})) {
162
- if (typeof c !== 'string' || !JS_TS_RE.test(fp)) continue;
170
+ for (const [fp, sets] of byFile) {
171
+ const c = fileContents[fp];
172
+ if (typeof c !== 'string' || /(^|\/)(test|tests|__tests__|fixtures?)\//i.test(fp) || /\.(test|spec)\.[cm]?[jt]sx?$/i.test(fp)) continue;
163
173
  const writeRe = /\.\s*(?:status|state|phase)\s*=\s*['"]([^'"]+)['"]/g;
164
174
  let m;
165
175
  while ((m = writeRe.exec(c))) {
166
176
  const value = m[1];
167
- const validSet = stateSets.find(s => s.values.size > 0);
168
- if (!validSet) continue;
169
- if (validSet.values.has(value)) continue;
177
+ if (sets.some(s => s.values.has(value))) continue;
178
+ const validSet = sets[0];
170
179
  const line = _lineOf(c, m.index);
171
180
  findings.push({
172
181
  id: `state-machine:${fp}:${line}:${value}`,
@@ -179,7 +188,7 @@ function extractStateMachine(fileContents) {
179
188
  parser: 'STATE-MACHINE',
180
189
  confidence: 0.5,
181
190
  snippet: m[0].slice(0, 200),
182
- remediation: `The statuses recognized by your system appear to be {${[...validSet.values].join(', ')}}. This write sets a different value ('${value}'). If the new value is legitimate, add it to the set; otherwise treat the write as a state-machine bypass and reject it.`,
191
+ remediation: `The statuses recognized by this file appear to be {${[...validSet.values].join(', ')}}. This write sets a different value ('${value}'). If the new value is legitimate, add it to the set; otherwise treat the write as a state-machine bypass and reject it.`,
183
192
  });
184
193
  }
185
194
  }