@crediolabs/policy-synth 0.2.0 → 0.3.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 (184) 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/adapters/interpreter/index.d.ts +1 -1
  5. package/dist/adapters/interpreter/index.js +1 -1
  6. package/dist/adapters/oz/adapter.js +1 -15
  7. package/dist/errors.d.ts +1 -1
  8. package/dist/index.d.ts +1 -4
  9. package/dist/index.js +1 -4
  10. package/dist/install/build-add-context-rule.d.ts +3 -19
  11. package/dist/install/build-add-context-rule.js +3 -98
  12. package/dist/install/build-install-policy.d.ts +4 -53
  13. package/dist/install/build-install-policy.js +44 -123
  14. package/dist/install/index.d.ts +0 -2
  15. package/dist/install/index.js +0 -7
  16. package/dist/ir/index.d.ts +1 -1
  17. package/dist/ir/types.d.ts +7 -76
  18. package/dist/ir/types.js +0 -2
  19. package/dist/predicate/decode.d.ts +1 -1
  20. package/dist/predicate/decode.js +2 -70
  21. package/dist/predicate/encode.js +45 -229
  22. package/dist/predicate/from-json.js +1 -42
  23. package/dist/record/decode.js +3 -8
  24. package/dist/review-card/builder.d.ts +4 -3
  25. package/dist/review-card/builder.js +18 -162
  26. package/dist/review-card/cross-check.js +2 -105
  27. package/dist/review-card/render-leaf.d.ts +4 -0
  28. package/dist/review-card/render-leaf.js +47 -0
  29. package/dist/run/index.d.ts +36 -139
  30. package/dist/run/index.js +125 -437
  31. package/dist/run/schemas.d.ts +168 -1929
  32. package/dist/run/schemas.js +40 -269
  33. package/dist/seams/types.d.ts +0 -2
  34. package/dist/simulate/deny-cases.d.ts +26 -0
  35. package/dist/simulate/deny-cases.js +282 -0
  36. package/dist/simulate/evaluate.d.ts +18 -0
  37. package/dist/simulate/evaluate.js +271 -0
  38. package/dist/simulate/index.d.ts +4 -0
  39. package/dist/simulate/index.js +8 -0
  40. package/dist/synth/compose-from-recording.d.ts +24 -51
  41. package/dist/synth/compose-from-recording.js +120 -282
  42. package/dist/synth/deny-cases.d.ts +5 -7
  43. package/dist/synth/deny-cases.js +23 -373
  44. package/dist/synth/evaluate.d.ts +0 -21
  45. package/dist/synth/evaluate.js +56 -336
  46. package/dist/synth/index.d.ts +0 -5
  47. package/dist/synth/index.js +0 -5
  48. package/dist/synth/permit-context.d.ts +2 -9
  49. package/dist/synth/permit-context.js +1 -82
  50. package/dist/synth/synthesize-from-recording.d.ts +1 -29
  51. package/dist/synth/synthesize-from-recording.js +41 -356
  52. package/dist/types.d.ts +8 -60
  53. package/dist/types.js +5 -2
  54. package/dist/verify/simulate.d.ts +1 -14
  55. package/dist/verify/simulate.js +2 -88
  56. package/dist/verify/verify.d.ts +1 -7
  57. package/dist/verify/verify.js +0 -8
  58. package/dist-cjs/adapters/interpreter/adapter.d.ts +19 -25
  59. package/dist-cjs/adapters/interpreter/adapter.js +56 -489
  60. package/dist-cjs/adapters/interpreter/index.d.ts +1 -1
  61. package/dist-cjs/adapters/interpreter/index.js +1 -2
  62. package/dist-cjs/adapters/oz/adapter.js +1 -15
  63. package/dist-cjs/errors.d.ts +1 -1
  64. package/dist-cjs/index.d.ts +1 -4
  65. package/dist-cjs/index.js +1 -4
  66. package/dist-cjs/install/build-add-context-rule.d.ts +3 -19
  67. package/dist-cjs/install/build-add-context-rule.js +1 -97
  68. package/dist-cjs/install/build-install-policy.d.ts +4 -53
  69. package/dist-cjs/install/build-install-policy.js +44 -122
  70. package/dist-cjs/install/index.d.ts +0 -2
  71. package/dist-cjs/install/index.js +2 -23
  72. package/dist-cjs/ir/index.d.ts +1 -1
  73. package/dist-cjs/ir/types.d.ts +7 -76
  74. package/dist-cjs/ir/types.js +0 -2
  75. package/dist-cjs/predicate/decode.d.ts +1 -1
  76. package/dist-cjs/predicate/decode.js +2 -70
  77. package/dist-cjs/predicate/encode.js +45 -229
  78. package/dist-cjs/predicate/from-json.js +1 -42
  79. package/dist-cjs/record/decode.js +3 -8
  80. package/dist-cjs/review-card/builder.d.ts +4 -3
  81. package/dist-cjs/review-card/builder.js +23 -167
  82. package/dist-cjs/review-card/cross-check.js +7 -110
  83. package/dist-cjs/review-card/render-leaf.d.ts +4 -0
  84. package/dist-cjs/review-card/render-leaf.js +52 -0
  85. package/dist-cjs/run/index.d.ts +36 -139
  86. package/dist-cjs/run/index.js +125 -444
  87. package/dist-cjs/run/schemas.d.ts +168 -1929
  88. package/dist-cjs/run/schemas.js +41 -270
  89. package/dist-cjs/seams/types.d.ts +0 -2
  90. package/dist-cjs/simulate/deny-cases.d.ts +26 -0
  91. package/dist-cjs/simulate/deny-cases.js +286 -0
  92. package/dist-cjs/simulate/evaluate.d.ts +18 -0
  93. package/dist-cjs/simulate/evaluate.js +274 -0
  94. package/dist-cjs/simulate/index.d.ts +4 -0
  95. package/dist-cjs/simulate/index.js +13 -0
  96. package/dist-cjs/synth/compose-from-recording.d.ts +24 -51
  97. package/dist-cjs/synth/compose-from-recording.js +120 -282
  98. package/dist-cjs/synth/deny-cases.d.ts +5 -7
  99. package/dist-cjs/synth/deny-cases.js +23 -374
  100. package/dist-cjs/synth/evaluate.d.ts +0 -21
  101. package/dist-cjs/synth/evaluate.js +57 -337
  102. package/dist-cjs/synth/index.d.ts +0 -5
  103. package/dist-cjs/synth/index.js +1 -11
  104. package/dist-cjs/synth/permit-context.d.ts +2 -9
  105. package/dist-cjs/synth/permit-context.js +1 -82
  106. package/dist-cjs/synth/synthesize-from-recording.d.ts +1 -29
  107. package/dist-cjs/synth/synthesize-from-recording.js +39 -354
  108. package/dist-cjs/types.d.ts +8 -60
  109. package/dist-cjs/types.js +6 -3
  110. package/dist-cjs/verify/simulate.d.ts +1 -14
  111. package/dist-cjs/verify/simulate.js +2 -88
  112. package/dist-cjs/verify/verify.d.ts +1 -7
  113. package/dist-cjs/verify/verify.js +0 -8
  114. package/package.json +4 -4
  115. package/src/adapters/interpreter/adapter.ts +76 -572
  116. package/src/errors.ts +0 -19
  117. package/src/index.ts +1 -4
  118. package/src/install/build-add-context-rule.ts +5 -139
  119. package/src/install/build-install-policy.ts +93 -214
  120. package/src/install/index.ts +0 -34
  121. package/src/predicate/decode.ts +2 -70
  122. package/src/predicate/encode.ts +49 -261
  123. package/src/predicate/from-json.ts +1 -43
  124. package/src/record/decode.ts +3 -8
  125. package/src/review-card/builder.ts +19 -168
  126. package/src/review-card/cross-check.ts +3 -105
  127. package/src/review-card/render-leaf.ts +48 -0
  128. package/src/run/index.ts +144 -572
  129. package/src/run/schemas.ts +42 -291
  130. package/src/simulate/deny-cases.ts +334 -0
  131. package/src/simulate/evaluate.ts +284 -0
  132. package/src/simulate/index.ts +11 -0
  133. package/src/synth/compose-from-recording.ts +148 -347
  134. package/src/synth/index.ts +0 -13
  135. package/src/synth/synthesize-from-recording.ts +43 -456
  136. package/src/types.ts +13 -49
  137. package/dist/install/authority-overlap.d.ts +0 -134
  138. package/dist/install/authority-overlap.js +0 -0
  139. package/dist/install/build-install-predicate.d.ts +0 -96
  140. package/dist/install/build-install-predicate.js +0 -444
  141. package/dist/install/build-merge-policy.d.ts +0 -70
  142. package/dist/install/build-merge-policy.js +0 -130
  143. package/dist/install/plan-merge-policy.d.ts +0 -49
  144. package/dist/install/plan-merge-policy.js +0 -86
  145. package/dist/install/read-account-rules.d.ts +0 -100
  146. package/dist/install/read-account-rules.js +0 -283
  147. package/dist-cjs/install/authority-overlap.d.ts +0 -134
  148. package/dist-cjs/install/authority-overlap.js +0 -0
  149. package/dist-cjs/install/build-install-predicate.d.ts +0 -96
  150. package/dist-cjs/install/build-install-predicate.js +0 -479
  151. package/dist-cjs/install/build-merge-policy.d.ts +0 -70
  152. package/dist-cjs/install/build-merge-policy.js +0 -134
  153. package/dist-cjs/install/plan-merge-policy.d.ts +0 -49
  154. package/dist-cjs/install/plan-merge-policy.js +0 -90
  155. package/dist-cjs/install/read-account-rules.d.ts +0 -100
  156. package/dist-cjs/install/read-account-rules.js +0 -296
  157. package/src/adapters/interpreter/index.ts +0 -8
  158. package/src/adapters/oz/adapter.ts +0 -376
  159. package/src/adapters/oz/index.ts +0 -9
  160. package/src/codegen/compile-gate.ts +0 -167
  161. package/src/codegen/index.ts +0 -17
  162. package/src/codegen/template.ts +0 -165
  163. package/src/install/authority-overlap.ts +0 -0
  164. package/src/install/build-merge-policy.ts +0 -219
  165. package/src/install/plan-merge-policy.ts +0 -133
  166. package/src/install/read-account-rules.ts +0 -376
  167. package/src/ir/index.ts +0 -13
  168. package/src/ir/types.ts +0 -132
  169. package/src/mandate/index.ts +0 -4
  170. package/src/mandate/to-ir.ts +0 -71
  171. package/src/mandate/types.ts +0 -21
  172. package/src/seams/index.ts +0 -11
  173. package/src/seams/types.ts +0 -81
  174. package/src/synth/deny-cases.ts +0 -663
  175. package/src/synth/evaluate.ts +0 -613
  176. package/src/synth/harness.ts +0 -68
  177. package/src/synth/minimize.ts +0 -48
  178. package/src/synth/permit-context.ts +0 -136
  179. package/src/synth/predicate-literals.ts +0 -27
  180. package/src/synth/synthesize-from-mandate.ts +0 -82
  181. package/src/verify/envelope.ts +0 -28
  182. package/src/verify/index.ts +0 -5
  183. package/src/verify/simulate.ts +0 -311
  184. package/src/verify/verify.ts +0 -243
