@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
@@ -2,16 +2,10 @@
2
2
  // src/adapters/interpreter/adapter.ts - the interpreter-policy CustodyAdapter.
3
3
  //
4
4
  // Compiles a PolicyIR to a single interpreter `PolicyDocument` + `PolicyRef`
5
- // carrying the canonical predicate encoding from `predicate/encode.ts`. Second
6
- // backend (the OZ built-in adapter is the first); the compose step (P3) routes
7
- // IR constructs between them.
5
+ // carrying the canonical predicate encoding from `predicate/encode.ts`. This is
6
+ // the only backend; the compose step lowers every constraint to it.
8
7
  //
9
8
  // Three fail-closed enforcement gates (per spec):
10
- // - oracle_price leaves MUST sit directly under the top-level `and`; nesting
11
- // under `not` / `or` / inside `in` throws ORACLE_LEAF_INVALID_POSITION.
12
- // - oracleParams overrides may only TIGHTEN vs wasm defaults
13
- // (maxStalenessSeconds <= 600, maxDeviationBps <= 200); a widening value
14
- // throws ORACLE_PARAMS_OUT_OF_RANGE at compile time.
15
9
  // - an `in` allowlist (or a `compare eq` vs an address, or any value in an
16
10
  // `eq_seq`) that targets the smart account's own address throws
17
11
  // SCOPE_SELF_CALL.
@@ -19,31 +13,16 @@
19
13
  // Anything the adapter genuinely cannot express (EVM calldata / value, unix
20
14
  // expiry, etc.) is named in `uncovered` rather than silently dropped.
21
15
  Object.defineProperty(exports, "__esModule", { value: true });
22
- exports.ORACLE_DEFAULTS = exports.PLACEHOLDER_INTERPRETER_ADDRESS = void 0;
23
- exports.createInterpreterAdapter = createInterpreterAdapter;
16
+ exports.PLACEHOLDER_INTERPRETER_ADDRESS = void 0;
24
17
  exports.lowerRuleToPredicate = lowerRuleToPredicate;
18
+ exports.compileInterpreterPolicy = compileInterpreterPolicy;
25
19
  const encode_ts_1 = require("../../predicate/encode.js");
20
+ const types_ts_1 = require("../../types.js");
26
21
  /** [VERIFY] NOT a real deployed address. The interpreter is a per-network
27
22
  * deploy artifact we do not have yet; install is a later phase. */
28
23
  exports.PLACEHOLDER_INTERPRETER_ADDRESS = 'VERIFY-interpreter-address';
