@crediolabs/policy-synth 0.2.0 → 0.3.1

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 (249) hide show
  1. package/README.md +4 -4
  2. package/dist/adapters/interpreter/adapter.d.ts +19 -25
  3. package/dist/adapters/interpreter/adapter.js +54 -487
  4. package/dist/errors.d.ts +1 -1
  5. package/dist/index.d.ts +1 -4
  6. package/dist/index.js +1 -4
  7. package/dist/install/build-add-context-rule.d.ts +3 -19
  8. package/dist/install/build-add-context-rule.js +3 -98
  9. package/dist/install/build-install-policy.d.ts +4 -53
  10. package/dist/install/build-install-policy.js +44 -123
  11. package/dist/install/index.d.ts +0 -2
  12. package/dist/install/index.js +0 -7
  13. package/dist/predicate/decode.d.ts +1 -1
  14. package/dist/predicate/decode.js +2 -70
  15. package/dist/predicate/encode.js +45 -229
  16. package/dist/predicate/from-json.js +1 -42
  17. package/dist/record/decode.js +3 -8
  18. package/dist/record/freshness.d.ts +17 -11
  19. package/dist/record/freshness.js +28 -18
  20. package/dist/review-card/builder.d.ts +4 -3
  21. package/dist/review-card/builder.js +18 -162
  22. package/dist/review-card/cross-check.js +2 -105
  23. package/dist/review-card/render-leaf.d.ts +4 -0
  24. package/dist/review-card/render-leaf.js +47 -0
  25. package/dist/run/index.d.ts +36 -139
  26. package/dist/run/index.js +125 -437
  27. package/dist/run/schemas.d.ts +168 -1929
  28. package/dist/run/schemas.js +40 -269
  29. package/dist/{synth → simulate}/deny-cases.d.ts +4 -8
  30. package/dist/simulate/deny-cases.js +282 -0
  31. package/dist/simulate/evaluate.d.ts +18 -0
  32. package/dist/simulate/evaluate.js +271 -0
  33. package/dist/simulate/index.d.ts +4 -0
  34. package/dist/simulate/index.js +8 -0
  35. package/dist/synth/compose-from-recording.d.ts +24 -51
  36. package/dist/synth/compose-from-recording.js +120 -282
  37. package/dist/synth/index.d.ts +0 -5
  38. package/dist/synth/index.js +0 -5
  39. package/dist/synth/synthesize-from-recording.d.ts +1 -29
  40. package/dist/synth/synthesize-from-recording.js +41 -356
  41. package/dist/types.d.ts +8 -60
  42. package/dist/types.js +5 -2
  43. package/dist-cjs/adapters/interpreter/adapter.d.ts +19 -25
  44. package/dist-cjs/adapters/interpreter/adapter.js +56 -489
  45. package/dist-cjs/errors.d.ts +1 -1
  46. package/dist-cjs/index.d.ts +1 -4
  47. package/dist-cjs/index.js +1 -4
  48. package/dist-cjs/install/build-add-context-rule.d.ts +3 -19
  49. package/dist-cjs/install/build-add-context-rule.js +1 -97
  50. package/dist-cjs/install/build-install-policy.d.ts +4 -53
  51. package/dist-cjs/install/build-install-policy.js +44 -122
  52. package/dist-cjs/install/index.d.ts +0 -2
  53. package/dist-cjs/install/index.js +2 -23
  54. package/dist-cjs/predicate/decode.d.ts +1 -1
  55. package/dist-cjs/predicate/decode.js +2 -70
  56. package/dist-cjs/predicate/encode.js +45 -229
  57. package/dist-cjs/predicate/from-json.js +1 -42
  58. package/dist-cjs/record/decode.js +3 -8
  59. package/dist-cjs/record/freshness.d.ts +17 -11
  60. package/dist-cjs/record/freshness.js +28 -18
  61. package/dist-cjs/review-card/builder.d.ts +4 -3
  62. package/dist-cjs/review-card/builder.js +23 -167
  63. package/dist-cjs/review-card/cross-check.js +7 -110
  64. package/dist-cjs/review-card/render-leaf.d.ts +4 -0
  65. package/dist-cjs/review-card/render-leaf.js +52 -0
  66. package/dist-cjs/run/index.d.ts +36 -139
  67. package/dist-cjs/run/index.js +125 -444
  68. package/dist-cjs/run/schemas.d.ts +168 -1929
  69. package/dist-cjs/run/schemas.js +41 -270
  70. package/dist-cjs/{synth → simulate}/deny-cases.d.ts +4 -8
  71. package/dist-cjs/simulate/deny-cases.js +286 -0
  72. package/dist-cjs/simulate/evaluate.d.ts +18 -0
  73. package/dist-cjs/simulate/evaluate.js +274 -0
  74. package/dist-cjs/simulate/index.d.ts +4 -0
  75. package/dist-cjs/simulate/index.js +13 -0
  76. package/dist-cjs/synth/compose-from-recording.d.ts +24 -51
  77. package/dist-cjs/synth/compose-from-recording.js +120 -282
  78. package/dist-cjs/synth/index.d.ts +0 -5
  79. package/dist-cjs/synth/index.js +1 -11
  80. package/dist-cjs/synth/synthesize-from-recording.d.ts +1 -29
  81. package/dist-cjs/synth/synthesize-from-recording.js +39 -354
  82. package/dist-cjs/types.d.ts +8 -60
  83. package/dist-cjs/types.js +6 -3
  84. package/package.json +5 -5
  85. package/src/adapters/interpreter/adapter.ts +76 -572
  86. package/src/errors.ts +0 -19
  87. package/src/index.ts +1 -4
  88. package/src/install/build-add-context-rule.ts +5 -139
  89. package/src/install/build-install-policy.ts +93 -214
  90. package/src/install/index.ts +0 -34
  91. package/src/predicate/decode.ts +2 -70
  92. package/src/predicate/encode.ts +49 -261
  93. package/src/predicate/from-json.ts +1 -43
  94. package/src/record/decode.ts +3 -8
  95. package/src/record/freshness.ts +29 -18
  96. package/src/review-card/builder.ts +19 -168
  97. package/src/review-card/cross-check.ts +3 -105
  98. package/src/review-card/render-leaf.ts +48 -0
  99. package/src/run/index.ts +144 -572
  100. package/src/run/schemas.ts +42 -291
  101. package/src/simulate/deny-cases.ts +334 -0
  102. package/src/simulate/evaluate.ts +284 -0
  103. package/src/simulate/index.ts +11 -0
  104. package/src/synth/compose-from-recording.ts +148 -347
  105. package/src/synth/index.ts +0 -13
  106. package/src/synth/synthesize-from-recording.ts +43 -456
  107. package/src/types.ts +13 -49
  108. package/dist/adapters/interpreter/index.d.ts +0 -1
  109. package/dist/adapters/interpreter/index.js +0 -2
  110. package/dist/adapters/oz/adapter.d.ts +0 -20
  111. package/dist/adapters/oz/adapter.js +0 -295
  112. package/dist/adapters/oz/index.d.ts +0 -1
  113. package/dist/adapters/oz/index.js +0 -2
  114. package/dist/codegen/compile-gate.d.ts +0 -33
  115. package/dist/codegen/compile-gate.js +0 -124
  116. package/dist/codegen/index.d.ts +0 -2
  117. package/dist/codegen/index.js +0 -8
  118. package/dist/codegen/template.d.ts +0 -18
  119. package/dist/codegen/template.js +0 -148
  120. package/dist/install/authority-overlap.d.ts +0 -134
  121. package/dist/install/authority-overlap.js +0 -0
  122. package/dist/install/build-install-predicate.d.ts +0 -96
  123. package/dist/install/build-install-predicate.js +0 -444
  124. package/dist/install/build-merge-policy.d.ts +0 -70
  125. package/dist/install/build-merge-policy.js +0 -130
  126. package/dist/install/plan-merge-policy.d.ts +0 -49
  127. package/dist/install/plan-merge-policy.js +0 -86
  128. package/dist/install/read-account-rules.d.ts +0 -100
  129. package/dist/install/read-account-rules.js +0 -283
  130. package/dist/ir/index.d.ts +0 -1
  131. package/dist/ir/index.js +0 -2
  132. package/dist/ir/types.d.ts +0 -140
  133. package/dist/ir/types.js +0 -11
  134. package/dist/mandate/index.d.ts +0 -2
  135. package/dist/mandate/index.js +0 -2
  136. package/dist/mandate/to-ir.d.ts +0 -3
  137. package/dist/mandate/to-ir.js +0 -60
  138. package/dist/mandate/types.d.ts +0 -20
  139. package/dist/mandate/types.js +0 -8
  140. package/dist/seams/index.d.ts +0 -1
  141. package/dist/seams/index.js +0 -2
  142. package/dist/seams/types.d.ts +0 -66
  143. package/dist/seams/types.js +0 -11
  144. package/dist/synth/deny-cases.js +0 -562
  145. package/dist/synth/evaluate.d.ts +0 -39
  146. package/dist/synth/evaluate.js +0 -551
  147. package/dist/synth/harness.d.ts +0 -28
  148. package/dist/synth/harness.js +0 -47
  149. package/dist/synth/minimize.d.ts +0 -4
  150. package/dist/synth/minimize.js +0 -38
  151. package/dist/synth/permit-context.d.ts +0 -15
  152. package/dist/synth/permit-context.js +0 -116
  153. package/dist/synth/predicate-literals.d.ts +0 -5
  154. package/dist/synth/predicate-literals.js +0 -25
  155. package/dist/synth/synthesize-from-mandate.d.ts +0 -20
  156. package/dist/synth/synthesize-from-mandate.js +0 -59
  157. package/dist/verify/envelope.d.ts +0 -15
  158. package/dist/verify/envelope.js +0 -22
  159. package/dist/verify/index.d.ts +0 -3
  160. package/dist/verify/index.js +0 -3
  161. package/dist/verify/simulate.d.ts +0 -31
  162. package/dist/verify/simulate.js +0 -258
  163. package/dist/verify/verify.d.ts +0 -21
  164. package/dist/verify/verify.js +0 -189
  165. package/dist-cjs/adapters/interpreter/index.d.ts +0 -1
  166. package/dist-cjs/adapters/interpreter/index.js +0 -8
  167. package/dist-cjs/adapters/oz/adapter.d.ts +0 -20
  168. package/dist-cjs/adapters/oz/adapter.js +0 -300
  169. package/dist-cjs/adapters/oz/index.d.ts +0 -1
  170. package/dist-cjs/adapters/oz/index.js +0 -8
  171. package/dist-cjs/codegen/compile-gate.d.ts +0 -33
  172. package/dist-cjs/codegen/compile-gate.js +0 -128
  173. package/dist-cjs/codegen/index.d.ts +0 -2
  174. package/dist-cjs/codegen/index.js +0 -14
  175. package/dist-cjs/codegen/template.d.ts +0 -18
  176. package/dist-cjs/codegen/template.js +0 -151
  177. package/dist-cjs/install/authority-overlap.d.ts +0 -134
  178. package/dist-cjs/install/authority-overlap.js +0 -0
  179. package/dist-cjs/install/build-install-predicate.d.ts +0 -96
  180. package/dist-cjs/install/build-install-predicate.js +0 -479
  181. package/dist-cjs/install/build-merge-policy.d.ts +0 -70
  182. package/dist-cjs/install/build-merge-policy.js +0 -134
  183. package/dist-cjs/install/plan-merge-policy.d.ts +0 -49
  184. package/dist-cjs/install/plan-merge-policy.js +0 -90
  185. package/dist-cjs/install/read-account-rules.d.ts +0 -100
  186. package/dist-cjs/install/read-account-rules.js +0 -296
  187. package/dist-cjs/ir/index.d.ts +0 -1
  188. package/dist-cjs/ir/index.js +0 -3
  189. package/dist-cjs/ir/types.d.ts +0 -140
  190. package/dist-cjs/ir/types.js +0 -12
  191. package/dist-cjs/mandate/index.d.ts +0 -2
  192. package/dist-cjs/mandate/index.js +0 -6
  193. package/dist-cjs/mandate/to-ir.d.ts +0 -3
  194. package/dist-cjs/mandate/to-ir.js +0 -63
  195. package/dist-cjs/mandate/types.d.ts +0 -20
  196. package/dist-cjs/mandate/types.js +0 -9
  197. package/dist-cjs/seams/index.d.ts +0 -1
  198. package/dist-cjs/seams/index.js +0 -3
  199. package/dist-cjs/seams/types.d.ts +0 -66
  200. package/dist-cjs/seams/types.js +0 -12
  201. package/dist-cjs/synth/deny-cases.js +0 -568
  202. package/dist-cjs/synth/evaluate.d.ts +0 -39
  203. package/dist-cjs/synth/evaluate.js +0 -554
  204. package/dist-cjs/synth/harness.d.ts +0 -28
  205. package/dist-cjs/synth/harness.js +0 -50
  206. package/dist-cjs/synth/minimize.d.ts +0 -4
  207. package/dist-cjs/synth/minimize.js +0 -41
  208. package/dist-cjs/synth/permit-context.d.ts +0 -15
  209. package/dist-cjs/synth/permit-context.js +0 -119
  210. package/dist-cjs/synth/predicate-literals.d.ts +0 -5
  211. package/dist-cjs/synth/predicate-literals.js +0 -28
  212. package/dist-cjs/synth/synthesize-from-mandate.d.ts +0 -20
  213. package/dist-cjs/synth/synthesize-from-mandate.js +0 -62
  214. package/dist-cjs/verify/envelope.d.ts +0 -15
  215. package/dist-cjs/verify/envelope.js +0 -23
  216. package/dist-cjs/verify/index.d.ts +0 -3
  217. package/dist-cjs/verify/index.js +0 -8
  218. package/dist-cjs/verify/simulate.d.ts +0 -31
  219. package/dist-cjs/verify/simulate.js +0 -261
  220. package/dist-cjs/verify/verify.d.ts +0 -21
  221. package/dist-cjs/verify/verify.js +0 -192
  222. package/src/adapters/interpreter/index.ts +0 -8
  223. package/src/adapters/oz/adapter.ts +0 -376
  224. package/src/adapters/oz/index.ts +0 -9
  225. package/src/codegen/compile-gate.ts +0 -167
  226. package/src/codegen/index.ts +0 -17
  227. package/src/codegen/template.ts +0 -165
  228. package/src/install/authority-overlap.ts +0 -0
  229. package/src/install/build-merge-policy.ts +0 -219
  230. package/src/install/plan-merge-policy.ts +0 -133
  231. package/src/install/read-account-rules.ts +0 -376
  232. package/src/ir/index.ts +0 -13
  233. package/src/ir/types.ts +0 -132
  234. package/src/mandate/index.ts +0 -4
  235. package/src/mandate/to-ir.ts +0 -71
  236. package/src/mandate/types.ts +0 -21
  237. package/src/seams/index.ts +0 -11
  238. package/src/seams/types.ts +0 -81
  239. package/src/synth/deny-cases.ts +0 -663
  240. package/src/synth/evaluate.ts +0 -613
  241. package/src/synth/harness.ts +0 -68
  242. package/src/synth/minimize.ts +0 -48
  243. package/src/synth/permit-context.ts +0 -136
  244. package/src/synth/predicate-literals.ts +0 -27
  245. package/src/synth/synthesize-from-mandate.ts +0 -82
  246. package/src/verify/envelope.ts +0 -28
  247. package/src/verify/index.ts +0 -5
  248. package/src/verify/simulate.ts +0 -311
  249. package/src/verify/verify.ts +0 -243