@@ -1,155 +1,23 @@
1
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
2
  // 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
3
  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 };
4
+ export { OVERPERMISSIVE_DIMENSIONS };
61
5
  /** Build deterministic model-evaluated alternatives without mutating the intended call.
62
6
  *
63
7
  * @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. */
8
+ * @param permitCtx - EvalContext for the intended (permitted) call
9
+ * @param dimensions - optional whitelist of dimension names to emit; when omitted
10
+ * all known dimensions (including over-permissiveness mutations)
11
+ * are emitted. */
69
12
  export function generateCases(predicate, permitCtx, dimensions) {
70
13
  const facts = inspectPredicate(predicate);
71
14
  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
- }
15
+ // arg_bound: set a constrained arg to an opaque ScVal.
16
+ // The reason depends on the predicate shape:
17
+ // - an `eq(call_arg[i], literal)` denies with ARG_MISMATCH
18
+ // - an `in(call_arg[i], [literals])` denies with NOT_IN_ALLOWLIST
19
+ // Both are correct in the TS evaluator; the reason is predicate-dependent
20
+ // so we cannot pin a single canonical reason here.
153
21
  const argumentConstraints = [];
154
22
  for (const comparison of facts.comparisons) {
155
23
  if (comparison.op === 'eq' &&
@@ -166,54 +34,10 @@ export function generateCases(predicate, permitCtx, dimensions) {
166
34
  for (const constraint of argumentConstraints) {
167
35
  const ctx = cloneContext(permitCtx);
168
36
  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
37
  denies.push({ dimension: 'arg_bound', ctx });
179
38
  }
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
- }
39
+ // soroswap_allowed_path: replace a bound vec arg with a different vector.
40
+ // Still valid for eq comparisons on literal_vec leaves.
217
41
  for (const comparison of facts.comparisons) {
218
42
  if (comparison.op !== 'eq' ||
219
43
  comparison.left.kind !== 'call_arg' ||
@@ -224,7 +48,7 @@ export function generateCases(predicate, permitCtx, dimensions) {
224
48
  ctx.args[comparison.left.index] = differentVector(ctx.args[comparison.left.index]);
225
49
  denies.push({ dimension: 'soroswap_allowed_path', ctx });
226
50
  }
227
- // --- argument_reorder: swap first two address args ---
51
+ // argument_reorder: swap first two address args.
228
52
  // Skipped when dimensions filter is active so the synth pipeline can emit a policy;
229
53
  // the over-permissiveness harness then tests this mutation as a FINDING.
230
54
  const constrainedArgIndices = new Set();
@@ -252,12 +76,10 @@ export function generateCases(predicate, permitCtx, dimensions) {
252
76
  ctx.args[1] = a0;
253
77
  denies.push({ dimension: 'argument_reorder', ctx });
254
78
  }
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.
79
+ // map_field_flip: flip a bound map field to a different valid value of
80
+ // the same type. OPT-IN only, never ORIGINAL.
257
81
  // 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. ---
82
+ // has a different value of the recorded field. ---
261
83
  if (!dimensions || dimensions.includes('map_field_flip')) {
262
84
  for (const comparison of facts.comparisons) {
263
85
  const sel = comparison.left;
@@ -286,9 +108,9 @@ export function generateCases(predicate, permitCtx, dimensions) {
286
108
  denies.push({ dimension: 'map_field_flip', ctx });
287
109
  }
288
110
  }
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. ---
111
+ // vec_append: append a new element to a bound vec. OPT-IN only, never ORIGINAL.
112
+ // Targets every `call_arg_len` leaf; without the length pin a caller can
113
+ // append an extra element to defeat per-element binds. ---
292
114
  if (!dimensions || dimensions.includes('vec_append')) {
293
115
  for (const comparison of facts.comparisons) {
294
116
  const sel = comparison.left;
@@ -305,7 +127,6 @@ export function generateCases(predicate, permitCtx, dimensions) {
305
127
  denies.push({ dimension: 'vec_append', ctx });
306
128
  }
307
129
  }
308
- // Version mismatch, malformed predicates, master authorization, and nonce replay are install-time checks and are intentionally omitted from model-evaluated cases.
309
130
  return { permit: cloneContext(permitCtx), denies };
310
131
  }
311
132
  function inspectPredicate(predicate) {
@@ -316,162 +137,17 @@ function inspectPredicate(predicate) {
316
137
  function visit(node, facts) {
317
138
  switch (node.op) {
318
139
  case 'and':
319
- case 'or':
320
140
  for (const child of node.children)
321
141
  visit(child, facts);
322
142
  return;
323
- case 'not':
324
- visit(node.child, facts);
325
- return;
326
143
  case 'in':
327
144
  facts.memberships.push(node);
328
145
  return;
329
146
  case 'eq':
330
- case 'lt':
331
147
  case 'lte':
332
- case 'gt':
333
- case 'gte':
334
148
  facts.comparisons.push(node);
335
149
  }
336
150
  }
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
151
  function differentVector(actual) {
476
152
  if (actual?.type !== 'vec')
477
153
  return { type: 'vec', value: [] };
@@ -488,40 +164,16 @@ function differentVector(actual) {
488
164
  value[0] = { type: 'other', value: 'deny-case-different-hop' };
489
165
  return { type: 'vec', value };
490
166
  }
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
167
  function cloneContext(ctx) {
498
- const cloned = {
168
+ return {
499
169
  contract: ctx.contract,
500
170
  fn: ctx.fn,
501
171
  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
172
  };
512
- if (ctx.validUntilLedger !== undefined)
513
- cloned.validUntilLedger = ctx.validUntilLedger;
514
- if (ctx.signerWeights !== undefined)
515
- cloned.signerWeights = { ...ctx.signerWeights };
516
- return cloned;
517
173
  }
518
174
  /** Deep-copy an ScVal so a mutation cannot alias the recorded call.
519
175
  * Exported so the permit-context builder shares this one implementation. */
520
176
  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
177
  if (value.type === 'vec') {
526
178
  if (depth >= MAX_SCVAL_CLONE_DEPTH) {
527
179
  throw cloneDepthError(value);
@@ -540,9 +192,8 @@ function cloneDepthError(value) {
540
192
  }
541
193
  /** Build a value of the SAME ScVal type that differs from the recorded one,
542
194
  * 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). */
195
+ * changing the wire type. Returns null when the ScVal type has no obvious
196
+ * different value (e.g. opaque/other). */
546
197
  function flipFieldValue(val) {
547
198
  switch (val.type) {
548
199
  case 'address': {
@@ -551,7 +202,6 @@ function flipFieldValue(val) {
551
202
  return { type: 'address', value: val.value === a ? b : a };
552
203
  }
553
204
  case 'i128':
554
- case 'u64':
555
205
  case 'u32':
556
206
  return { type: val.type, value: String(BigInt(val.value) + 1n) };
557
207
  case 'symbol':
@@ -6,27 +6,6 @@ export interface EvalContext {
6
6
  fn: string;
7
7
  /** Decoded `ScVal[]` of the top-level authorized call. */
8
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
9
  }
31
10
  export type EvalResult = {
32
11
  permit: true;