29
- /** Wasm-level oracle defaults (mirrors the interpreter's `OracleParams`).
30
- * Per-policy overrides may TIGHTEN only - they may never exceed these. */
31
- exports.ORACLE_DEFAULTS = {
32
- maxStalenessSeconds: 600,
33
- maxDeviationBps: 200,
34
- };
35
- const CAPABILITIES = {
36
- supportsSpendWindow: true,
37
- supportsThreshold: false, // thresholds are the OZ adapter's job
38
- // Expiry is via the context rule's validUntilLedger, not a predicate - the
39
- // interpreter refuses a `valid_until` leaf at install.
40
- supportsTimeExpiry: false,
41
- supportsOraclePrice: true,
42
- supportsInvocationCount: true,
43
- supportsGeneralPredicate: true,
44
- };
45
- /** Parse confidence for a deterministic (non-decoded) input: full (1.0). A
46
- * mandate needs no decoding, so the gate is not applicable. */
24
+ /** Parse confidence for a deterministic (non-decoded) input: full (1.0). An
25
+ * input that needs no decoding has nothing for the gate to judge. */
47
26
  const FULL_PARSE_CONFIDENCE = {
48
27
  overall: 1,
49
28
  knownContracts: [],
@@ -51,53 +30,26 @@ const FULL_PARSE_CONFIDENCE = {
51
30
  opaqueScVals: [],
52
31
  thresholdUsed: 1,
53
32
  };
54
- function createInterpreterAdapter(config) {
55
- return {
56
- name: 'interpreter',
57
- mode: 'enforce',
58
- capabilities: () => ({ ...CAPABILITIES }),
59
- compile: (ir) => compile(ir, config),
60
- simulate: () => simulateStub(),
61
- export: (ir) => canonicalStringify(ir),
62
- };
63
- }
64
- /** Lower a single IR rule to the canonical pre-encoding `PredicateNode`. The
65
- * orchestrator uses this to wire the self-verify + minimise pipeline: after
66
- * `compile(ir)` succeeds it re-derives the PredicateNode via this helper to
67
- * drive `minimize` and `runHarness` on the SAME shape the encoder saw. Pure
68
- * and deterministic: same `rule + config` -> byte-identical PredicateNode.
69
- * The `uncovered` list is NOT re-derived - callers needing it must use
70
- * `compile(ir)`. */
33
+ /** Lower a single rule to the canonical pre-encoding `PredicateNode`. The
34
+ * orchestrator re-derives the PredicateNode through this helper so it holds the
35
+ * SAME shape the encoder saw. Pure and deterministic: same `rule + config` ->
36
+ * byte-identical PredicateNode. The `uncovered` list is NOT re-derived -
37
+ * callers needing it must use `compileInterpreterPolicy(rule, config)`. */
71
38
  function lowerRuleToPredicate(rule, config) {
72
39
  return lowerRule(rule, config).predicate;
73
40
  }
74
- function compile(ir, config) {
75
- // Tighten-only oracle-params gate (config-time). Widening -> throw at compile
76
- // time before any lowering happens, so a misuse fails closed loudly.
77
- assertOracleParamsTighten(config.oracleParams);
78
- const firstRule = ir.rules[0];
79
- if (!firstRule) {
80
- return { covered: false, uncovered: ['empty PolicyIR (no rules to compile)'] };
81
- }
82
- if (ir.rules.length > 1) {
83
- return {
84
- covered: false,
85
- uncovered: [
86
- `multi-rule PolicyIR: ${ir.rules.length - 1} rule(s) beyond the first are not compiled (the interpreter adapter handles one rule per document in this slice)`,
87
- ],
88
- };
89
- }
90
- const lowered = lowerRule(firstRule, config);
41
+ /** Compile a composed rule to an installable interpreter policy. */
42
+ function compileInterpreterPolicy(rule, config) {
43
+ const lowered = lowerRule(rule, config);
91
44
  if (lowered.uncovered.length > 0) {
92
45
  return { covered: false, uncovered: lowered.uncovered };
93
46
  }
94
47
  const { encodedPredicate, predicateHash } = (0, encode_ts_1.encodePredicate)(lowered.predicate);
95
48
  const policyDocument = {
96
- grammarVersion: 1,
49
+ grammarVersion: types_ts_1.GRAMMAR_VERSION,
97
50
  installNonce: config.installNonce,
98
51
  encodedPredicate,
99
52
  predicateHash,
100
- ...(config.oracleParams ? { oracleParams: config.oracleParams } : {}),
101
53
  };
102
54
  const policyRef = {
103
55
  kind: 'interpreter',
@@ -118,56 +70,18 @@ function lowerRule(rule, config) {
118
70
  const uncovered = [];
119
71
  // scope -> context rule + sibling predicates. contract/method each become
120
72
  // their own `eq` leaf and are merged into the top-level `and` alongside the
121
- // constraints. The top-level MUST be `and` so oracle leaves sit directly
122
- // under it (mandatory per the oracle position rule + canonical hash stability).
73
+ // constraints. The top-level MUST be `and` for canonical hash stability.
123
74
  const scopeContract = rule.scope.contract;
124
75
  const scopeMethod = rule.scope.method;
125
- if (rule.scope.chainId !== undefined) {
126
- uncovered.push(`chainId \`${rule.scope.chainId}\` not bindable by the interpreter adapter (network is per-context, not per-rule)`);
127
- }
128
- if (rule.roles.length > 0) {
129
- uncovered.push(`roles [${rule.roles.join(', ')}] dropped (role-to-signer mapping is a later phase; interpreter policies reference addresses, not role names)`);
130
- }
131
- if (rule.guard) {
132
- uncovered.push(`guard: ${describeCondition(rule.guard)} (interpreter adapter currently treats guards as uncovered)`);
133
- }
134
- // expiry -> context rule validUntilLedger only. Unix-timestamp expiry is
135
- // honoured by the interpreter (the `valid_until` selector), but we surface
136
- // it here as Path-B because the IR contract already says "OZ context rules
137
- // expire by ledger sequence" and we keep one expiry model for both adapters.
138
- let validUntilLedger = null;
139
- if (rule.expiry) {
140
- if (rule.expiry.validUntilLedger !== undefined) {
141
- validUntilLedger = rule.expiry.validUntilLedger;
142
- }
143
- else if (rule.expiry.validUntilUnixSeconds !== undefined) {
144
- uncovered.push('time expiry given as a unix timestamp (interpreter adapter currently lowers expiry only via the OZ context-rule validUntilLedger; supply expiry.validUntilLedger)');
145
- }
146
- }
147
- // approval is the OZ adapter's job (it lowers to simple_threshold /
148
- // weighted_threshold primitives). Surface it here so a caller that drops it
149
- // through to the interpreter adapter sees an explicit uncovered entry
150
- // instead of a silently-dropped M-of-N gate.
151
- if (rule.approval) {
152
- uncovered.push(`approval threshold ${rule.approval.threshold} not emitted by the interpreter adapter (thresholds lower to OZ built-in primitives in the OZ adapter)`);
153
- }
76
+ // expiry -> the context rule's own validUntilLedger. The interpreter has no
77
+ // expiry selector; a policy's lifetime is the OZ context rule's lifetime.
78
+ const validUntilLedger = rule.expiry?.validUntilLedger ?? null;
154
79
  // Pre-scan constraints: anything the interpreter adapter cannot express is
155
80
  // named in `uncovered` and skipped from the predicate lowering. We surface
156
81
  // these BEFORE lowering so the predicate is built from only the
157
82
  // expressible subset.
158
- const expressibleConstraints = [];
159
- for (const c of rule.constraints) {
160
- const unsupp = unsupportedConstruct(c);
161
- if (unsupp !== null) {
162
- uncovered.push(unsupp);
163
- }
164
- else {
165
- expressibleConstraints.push(c);
166
- }
167
- }
168
83
  // Build the top-level `and` of scope + expressible constraints. The
169
- // top-level MUST be `and` so oracle leaves sit directly under it - the
170
- // position rule is enforced inside `lowerCondition`.
84
+ // top-level MUST be `and` for canonical hash stability.
171
85
  const topChildren = [];
172
86
  if (scopeContract !== undefined) {
173
87
  topChildren.push({
@@ -183,14 +97,23 @@ function lowerRule(rule, config) {
183
97
  right: { kind: 'literal_symbol', value: scopeMethod },
184
98
  });
185
99
  }
186
- for (const c of expressibleConstraints) {
187
- topChildren.push(lowerCondition(c, config));
100
+ topChildren.push(...rule.constraints);
101
+ // One walk over the whole tree rejects any literal naming the smart account
102
+ // itself. Previously three separate call sites did this per construct, which
103
+ // meant a new construct could be added without one.
104
+ for (const node of topChildren) {
105
+ assertNoSelfCall(node, config);
188
106
  }
189
107
  const predicate = topChildren.length === 1 && topChildren[0] !== undefined
190
108
  ? topChildren[0]
191
109
  : { op: 'and', children: topChildren };
110
+ // Scope the context rule to the contract when the rule names one, matching
111
+ // what OZ's CallContract scoping did. A `default` rule would route EVERY
112
+ // call through this policy instead of only calls to the scoped contract.
192
113
  const contextRule = {
193
- contextRuleType: { kind: 'default' },
114
+ contextRuleType: scopeContract !== undefined
115
+ ? { kind: 'call_contract', contract: scopeContract }
116
+ : { kind: 'default' },
194
117
  name: 'interpreter',
195
118
  validUntilLedger,
196
119
  signers: [],
@@ -198,392 +121,36 @@ function lowerRule(rule, config) {
198
121
  };
199
122
  return { predicate, contextRule, uncovered };
200
123
  }
201
- /** Detect IR constructs the interpreter adapter cannot express; return a
202
- * human-readable descriptor (or null if the construct IS expressible). Used
203
- * by `lowerRule` to populate `uncovered` before lowering. */
204
- function unsupportedConstruct(cond) {
205
- switch (cond.op) {
206
- // Expressible: `call_arg_scaled` is exactly this construct.
207
- case 'slippage_floor':
208
- return null;
209
- case 'in':
210
- // The oracle position rule is a hard error and must be raised before a
211
- // construct can be written off as uncovered - otherwise a subtree that
212
- // is BOTH misplaced-oracle and unsourceable would be silently dropped
213
- // instead of rejected.
214
- assertNoOracleDescendants(cond);
215
- return (unsourceableSelector(cond.selector) ??
216
- (cond.selector.kind === 'calldata' || cond.selector.kind === 'value'
217
- ? `EVM \`${cond.selector.kind}\` selector on allowlist (predicate DSL)`
218
- : null));
219
- case 'eq_seq':
220
- return (unsourceableSelector(cond.selector) ??
221
- (cond.selector.kind === 'calldata' || cond.selector.kind === 'value'
222
- ? `EVM \`${cond.selector.kind}\` selector on ordered-sequence equality (predicate DSL)`
223
- : null));
224
- case 'compare': {
225
- const s = cond.compare.selector;
226
- if (s.kind === 'calldata')
227
- return 'EVM calldata comparison (predicate DSL)';
228
- if (s.kind === 'value')
229
- return 'tx.value comparison (predicate DSL)';
230
- // The on-chain interpreter sees ONE authorized call - no
231
- // `Context.sub_invocations` in v1 - so it cannot observe token
232
- // movements. `amount` has no value to read, and `window_spent`
233
- // accumulates BY that amount, so its counter would never move.
234
- // Deriving either from the call payload would quietly swap "value
235
- // actually moved" for "value the caller declared" - a weaker guarantee
236
- // than the review card would be claiming.
237
- //
238
- // Rolling spend caps belong to the OZ `spending_limit` primitive (already
239
- // audited, emitted by the OZ adapter). A per-call cap is expressible here
240
- // as `arg_field`; bounding it with `invocation_count` gives an enforceable
241
- // ceiling per window.
242
- return unsourceableSelector(s);
243
- }
244
- // Recurse: a nested `and`/`or`/`not` must not smuggle a selector past the
245
- // pre-scan, which only sees top-level constraints.
246
- case 'and':
247
- return cond.children.map(unsupportedConstruct).find((u) => u !== null) ?? null;
248
- case 'or':
249
- assertNoOracleDescendants(cond);
250
- return cond.children.map(unsupportedConstruct).find((u) => u !== null) ?? null;
251
- case 'not':
252
- assertNoOracleDescendants(cond);
253
- return unsupportedConstruct(cond.child);
254
- }
255
- }
256
- /** Selectors whose value the on-chain interpreter has no way to obtain. */
257
- function unsourceableSelector(s) {
258
- if (s.kind === 'amount') {
259
- return `per-call amount comparison on ${s.token} - the interpreter cannot observe token movements; express a per-call cap with arg_field, or a rolling cap with the OZ spending_limit primitive`;
260
- }
261
- if (s.kind === 'window_spent') {
262
- return `rolling spend cap on ${s.token} over ${s.windowSeconds}s - not enforceable by the interpreter; use the OZ spending_limit primitive, or bound per-call value with arg_field plus invocation_count`;
263
- }
264
- return null;
265
- }
266
- /** Lower one IR condition to a PredicateNode. Enforces the oracle position
267
- * rule (oracle_price leaves must sit directly under the top-level `and`) and
268
- * the self-call rule (`in` allowlist / `compare eq` vs the smart account
269
- * address -> SCOPE_SELF_CALL). */
270
- function lowerCondition(cond, config) {
271
- switch (cond.op) {
272
- // `out >= in * num/den`. The contract refuses den == 0 or a non-positive
273
- // ratio at install, so a malformed floor fails loudly rather than
274
- // silently inverting the comparison.
275
- case 'slippage_floor':
276
- return {
277
- op: 'gte',
278
- left: { kind: 'call_arg', index: cond.outArgIndex },
279
- right: {
280
- kind: 'call_arg_scaled',
281
- index: cond.inArgIndex,
282
- num: cond.num,
283
- den: cond.den,
284
- },
285
- };
286
- case 'and':
287
- return { op: 'and', children: cond.children.map((c) => lowerCondition(c, config)) };
288
- case 'or':
289
- // `or` at this depth: oracle_price anywhere inside is invalid (would
290
- // not be a direct child of the top-level `and`).
291
- assertNoOracleDescendants(cond);
292
- return { op: 'or', children: cond.children.map((c) => lowerCondition(c, config)) };
293
- case 'not':
294
- // `not` at this depth: oracle_price anywhere inside is invalid.
295
- assertNoOracleDescendants(cond);
296
- return { op: 'not', child: lowerCondition(cond.child, config) };
297
- case 'in': {
298
- assertNoOracleDescendants(cond);
299
- for (const v of cond.values) {
300
- assertNotSelfCallAddress(v, config);
301
- }
302
- // `in` is PURE set membership; the encoder sorts the haystack. An exact
303
- // ordered sequence (e.g. swap hop path) is expressed as `eq_seq` and
304
- // lowers to `eq(selector, literal_vec)` instead - never to `in` with an
305
- // `ordered` flag.
306
- return {
307
- op: 'in',
308
- needle: lowerSelector(cond.selector),
309
- haystack: cond.values.map((v) => literalFromScalar(v, selectorScalarType(cond.selector))),
310
- };
311
- }
312
- case 'eq_seq': {
313
- assertNoOracleDescendants(cond);
314
- for (const v of cond.values) {
315
- assertNotSelfCallAddress(v, config);
316
- }
317
- // Exact ordered sequence equality: the right-hand side is a literal_vec
318
- // whose element order is preserved verbatim by the encoder. `eq` does
319
- // deep equality at evaluate time - this is the ONLY way to express an
320
- // exact ordered sequence in the predicate grammar (the path of a swap).
321
- return {
322
- op: 'eq',
323
- left: lowerSelector(cond.selector),
324
- right: {
325
- kind: 'literal_vec',
326
- elements: cond.values.map((v) => literalFromScalar(v, selectorScalarType(cond.selector))),
327
- },
328
- };
329
- }
330
- case 'compare': {
331
- // Self-call on an address eq.
332
- if (cond.compare.operator === 'eq' &&
333
- cond.compare.selector.kind === 'arg' &&
334
- cond.compare.selector.scalarType === 'address') {
335
- assertNotSelfCallAddress(cond.compare.value, config);
336
- }
337
- return {
338
- op: cond.compare.operator,
339
- left: lowerSelector(cond.compare.selector),
340
- right: literalFromIRCompare(cond.compare),
341
- };
124
+ /** Reject a value that targets the smart account's own address (a self-call
125
+ * is a structural privilege-escalation hole the interpreter forbids). */
126
+ /** Reject any address literal anywhere in the predicate that names the smart
127
+ * account itself: a policy permitting the account to call itself would let an
128
+ * agent re-enter the guarded account and escape the scope the rule pins. */
129
+ function assertNoSelfCall(node, config) {
130
+ const checkLeaf = (leaf) => {
131
+ if (leaf.kind === 'literal_vec') {
132
+ for (const e of leaf.elements)
133
+ checkLeaf(e);
134
+ return;
342
135
  }
343
- }
344
- }
345
- /** Lower an IR selector to the matching PredicateLeaf. */
346
- function lowerSelector(s) {
347
- switch (s.kind) {
348
- case 'arg':
349
- return { kind: 'call_arg', index: s.argIndex };
350
- case 'arg_len':
351
- return { kind: 'call_arg_len', index: s.argIndex };
352
- case 'arg_field':
353
- return { kind: 'call_arg_field', index: s.argIndex, element: s.element, field: s.field };
354
- // `amount` / `window_spent` are filtered out by `unsupportedConstruct`
355
- // before lowering - the interpreter cannot source either on chain.
356
- // Reaching here means the pre-scan was bypassed; fail loudly rather than
357
- // emit a leaf the contract will refuse.
358
- case 'amount':
359
- case 'window_spent':
360
- throw new Error(`interpreter adapter cannot lower \`${s.kind}\`: it should have been reported as uncovered`);
361
- case 'invocation_count':
362
- return { kind: 'invocation_count_in_window', windowSecs: s.windowSeconds };
363
- case 'now':
364
- return { kind: 'now' };
365
- case 'valid_until':
366
- return { kind: 'valid_until' };
367
- case 'oracle_price':
368
- return { kind: 'oracle_price', asset: s.asset };
369
- case 'calldata':
370
- case 'value':
371
- // Unreachable: the caller flagged these as Path-B before reaching here.
372
- throw toolError('SYNTHESIS_ERROR', `selector kind \`${s.kind}\` is not lowerable to a predicate leaf`);
373
- }
374
- }
375
- /** Build a literal leaf from the right-hand side of an IRCompare, mapping the
376
- * selector kind to the matching `literal_*` kind. The IR compare value is a
377
- * raw string (i128-safe); the selector kind fixes the canonical wire type:
378
- * - arg -> IR scalarType (set by the recorder/parser)
379
- * - arg_len -> u32 (vec length is a small non-negative integer)
380
- * - arg_field -> IR scalarType (the field's recorded type)
381
- * - amount -> i128 (canonical Stellar token amount encoding)
382
- * - window_spent -> i128 (canonical amount encoding)
383
- * - oracle_price -> i128 (canonical price encoding)
384
- * - invocation_count -> u32 (counts are small non-negative integers)
385
- * - now / valid_until -> u64 (unix timestamps in seconds) */
386
- function literalFromIRCompare(c) {
387
- // An oracle comparand is not a bare literal: the interpreter refuses one,
388
- // because assuming it shares the normalised 9-dp basis is what made a raw
389
- // 14-dp threshold permit everything it was written to deny.
390
- if (c.selector.kind === 'oracle_price') {
391
- if (c.valueDecimals === undefined) {
392
- throw toolError('ORACLE_THRESHOLD_BASIS_REQUIRED', 'An oracle price bound must declare the decimal basis of its threshold ' +
393
- '(oracle prices normalise to 9 decimals).');
136
+ if (leaf.kind === 'literal_address' && leaf.value === config.smartAccountAddress) {
137
+ throw toolError('SCOPE_SELF_CALL', `value \`${leaf.value}\` is the smart account's own address (self-call in an allowlist / compare is rejected)`);
394
138
  }
395
- return { kind: 'oracle_threshold', value: c.value, decimals: c.valueDecimals };
396
- }
397
- const scalarType = selectorScalarType(c.selector);
398
- return literalFromScalar(c.value, scalarType);
399
- }
400
- function literalScalarForSelector(kind) {
401
- switch (kind) {
402
- case 'amount':
403
- case 'window_spent':
404
- case 'oracle_price':
405
- return 'i128';
406
- case 'invocation_count':
407
- case 'arg_len':
408
- return 'u32';
409
- case 'now':
410
- case 'valid_until':
411
- return 'u64';
412
- case 'arg':
413
- case 'arg_field':
414
- case 'calldata':
415
- case 'value':
416
- // arg / arg_field -> caller handles scalarType; calldata/value -> unreachable (Path-B).
417
- return 'i128';
418
- }
419
- }
420
- /** Build a literal leaf from a raw string value + an IRScalarType hint. */
421
- function literalFromScalar(value, scalarType) {
422
- switch (scalarType) {
423
- case 'address':
424
- return { kind: 'literal_address', value };
425
- case 'i128':
426
- case 'u128':
427
- return { kind: 'literal_i128', value };
428
- case 'u32':
429
- return { kind: 'literal_u32', value: Number.parseInt(value, 10) };
430
- case 'u64':
431
- case 'i64':
432
- return { kind: 'literal_u64', value };
433
- case 'symbol':
434
- return { kind: 'literal_symbol', value };
435
- case 'bytes':
436
- return { kind: 'literal_bytes', value };
437
- case 'bool':
438
- throw toolError('MALFORMED_PREDICATE', `boolean literal not supported in v1 predicate grammar`);
439
- }
440
- }
441
- /** Scalar type of an IRSelector for the purpose of building literal leaves
442
- * (the right-hand side of `eq` / elements of an `in` haystack / elements of
443
- * a `literal_vec`). Mirrors `literalScalarForSelector` for OZ extensions and
444
- * uses the selector's own `scalarType` for `arg` and `arg_field` selectors. */
445
- function selectorScalarType(selector) {
446
- if (selector.kind === 'arg')
447
- return selector.scalarType;
448
- if (selector.kind === 'arg_field')
449
- return selector.scalarType;
450
- return literalScalarForSelector(selector.kind);
451
- }
452
- /** Walk a condition tree and throw if any oracle_price leaf is found anywhere
453
- * inside. Used by `lowerCondition` for `not`, `or`, and `in` - the three
454
- * positions where oracle leaves would not be direct children of the
455
- * top-level `and`. */
456
- function assertNoOracleDescendants(node) {
457
- if (containsOracle(node)) {
458
- throw toolError('ORACLE_LEAF_INVALID_POSITION', 'oracle_price leaves must sit directly under the top-level `and`');
459
- }
460
- }
461
- function containsOracle(node) {
139
+ };
462
140
  switch (node.op) {
463
- case 'slippage_floor':
464
- return false;
465
- case 'compare':
466
- return node.compare.selector.kind === 'oracle_price';
467
- case 'in':
468
- return node.selector.kind === 'oracle_price';
469
- case 'eq_seq':
470
- return node.selector.kind === 'oracle_price';
471
- case 'not':
472
- return containsOracle(node.child);
473
141
  case 'and':
474
- case 'or':
475
- return node.children.some(containsOracle);
476
- }
477
- }
478
- /** Reject a value that targets the smart account's own address (a self-call
479
- * is a structural privilege-escalation hole the interpreter forbids). */
480
- function assertNotSelfCallAddress(value, config) {
481
- if (value === config.smartAccountAddress) {
482
- throw toolError('SCOPE_SELF_CALL', `value \`${value}\` is the smart account's own address (self-call in an allowlist / compare is rejected)`);
483
- }
484
- }
485
- /** Tighten-only oracle-params gate. Any widening value (above the wasm-level
486
- * defaults) throws ORACLE_PARAMS_OUT_OF_RANGE at compile time. */
487
- function assertOracleParamsTighten(params) {
488
- if (!params)
489
- return;
490
- if (params.maxStalenessSeconds !== undefined &&
491
- params.maxStalenessSeconds > exports.ORACLE_DEFAULTS.maxStalenessSeconds) {
492
- throw toolError('ORACLE_PARAMS_OUT_OF_RANGE', `maxStalenessSeconds ${params.maxStalenessSeconds} exceeds default ${exports.ORACLE_DEFAULTS.maxStalenessSeconds}`);
493
- }
494
- if (params.maxDeviationBps !== undefined &&
495
- params.maxDeviationBps > exports.ORACLE_DEFAULTS.maxDeviationBps) {
496
- throw toolError('ORACLE_PARAMS_OUT_OF_RANGE', `maxDeviationBps ${params.maxDeviationBps} exceeds default ${exports.ORACLE_DEFAULTS.maxDeviationBps}`);
497
- }
498
- }
499
- /** Human-readable descriptor for a construct the interpreter adapter cannot
500
- * express. Mirrors the OZ adapter's `describeCondition` for parity. */
501
- function describeCondition(cond) {
502
- switch (cond.op) {
503
- case 'slippage_floor':
504
- return `slippage floor: arg[${cond.outArgIndex}] >= arg[${cond.inArgIndex}] * ${cond.num}/${cond.den}`;
142
+ for (const c of node.children)
143
+ assertNoSelfCall(c, config);
144
+ return;
505
145
  case 'in':
506
- return `value allowlist on ${describeSelector(cond.selector)} (predicate DSL)`;
507
- case 'eq_seq':
508
- return `exact ordered sequence on ${describeSelector(cond.selector)} (predicate DSL)`;
509
- case 'not':
510
- return 'negated condition (predicate DSL)';
511
- case 'and':
512
- case 'or':
513
- return `nested ${cond.op} condition (predicate DSL)`;
514
- case 'compare': {
515
- const s = cond.compare.selector;
516
- switch (s.kind) {
517
- case 'oracle_price':
518
- return `oracle price condition on ${s.asset}`;
519
- case 'invocation_count':
520
- return `invocation-count window (${s.windowSeconds}s) condition`;
521
- case 'window_spent':
522
- return `spend-window comparison with operator '${cond.compare.operator}'`;
523
- case 'amount':
524
- return `per-call amount comparison on ${s.token}`;
525
- case 'arg':
526
- return `argument comparison on arg ${s.argIndex}`;
527
- case 'arg_len':
528
- return `vec length comparison on arg ${s.argIndex}`;
529
- case 'arg_field':
530
- return `map field comparison on arg ${s.argIndex}.${s.field}`;
531
- case 'calldata':
532
- return 'EVM calldata comparison';
533
- case 'value':
534
- return 'tx.value comparison';
535
- case 'now':
536
- case 'valid_until':
537
- return 'time comparison';
538
- }
539
- }
540
- }
541
- }
542
- function describeSelector(s) {
543
- switch (s.kind) {
544
- case 'arg':
545
- return `arg ${s.argIndex}`;
546
- case 'arg_len':
547
- return `arg_len(${s.argIndex})`;
548
- case 'arg_field':
549
- return `arg_field(${s.argIndex}, ${s.element}, ${s.field})`;
550
- case 'amount':
551
- return `amount(${s.token})`;
552
- case 'window_spent':
553
- return `window_spent(${s.token})`;
554
- case 'oracle_price':
555
- return `oracle_price(${s.asset})`;
556
- case 'invocation_count':
557
- return `invocation_count(${s.windowSeconds}s)`;
558
- case 'calldata':
559
- return `calldata[${s.offset}:${s.offset + s.length}]`;
146
+ // The needle is a selector; only the haystack carries literals.
147
+ for (const h of node.haystack)
148
+ checkLeaf(h);
149
+ return;
560
150
  default:
561
- return s.kind;
562
- }
563
- }
564
- function simulateStub() {
565
- return {
566
- backend: 'ts-model',
567
- permitted: null,
568
- evaluations: [],
569
- notes: ['stub: real permit/deny semantics wiring is a later phase'],
570
- };
571
- }
572
- /** Canonical JSON with recursively sorted object keys (stable across runs). */
573
- function canonicalStringify(value) {
574
- return JSON.stringify(sortKeys(value));
575
- }
576
- function sortKeys(value) {
577
- if (Array.isArray(value))
578
- return value.map(sortKeys);
579
- if (value && typeof value === 'object') {
580
- const out = {};
581
- for (const key of Object.keys(value).sort()) {
582
- out[key] = sortKeys(value[key]);
583
- }
584
- return out;
151
+ checkLeaf(node.left);
152
+ checkLeaf(node.right);
585
153
  }
586
- return value;
587
154
  }
588
155
  /** Build a synthetic Error carrying the ToolError code/severity/retryable
589
156
  * fields the existing ToolError shape uses (the encoder pattern). Callers
@@ -1 +1 @@
1
- export { createInterpreterAdapter, type InterpreterAdapterConfig, ORACLE_DEFAULTS, PLACEHOLDER_INTERPRETER_ADDRESS, } from './adapter.ts';
1
+ export { createInterpreterAdapter, type InterpreterAdapterConfig, PLACEHOLDER_INTERPRETER_ADDRESS, } from './adapter.ts';
@@ -1,8 +1,7 @@
1
1
  "use strict";
2
2
  // src/adapters/interpreter/index.ts - re-export the interpreter-policy CustodyAdapter.
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.PLACEHOLDER_INTERPRETER_ADDRESS = exports.ORACLE_DEFAULTS = exports.createInterpreterAdapter = void 0;
4
+ exports.PLACEHOLDER_INTERPRETER_ADDRESS = exports.createInterpreterAdapter = void 0;
5
5
  var adapter_ts_1 = require("./adapter.js");
6
6
  Object.defineProperty(exports, "createInterpreterAdapter", { enumerable: true, get: function () { return adapter_ts_1.createInterpreterAdapter; } });
7
- Object.defineProperty(exports, "ORACLE_DEFAULTS", { enumerable: true, get: function () { return adapter_ts_1.ORACLE_DEFAULTS; } });
8
7
  Object.defineProperty(exports, "PLACEHOLDER_INTERPRETER_ADDRESS", { enumerable: true, get: function () { return adapter_ts_1.PLACEHOLDER_INTERPRETER_ADDRESS; } });