@@ -1,562 +0,0 @@
1
- import { MAX_SCVAL_CLONE_DEPTH } from "../types.js";
2
- import { literalNumericBigInt } from "./predicate-literals.js";
3
- const ORACLE_CASES = [
4
- ['oracle_stale', 'stale'],
5
- ['oracle_missing', 'missing'],
6
- ['oracle_deviation_exceeded', 'deviation'],
7
- ['oracle_paused', 'paused'],
8
- ];
9
- // Cross-layer reason contract: the TS evaluator's deny reason for each
10
- // oracle error category must match the Rust interpreter's `DenyReason` code
11
- // string (the same strings surface in the conformance fixture). The harness
12
- // asserts them via the per-case `expectedReason`; a future divergence
13
- // surfaces as a REASON_MISMATCH failure.
14
- const ORACLE_ERROR_REASON = {
15
- stale: 'ORACLE_STALE',
16
- missing: 'ORACLE_MISSING',
17
- deviation: 'ORACLE_DEVIATION_EXCEEDED',
18
- paused: 'ORACLE_PAUSED',
19
- };
20
- // Deterministic XLM/USDC adjacency fixture; the shared registry can replace this boundary later.
21
- const ADJACENT_ASSETS = [
22
- 'CAS3J7GYLGXMF6TDJ5WQ2PEN4GRVNXJUIQ2TZU3ZB3OQ2V4DRCWI7WPF',
23
- 'CCWCLTASNDT57N3BCHOSVB5QWMV5URK4BXLDDF6ZZQYMBQ4OKZA3ZB2N',
24
- ];
25
- // Property-harness mutation dimensions excluded from the synth pipeline's
26
- // self-verify call so existing fixtures still emit policies. The harness tests
27
- // them as FINDINGS against the already-emitted policy.
28
- const OVERPERMISSIVE_DIMENSIONS = ['argument_reorder'];
29
- // The dimensions the synth pipeline uses for self-verify and minimise. No count
30
- // is written here on purpose: this comment said 15 while the array held 17, and
31
- // anyone checking the number should read the array rather than the prose.
32
- // Phase 1 grammar extension: `vec_append` and `map_field_flip` are listed below
33
- // alongside the existing dimensions so the per-element binds emitted for
34
- // Blend `submit` (call_arg_len + 3 call_arg_field per element) survive
35
- // minimise. Without these, no deny case exercises the new leaves and the
36
- // minimise pass drops them as "redundant" - reopening the element-append
37
- // hole they were added to close. Both dimensions are no-ops for any fixture
38
- // whose predicate has no `call_arg_len` / `call_arg_field` leaves (the
39
- // generator short-circuits on leaf-kind), so they cannot make production
40
- // synthesis refuse for fixtures that do not exercise the new grammar.
41
- const ORIGINAL_DIMENSIONS = [
42
- 'amount',
43
- 'asset',
44
- 'contract',
45
- 'function',
46
- 'timing',
47
- 'time_window',
48
- 'invocation_count',
49
- 'arg_amount_bound',
50
- 'arg_bound',
51
- 'scope_contract_fn_arg',
52
- 'oracle_stale',
53
- 'oracle_missing',
54
- 'oracle_deviation_exceeded',
55
- 'oracle_paused',
56
- 'soroswap_allowed_path',
57
- 'vec_append',
58
- 'map_field_flip',
59
- ];
60
- export { ORIGINAL_DIMENSIONS, OVERPERMISSIVE_DIMENSIONS };
61
- /** Build deterministic model-evaluated alternatives without mutating the intended call.
62
- *
63
- * @param predicate - the synthesized predicate
64
- * @param permitCtx - EvalContext for the intended (permitted) call
65
- * @param dimensions - optional whitelist of dimension names to emit; when omitted
66
- * all known dimensions (including over-permissiveness mutations)
67
- * are emitted. The synth pipeline passes ORIGINAL_DIMENSIONS so
68
- * existing fixtures do not regress. */
69
- export function generateCases(predicate, permitCtx, dimensions) {
70
- const facts = inspectPredicate(predicate);
71
- const denies = [];
72
- for (const comparison of facts.comparisons) {
73
- if (comparison.left.kind !== 'amount')
74
- continue;
75
- const mutated = mutateBigIntRecord(permitCtx, 'amountByToken', comparison.left.token, comparison);
76
- if (mutated)
77
- denies.push({ dimension: 'amount', ctx: mutated, expectedReason: 'AMOUNT_BOUND' });
78
- }
79
- const movedTokens = new Set();
80
- for (const comparison of facts.comparisons) {
81
- if (comparison.left.kind === 'amount' || comparison.left.kind === 'window_spent') {
82
- movedTokens.add(comparison.left.token);
83
- }
84
- }
85
- for (const token of movedTokens) {
86
- // The `asset` deny case mutates BOTH the amount record AND the contract
87
- // binding (token's value moved to an adjacent asset; the same swap applied
88
- // to any address-typed `call_arg` / `call_contract` literal). The actual
89
- // deny reason is therefore predicate-dependent - the order of evaluation
90
- // is CONTRACT_SCOPE (step 3) before AMOUNT_BOUND (step 6), so a policy
91
- // that has both bindings denies with CONTRACT_SCOPE, while a policy with
92
- // only an amount binding denies with AMOUNT_BOUND. The reason assertion
93
- // would be brittle here; the boolean decision is what we strictly need
94
- // to pin. Leave `expectedReason` unset for this dimension.
95
- denies.push({ dimension: 'asset', ctx: mutateAsset(predicate, permitCtx, token) });
96
- }
97
- const contractConstraints = [
98
- ...facts.comparisons.filter((node) => node.op === 'eq' &&
99
- node.left.kind === 'call_contract' &&
100
- node.right.kind === 'literal_address'),
101
- ...facts.memberships.filter((node) => node.needle.kind === 'call_contract'),
102
- ];
103
- for (const _constraint of contractConstraints) {
104
- const ctx = cloneContext(permitCtx);
105
- ctx.contract = distinctText(permitCtx.contract, 'contract');
106
- denies.push({ dimension: 'contract', ctx, expectedReason: 'CONTRACT_SCOPE' });
107
- }
108
- const functionConstraints = [
109
- ...facts.comparisons.filter((node) => node.op === 'eq' && node.left.kind === 'call_fn' && node.right.kind === 'literal_symbol'),
110
- ...facts.memberships.filter((node) => node.needle.kind === 'call_fn'),
111
- ];
112
- for (const _constraint of functionConstraints) {
113
- const ctx = cloneContext(permitCtx);
114
- ctx.fn = distinctText(permitCtx.fn, 'function');
115
- denies.push({ dimension: 'function', ctx, expectedReason: 'FN_MISMATCH' });
116
- }
117
- if (permitCtx.validUntilLedger !== undefined) {
118
- const ctx = cloneContext(permitCtx);
119
- ctx.atLedger = permitCtx.validUntilLedger + 1;
120
- denies.push({ dimension: 'timing', ctx, expectedReason: 'EXPIRED' });
121
- }
122
- for (const comparison of facts.comparisons) {
123
- if (comparison.left.kind !== 'window_spent')
124
- continue;
125
- const mutated = mutateBigIntRecord(permitCtx, 'windowSpentByToken', comparison.left.token, comparison, false);
126
- if (mutated)
127
- denies.push({ dimension: 'time_window', ctx: mutated, expectedReason: 'AMOUNT_BOUND' });
128
- }
129
- for (const comparison of facts.comparisons) {
130
- if (comparison.left.kind !== 'invocation_count_in_window')
131
- continue;
132
- if (comparison.right.kind !== 'literal_u32')
133
- continue;
134
- const ctx = cloneContext(permitCtx);
135
- ctx.invocationCountByWindow[comparison.left.windowSecs] = violatingNumber(comparison.op, comparison.right.value);
136
- denies.push({ dimension: 'invocation_count', ctx, expectedReason: 'FREQUENCY' });
137
- }
138
- // Ordered numeric bound on a call_arg (e.g. a SoroSwap input-amount cap
139
- // `call_arg[0] <= limit`). A violating deny case pushes the arg past the
140
- // bound so the leaf is exercised - and, critically, so `minimize` keeps it:
141
- // a conjunct that no deny case needs is pruned as redundant, which would
142
- // silently drop a caller-requested restriction.
143
- for (const comparison of facts.comparisons) {
144
- if (comparison.op === 'eq' || comparison.left.kind !== 'call_arg')
145
- continue;
146
- const violating = violatingArgScVal(comparison.op, comparison.right);
147
- if (!violating)
148
- continue;
149
- const ctx = cloneContext(permitCtx);
150
- ctx.args[comparison.left.index] = violating;
151
- denies.push({ dimension: 'arg_amount_bound', ctx, expectedReason: 'ARG_MISMATCH' });
152
- }
153
- const argumentConstraints = [];
154
- for (const comparison of facts.comparisons) {
155
- if (comparison.op === 'eq' &&
156
- comparison.left.kind === 'call_arg' &&
157
- comparison.right.kind !== 'literal_vec') {
158
- argumentConstraints.push({ index: comparison.left.index });
159
- }
160
- }
161
- for (const membership of facts.memberships) {
162
- if (membership.needle.kind === 'call_arg') {
163
- argumentConstraints.push({ index: membership.needle.index });
164
- }
165
- }
166
- for (const constraint of argumentConstraints) {
167
- const ctx = cloneContext(permitCtx);
168
- ctx.args[constraint.index] = { type: 'other', value: 'deny-case-opaque-argument' };
169
- // The `arg_bound` case sets the arg to an opaque ScVal. The deny reason
170
- // depends on the predicate shape:
171
- // - an `eq(call_arg[i], literal)` denies with ARG_MISMATCH
172
- // - an `in(call_arg[i], [literals])` denies with NOT_IN_ALLOWLIST
173
- // (opaque needles fail-closed at the `in` membership gate, step 5)
174
- // Both are correct in the TS evaluator; the reason is predicate-dependent
175
- // so we cannot pin a single canonical reason here. The boolean decision
176
- // is what we strictly need to assert; the reason is recorded (not asserted)
177
- // when the harness runs this case.
178
- denies.push({ dimension: 'arg_bound', ctx });
179
- }
180
- const scopedArgumentIndices = new Set(argumentConstraints.map(({ index }) => index));
181
- for (const comparison of facts.comparisons) {
182
- if (comparison.op === 'eq' &&
183
- comparison.left.kind === 'call_arg' &&
184
- comparison.right.kind === 'literal_vec') {
185
- scopedArgumentIndices.add(comparison.left.index);
186
- }
187
- }
188
- if (contractConstraints.length > 0 &&
189
- functionConstraints.length > 0 &&
190
- scopedArgumentIndices.size > 0) {
191
- const ctx = cloneContext(permitCtx);
192
- ctx.contract = distinctText(permitCtx.contract, 'authorized-call-contract');
193
- ctx.fn = distinctText(permitCtx.fn, 'authorized-call-function');
194
- for (const index of scopedArgumentIndices) {
195
- ctx.args[index] = { type: 'other', value: 'deny-case-authorized-call-argument' };
196
- }
197
- // `scope_contract_fn_arg` changes contract, fn, AND args simultaneously.
198
- // The first failing child of the AND decides the reason, and the contract
199
- // check (step 3) does fire first in the evaluator. The reason is therefore
200
- // CONTRACT_SCOPE for the canonical case, but a predicate that lists the
201
- // call_fn leaf first (or that has a `call_arg_field` for that arg index)
202
- // can flip the order. Pin the assertion here only when no `in` /
203
- // call_arg_field binds the same arg index exist - the canonical blend
204
- // case does have those binds, so the assertion would fire there.
205
- denies.push({ dimension: 'scope_contract_fn_arg', ctx });
206
- }
207
- const oracleComparisons = facts.comparisons.filter((node) => node.left.kind === 'oracle_price');
208
- for (const [dimension, error] of ORACLE_CASES) {
209
- for (const comparison of oracleComparisons) {
210
- if (comparison.left.kind !== 'oracle_price')
211
- continue;
212
- const ctx = cloneContext(permitCtx);
213
- ctx.oraclePriceByAsset[comparison.left.asset] = { error };
214
- denies.push({ dimension, ctx, expectedReason: ORACLE_ERROR_REASON[error] });
215
- }
216
- }
217
- for (const comparison of facts.comparisons) {
218
- if (comparison.op !== 'eq' ||
219
- comparison.left.kind !== 'call_arg' ||
220
- comparison.right.kind !== 'literal_vec') {
221
- continue;
222
- }
223
- const ctx = cloneContext(permitCtx);
224
- ctx.args[comparison.left.index] = differentVector(ctx.args[comparison.left.index]);
225
- denies.push({ dimension: 'soroswap_allowed_path', ctx });
226
- }
227
- // --- argument_reorder: swap first two address args ---
228
- // Skipped when dimensions filter is active so the synth pipeline can emit a policy;
229
- // the over-permissiveness harness then tests this mutation as a FINDING.
230
- const constrainedArgIndices = new Set();
231
- for (const comparison of facts.comparisons) {
232
- if (comparison.left.kind === 'call_arg')
233
- constrainedArgIndices.add(comparison.left.index);
234
- }
235
- for (const membership of facts.memberships) {
236
- if (membership.needle.kind === 'call_arg')
237
- constrainedArgIndices.add(membership.needle.index);
238
- }
239
- const hasConstrainedArg = constrainedArgIndices.size > 0;
240
- if ((!dimensions || dimensions.includes('argument_reorder')) &&
241
- hasConstrainedArg &&
242
- permitCtx.args.length >= 2 &&
243
- permitCtx.args[0]?.type === 'address' &&
244
- permitCtx.args[1]?.type === 'address' &&
245
- permitCtx.args[0].value !==
246
- permitCtx.args[1].value) {
247
- const ctx = cloneContext(permitCtx);
248
- // Guard above guarantees args[0] and args[1] exist and are address-typed.
249
- const a0 = ctx.args[0];
250
- const a1 = ctx.args[1];
251
- ctx.args[0] = a1;
252
- ctx.args[1] = a0;
253
- denies.push({ dimension: 'argument_reorder', ctx });
254
- }
255
- // --- map_field_flip: flip a bound map field to a different valid value of
256
- // the same type. Mirrors argument_reorder: OPT-IN only, never ORIGINAL.
257
- // Targets each `call_arg_field` leaf and produces a ctx whose vec element
258
- // has a different value of the recorded field (different request_type,
259
- // different amount, different address). The blend-submit case uses this
260
- // to detect a missing request_type pin. ---
261
- if (!dimensions || dimensions.includes('map_field_flip')) {
262
- for (const comparison of facts.comparisons) {
263
- const sel = comparison.left;
264
- if (sel.kind !== 'call_arg_field')
265
- continue;
266
- const arg = permitCtx.args[sel.index];
267
- if (arg?.type !== 'vec')
268
- continue;
269
- const element = arg.value[sel.element];
270
- if (element?.type !== 'map' || !Array.isArray(element.value))
271
- continue;
272
- const entry = element.value.find((e) => e.key === sel.field);
273
- if (!entry)
274
- continue;
275
- const flipped = flipFieldValue(entry.val);
276
- if (flipped === null)
277
- continue;
278
- const ctx = cloneContext(permitCtx);
279
- const clonedVec = arg.value.map(cloneScVal);
280
- const clonedElement = clonedVec[sel.element];
281
- if (clonedElement?.type !== 'map' || !Array.isArray(clonedElement.value))
282
- continue;
283
- clonedElement.value = clonedElement.value.map((e) => e.key === sel.field ? { key: e.key, val: flipped } : e);
284
- clonedVec[sel.element] = clonedElement;
285
- ctx.args[sel.index] = { type: 'vec', value: clonedVec };
286
- denies.push({ dimension: 'map_field_flip', ctx });
287
- }
288
- }
289
- // --- vec_append: append a new element to a bound vec. Mirrors map_field_flip:
290
- // OPT-IN only, never ORIGINAL. Targets every `call_arg_len` leaf; without the
291
- // length pin a caller can append an extra element to defeat per-element binds. ---
292
- if (!dimensions || dimensions.includes('vec_append')) {
293
- for (const comparison of facts.comparisons) {
294
- const sel = comparison.left;
295
- if (sel.kind !== 'call_arg_len')
296
- continue;
297
- const arg = permitCtx.args[sel.index];
298
- if (arg?.type !== 'vec')
299
- continue;
300
- const ctx = cloneContext(permitCtx);
301
- ctx.args[sel.index] = {
302
- type: 'vec',
303
- value: [...arg.value, { type: 'other', value: 'deny-case-vec-append' }],
304
- };
305
- denies.push({ dimension: 'vec_append', ctx });
306
- }
307
- }
308
- // Version mismatch, malformed predicates, master authorization, and nonce replay are install-time checks and are intentionally omitted from model-evaluated cases.
309
- return { permit: cloneContext(permitCtx), denies };
310
- }
311
- function inspectPredicate(predicate) {
312
- const facts = { comparisons: [], memberships: [] };
313
- visit(predicate, facts);
314
- return facts;
315
- }
316
- function visit(node, facts) {
317
- switch (node.op) {
318
- case 'and':
319
- case 'or':
320
- for (const child of node.children)
321
- visit(child, facts);
322
- return;
323
- case 'not':
324
- visit(node.child, facts);
325
- return;
326
- case 'in':
327
- facts.memberships.push(node);
328
- return;
329
- case 'eq':
330
- case 'lt':
331
- case 'lte':
332
- case 'gt':
333
- case 'gte':
334
- facts.comparisons.push(node);
335
- }
336
- }
337
- function mutateBigIntRecord(permitCtx, recordKey, token, comparison, preferScaledAmount = true) {
338
- if (comparison.right.kind !== 'literal_i128')
339
- return null;
340
- try {
341
- const current = BigInt(permitCtx[recordKey][token] ?? '0');
342
- const bound = BigInt(comparison.right.value);
343
- const value = violatingBigInt(comparison.op, current, bound, preferScaledAmount);
344
- const ctx = cloneContext(permitCtx);
345
- ctx[recordKey][token] = value.toString();
346
- return ctx;
347
- }
348
- catch {
349
- return null;
350
- }
351
- }
352
- function violatingBigInt(op, current, bound, preferScaledAmount) {
353
- if (preferScaledAmount) {
354
- const scaledCandidates = [(current * 101n) / 100n, current * 10n];
355
- for (const candidate of scaledCandidates) {
356
- if (!bigIntComparison(op, candidate, bound))
357
- return candidate;
358
- }
359
- }
360
- return boundaryViolatingBigInt(op, bound);
361
- }
362
- /** The single value at the boundary that violates `<selector> op bound`:
363
- * bound+1 for `lte`/`eq`, bound-1 for `gte`, and bound itself for the strict
364
- * `lt`/`gt`. */
365
- function boundaryViolatingBigInt(op, bound) {
366
- switch (op) {
367
- case 'lt':
368
- case 'gt':
369
- return bound;
370
- case 'lte':
371
- case 'eq':
372
- return bound + 1n;
373
- case 'gte':
374
- return bound - 1n;
375
- }
376
- }
377
- function violatingNumber(op, bound) {
378
- switch (op) {
379
- case 'lt':
380
- case 'gt':
381
- return bound;
382
- case 'lte':
383
- case 'eq':
384
- return bound + 1;
385
- case 'gte':
386
- return bound - 1;
387
- }
388
- }
389
- /** Build an ScVal that VIOLATES an ordered numeric bound on a call_arg, given
390
- * the comparison op and its numeric-literal right-hand side. Returns null when
391
- * the literal is not an integer (the bound is not a numeric compare). The arg
392
- * is emitted as an i128 - the recorder's numeric args are read via BigInt, so
393
- * the wire type only needs to be a numeric ScVal to exercise the bound. */
394
- function violatingArgScVal(op, right) {
395
- const bound = literalNumericBigInt(right);
396
- if (bound === null)
397
- return null;
398
- return { type: 'i128', value: boundaryViolatingBigInt(op, bound).toString() };
399
- }
400
- function bigIntComparison(op, value, bound) {
401
- switch (op) {
402
- case 'eq':
403
- return value === bound;
404
- case 'lt':
405
- return value < bound;
406
- case 'lte':
407
- return value <= bound;
408
- case 'gt':
409
- return value > bound;
410
- case 'gte':
411
- return value >= bound;
412
- }
413
- }
414
- function mutateAsset(predicate, permitCtx, token) {
415
- const binding = findAddressBinding(predicate, token);
416
- const adjacent = adjacentAsset(token);
417
- const ctx = cloneContext(permitCtx);
418
- moveRecordEntry(ctx.amountByToken, token, adjacent);
419
- moveRecordEntry(ctx.windowSpentByToken, token, adjacent);
420
- if (binding.contract && ctx.contract === token)
421
- ctx.contract = adjacent;
422
- for (const index of binding.argumentIndices) {
423
- const value = ctx.args[index];
424
- if (value)
425
- ctx.args[index] = replaceAddress(value, token, adjacent);
426
- }
427
- return ctx;
428
- }
429
- function findAddressBinding(predicate, address) {
430
- const facts = inspectPredicate(predicate);
431
- let contract = false;
432
- const argumentIndices = new Set();
433
- for (const comparison of facts.comparisons) {
434
- if (comparison.op !== 'eq' || !leafContainsAddress(comparison.right, address))
435
- continue;
436
- if (comparison.left.kind === 'call_contract')
437
- contract = true;
438
- if (comparison.left.kind === 'call_arg')
439
- argumentIndices.add(comparison.left.index);
440
- }
441
- for (const membership of facts.memberships) {
442
- if (!membership.haystack.some((leaf) => leafContainsAddress(leaf, address)))
443
- continue;
444
- if (membership.needle.kind === 'call_contract')
445
- contract = true;
446
- if (membership.needle.kind === 'call_arg')
447
- argumentIndices.add(membership.needle.index);
448
- }
449
- return { contract, argumentIndices };
450
- }
451
- function leafContainsAddress(leaf, address) {
452
- if (leaf.kind === 'literal_address')
453
- return leaf.value === address;
454
- if (leaf.kind === 'literal_vec') {
455
- return leaf.elements.some((element) => leafContainsAddress(element, address));
456
- }
457
- return false;
458
- }
459
- function moveRecordEntry(record, from, to) {
460
- const value = record[from];
461
- if (value === undefined)
462
- return;
463
- delete record[from];
464
- record[to] = value;
465
- }
466
- function replaceAddress(value, from, to) {
467
- if (value.type === 'address') {
468
- return value.value === from ? { type: 'address', value: to } : { ...value };
469
- }
470
- if (value.type === 'vec') {
471
- return { type: 'vec', value: value.value.map((item) => replaceAddress(item, from, to)) };
472
- }
473
- return { ...value };
474
- }
475
- function differentVector(actual) {
476
- if (actual?.type !== 'vec')
477
- return { type: 'vec', value: [] };
478
- if (actual.value.length === 0) {
479
- return { type: 'vec', value: [{ type: 'other', value: 'deny-case-extra-hop' }] };
480
- }
481
- if (actual.value.length > 1) {
482
- const reversed = actual.value.map(cloneScVal).reverse();
483
- if (JSON.stringify(reversed) !== JSON.stringify(actual.value)) {
484
- return { type: 'vec', value: reversed };
485
- }
486
- }
487
- const value = actual.value.map(cloneScVal);
488
- value[0] = { type: 'other', value: 'deny-case-different-hop' };
489
- return { type: 'vec', value };
490
- }
491
- function adjacentAsset(asset) {
492
- return asset === ADJACENT_ASSETS[0] ? ADJACENT_ASSETS[1] : ADJACENT_ASSETS[0];
493
- }
494
- function distinctText(value, label) {
495
- return `${value}#${label}`;
496
- }
497
- function cloneContext(ctx) {
498
- const cloned = {
499
- contract: ctx.contract,
500
- fn: ctx.fn,
501
- args: ctx.args.map(cloneScVal),
502
- atLedger: ctx.atLedger,
503
- nowSeconds: ctx.nowSeconds,
504
- amountByToken: { ...ctx.amountByToken },
505
- windowSpentByToken: { ...ctx.windowSpentByToken },
506
- invocationCountByWindow: { ...ctx.invocationCountByWindow },
507
- oraclePriceByAsset: Object.fromEntries(Object.entries(ctx.oraclePriceByAsset).map(([asset, entry]) => [
508
- asset,
509
- 'error' in entry ? { error: entry.error } : { ...entry },
510
- ])),
511
- };
512
- if (ctx.validUntilLedger !== undefined)
513
- cloned.validUntilLedger = ctx.validUntilLedger;
514
- if (ctx.signerWeights !== undefined)
515
- cloned.signerWeights = { ...ctx.signerWeights };
516
- return cloned;
517
- }
518
- /** Deep-copy an ScVal so a mutation cannot alias the recorded call.
519
- * Exported so the permit-context builder shares this one implementation. */
520
- export function cloneScVal(value, depth = 0) {
521
- // Recursion is bounded by MAX_SCVAL_CLONE_DEPTH so a hand-crafted nested-vec
522
- // payload cannot RangeError the JS stack during deny-case mutation. Over-depth
523
- // throws a ToolError-shaped error that the `synthesizeFromRecording` envelope
524
- // (item 3) converts to a structured `{ok:false, error}`.
525
- if (value.type === 'vec') {
526
- if (depth >= MAX_SCVAL_CLONE_DEPTH) {
527
- throw cloneDepthError(value);
528
- }
529
- return { type: 'vec', value: value.value.map((v) => cloneScVal(v, depth + 1)) };
530
- }
531
- return { ...value };
532
- }
533
- function cloneDepthError(value) {
534
- const err = new Error(`ScVal clone depth exceeds MAX_SCVAL_CLONE_DEPTH (${MAX_SCVAL_CLONE_DEPTH})`);
535
- err.code = 'SYNTHESIS_ERROR';
536
- err.severity = 'error';
537
- err.retryable = false;
538
- err.depthContext = value.type;
539
- throw err;
540
- }
541
- /** Build a value of the SAME ScVal type that differs from the recorded one,
542
- * used by the `map_field_flip` mutation to defeat a per-element pin without
543
- * changing the wire type (a type/arity change is rejected by host dispatch
544
- * before Policy::enforce, so it is not the predicate's job). Returns null
545
- * when the ScVal type has no obvious different value (e.g. opaque/other). */
546
- function flipFieldValue(val) {
547
- switch (val.type) {
548
- case 'address': {
549
- const a = 'GBFKRGJYZXLTDEI36ZCQEIM225NMOCR2VDBOIHJTXJ54FEFFVL2FKALE';
550
- const b = 'GD6XSMQJ47EHHJOWXQOND5YDVZC37JWZJHYHBKE6QJFSLLJ5KQXM5QS5';
551
- return { type: 'address', value: val.value === a ? b : a };
552
- }
553
- case 'i128':
554
- case 'u64':
555
- case 'u32':
556
- return { type: val.type, value: String(BigInt(val.value) + 1n) };
557
- case 'symbol':
558
- return { type: 'symbol', value: `${val.value}x` };
559
- default:
560
- return null;
561
- }
562
- }
@@ -1,39 +0,0 @@
1
- import type { PredicateNode, ScVal } from '../types.ts';
2
- export interface EvalContext {
3
- /** Contract the interpreter is asked to enforce against. */
4
- contract: string;
5
- /** Function name on that contract. */
6
- fn: string;
7
- /** Decoded `ScVal[]` of the top-level authorized call. */
8
- args: ScVal[];
9
- /** Ledger sequence at which the interpreter is invoked. */
10
- atLedger: number;
11
- /** Optional policy expiry; absent -> step 1 is skipped. */
12
- validUntilLedger?: number;
13
- /** Unix seconds; used by `now` / `valid_until` leaves. */
14
- nowSeconds: number;
15
- /** Per-token amount moved by the current call (i128 decimal string). */
16
- amountByToken: Record<string, string>;
17
- /** Per-token window-rolling spend prior to this call (i128 decimal string). */
18
- windowSpentByToken: Record<string, string>;
19
- /** Recorded invocation counts keyed by window seconds. */
20
- invocationCountByWindow: Record<number, number>;
21
- /** Optional signer-weight map for the threshold gate. Absent -> skip. */
22
- signerWeights?: Record<string, number>;
23
- /** Per-asset oracle snapshot. Missing keys default to ORACLE_STALE. */
24
- oraclePriceByAsset: Record<string, {
25
- price: string;
26
- timestampSeconds: number;
27
- } | {
28
- error: 'stale' | 'missing' | 'deviation' | 'paused' | 'decimals' | 'fingerprint';
29
- }>;
30
- }
31
- export type EvalResult = {
32
- permit: true;
33
- } | {
34
- permit: false;
35
- reason: string;
36
- };
37
- /** Evaluate a `PredicateNode` against the candidate call described by `ctx`.
38
- * Pure function. Returns `{ permit: true }` or `{ permit: false; reason }`. */
39
- export declare function evaluate(predicate: PredicateNode, ctx: EvalContext): EvalResult;