@crediolabs/policy-synth 0.1.4 → 0.1.5

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 (128) hide show
  1. package/LICENSE +21 -0
  2. package/dist/codegen/template.js +3 -3
  3. package/dist/record/decode.d.ts +6 -3
  4. package/dist/record/decode.js +13 -3
  5. package/dist/run/index.d.ts +31 -0
  6. package/dist/run/index.js +164 -0
  7. package/dist/run/schemas.d.ts +2003 -0
  8. package/dist/run/schemas.js +215 -0
  9. package/dist/synth/deny-cases.js +18 -2
  10. package/dist/synth/evaluate.js +2 -2
  11. package/dist/synth/synthesize-from-recording.d.ts +5 -2
  12. package/dist/synth/synthesize-from-recording.js +121 -63
  13. package/dist/types.d.ts +17 -0
  14. package/dist/types.js +17 -0
  15. package/dist/verify/simulate.js +18 -2
  16. package/dist/verify/verify.js +18 -2
  17. package/dist-cjs/adapters/interpreter/adapter.d.ts +38 -0
  18. package/dist-cjs/adapters/interpreter/adapter.js +527 -0
  19. package/dist-cjs/adapters/interpreter/index.d.ts +1 -0
  20. package/dist-cjs/adapters/interpreter/index.js +8 -0
  21. package/dist-cjs/adapters/oz/adapter.d.ts +20 -0
  22. package/dist-cjs/adapters/oz/adapter.js +289 -0
  23. package/dist-cjs/adapters/oz/index.d.ts +1 -0
  24. package/dist-cjs/adapters/oz/index.js +8 -0
  25. package/dist-cjs/codegen/compile-gate.d.ts +33 -0
  26. package/dist-cjs/codegen/compile-gate.js +123 -0
  27. package/dist-cjs/codegen/index.d.ts +2 -0
  28. package/dist-cjs/codegen/index.js +14 -0
  29. package/dist-cjs/codegen/template.d.ts +18 -0
  30. package/dist-cjs/codegen/template.js +134 -0
  31. package/dist-cjs/errors.d.ts +37 -0
  32. package/dist-cjs/errors.js +3 -0
  33. package/dist-cjs/index.d.ts +11 -0
  34. package/dist-cjs/index.js +27 -0
  35. package/dist-cjs/ir/index.d.ts +1 -0
  36. package/dist-cjs/ir/index.js +3 -0
  37. package/dist-cjs/ir/types.d.ts +108 -0
  38. package/dist-cjs/ir/types.js +12 -0
  39. package/dist-cjs/mandate/index.d.ts +2 -0
  40. package/dist-cjs/mandate/index.js +6 -0
  41. package/dist-cjs/mandate/to-ir.d.ts +3 -0
  42. package/dist-cjs/mandate/to-ir.js +63 -0
  43. package/dist-cjs/mandate/types.d.ts +20 -0
  44. package/dist-cjs/mandate/types.js +9 -0
  45. package/dist-cjs/package.json +3 -0
  46. package/dist-cjs/predicate/encode.d.ts +10 -0
  47. package/dist-cjs/predicate/encode.js +252 -0
  48. package/dist-cjs/predicate/index.d.ts +1 -0
  49. package/dist-cjs/predicate/index.js +6 -0
  50. package/dist-cjs/record/decode.d.ts +79 -0
  51. package/dist-cjs/record/decode.js +398 -0
  52. package/dist-cjs/record/freshness.d.ts +17 -0
  53. package/dist-cjs/record/freshness.js +55 -0
  54. package/dist-cjs/record/index.d.ts +21 -0
  55. package/dist-cjs/record/index.js +166 -0
  56. package/dist-cjs/record/movements.d.ts +20 -0
  57. package/dist-cjs/record/movements.js +192 -0
  58. package/dist-cjs/record/rpc.d.ts +22 -0
  59. package/dist-cjs/record/rpc.js +74 -0
  60. package/dist-cjs/record/validate.d.ts +22 -0
  61. package/dist-cjs/record/validate.js +63 -0
  62. package/dist-cjs/registry/identify.d.ts +11 -0
  63. package/dist-cjs/registry/identify.js +87 -0
  64. package/dist-cjs/registry/index.d.ts +3 -0
  65. package/dist-cjs/registry/index.js +15 -0
  66. package/dist-cjs/registry/known-addresses.d.ts +16 -0
  67. package/dist-cjs/registry/known-addresses.js +53 -0
  68. package/dist-cjs/registry/protocols.d.ts +38 -0
  69. package/dist-cjs/registry/protocols.js +153 -0
  70. package/dist-cjs/review-card/builder.d.ts +14 -0
  71. package/dist-cjs/review-card/builder.js +264 -0
  72. package/dist-cjs/review-card/conflict.d.ts +40 -0
  73. package/dist-cjs/review-card/conflict.js +114 -0
  74. package/dist-cjs/review-card/cross-check.d.ts +11 -0
  75. package/dist-cjs/review-card/cross-check.js +151 -0
  76. package/dist-cjs/review-card/index.d.ts +3 -0
  77. package/dist-cjs/review-card/index.js +10 -0
  78. package/dist-cjs/run/index.d.ts +31 -0
  79. package/dist-cjs/run/index.js +178 -0
  80. package/dist-cjs/run/schemas.d.ts +2003 -0
  81. package/dist-cjs/run/schemas.js +218 -0
  82. package/dist-cjs/seams/index.d.ts +1 -0
  83. package/dist-cjs/seams/index.js +3 -0
  84. package/dist-cjs/seams/types.d.ts +66 -0
  85. package/dist-cjs/seams/types.js +12 -0
  86. package/dist-cjs/synth/compose-from-recording.d.ts +64 -0
  87. package/dist-cjs/synth/compose-from-recording.js +356 -0
  88. package/dist-cjs/synth/deny-cases.d.ts +12 -0
  89. package/dist-cjs/synth/deny-cases.js +380 -0
  90. package/dist-cjs/synth/evaluate.d.ts +39 -0
  91. package/dist-cjs/synth/evaluate.js +428 -0
  92. package/dist-cjs/synth/harness.d.ts +16 -0
  93. package/dist-cjs/synth/harness.js +29 -0
  94. package/dist-cjs/synth/index.d.ts +9 -0
  95. package/dist-cjs/synth/index.js +23 -0
  96. package/dist-cjs/synth/lower.d.ts +23 -0
  97. package/dist-cjs/synth/lower.js +119 -0
  98. package/dist-cjs/synth/minimize.d.ts +4 -0
  99. package/dist-cjs/synth/minimize.js +41 -0
  100. package/dist-cjs/synth/predicate-literals.d.ts +5 -0
  101. package/dist-cjs/synth/predicate-literals.js +28 -0
  102. package/dist-cjs/synth/scope.d.ts +26 -0
  103. package/dist-cjs/synth/scope.js +81 -0
  104. package/dist-cjs/synth/synthesize-from-mandate.d.ts +5 -0
  105. package/dist-cjs/synth/synthesize-from-mandate.js +37 -0
  106. package/dist-cjs/synth/synthesize-from-recording.d.ts +49 -0
  107. package/dist-cjs/synth/synthesize-from-recording.js +711 -0
  108. package/dist-cjs/types.d.ts +288 -0
  109. package/dist-cjs/types.js +55 -0
  110. package/dist-cjs/verify/envelope.d.ts +15 -0
  111. package/dist-cjs/verify/envelope.js +23 -0
  112. package/dist-cjs/verify/index.d.ts +3 -0
  113. package/dist-cjs/verify/index.js +8 -0
  114. package/dist-cjs/verify/simulate.d.ts +31 -0
  115. package/dist-cjs/verify/simulate.js +261 -0
  116. package/dist-cjs/verify/verify.d.ts +21 -0
  117. package/dist-cjs/verify/verify.js +192 -0
  118. package/package.json +43 -3
  119. package/src/codegen/template.ts +3 -3
  120. package/src/record/decode.ts +21 -10
  121. package/src/run/index.ts +213 -0
  122. package/src/run/schemas.ts +242 -0
  123. package/src/synth/deny-cases.ts +21 -2
  124. package/src/synth/evaluate.ts +2 -2
  125. package/src/synth/synthesize-from-recording.ts +141 -62
  126. package/src/types.ts +19 -0
  127. package/src/verify/simulate.ts +21 -2
  128. package/src/verify/verify.ts +21 -2
@@ -28,6 +28,7 @@
28
28
  // suite to stay in lockstep.
29
29
  import { generateCases } from "../synth/deny-cases.js";
30
30
  import { evaluate } from "../synth/evaluate.js";
31
+ import { MAX_SCVAL_CLONE_DEPTH } from "../types.js";
31
32
  const SIMULATOR_VERSION = 'ts-model-1.0.0';
32
33
  /** Replay a recorded transaction against a proposed predicate and emit the
33
34
  * `SimulationResult` envelope. The simulator returns the SAME permit
@@ -177,12 +178,27 @@ function buildPermitContext(predicate, tx, topLevel, opts) {
177
178
  }
178
179
  return ctx;
179
180
  }
180
- function cloneScVal(value) {
181
+ function cloneScVal(value, depth = 0) {
182
+ // Recursion is bounded by MAX_SCVAL_CLONE_DEPTH so a hand-crafted nested-vec
183
+ // payload cannot RangeError the JS stack during context building. Over-depth
184
+ // throws a ToolError-shaped error that the simulator's existing try/catch
185
+ // converts to a structured `{ok:false, error}` (not a thrown RangeError).
181
186
  if (value.type === 'vec') {
182
- return { type: 'vec', value: value.value.map(cloneScVal) };
187
+ if (depth >= MAX_SCVAL_CLONE_DEPTH) {
188
+ throw cloneDepthError(value);
189
+ }
190
+ return { type: 'vec', value: value.value.map((v) => cloneScVal(v, depth + 1)) };
183
191
  }
184
192
  return { ...value };
185
193
  }
194
+ function cloneDepthError(value) {
195
+ const err = new Error(`ScVal clone depth exceeds MAX_SCVAL_CLONE_DEPTH (${MAX_SCVAL_CLONE_DEPTH})`);
196
+ err.code = 'SIMULATION_ERROR';
197
+ err.severity = 'error';
198
+ err.retryable = false;
199
+ err.depthContext = value.type;
200
+ throw err;
201
+ }
186
202
  function visitOracleLeaves(node, visit) {
187
203
  switch (node.op) {
188
204
  case 'and':
@@ -28,6 +28,7 @@
28
28
  //
29
29
  // Determinism: same `(predicate, permitTx)` -> byte-identical verdict.
30
30
  import { minimize } from "../synth/minimize.js";
31
+ import { MAX_SCVAL_CLONE_DEPTH } from "../types.js";
31
32
  /** Run the static minimality check on a proposed predicate + recorded tx.
32
33
  * Returns `{ ok: true }` when the predicate is minimal (every top-level
33
34
  * conjunct carries load) and `VERIFICATION_FAILED` when a conjunct could
@@ -125,12 +126,27 @@ function buildPermitContextForVerify(tx, topLevel, opts) {
125
126
  }
126
127
  return ctx;
127
128
  }
128
- function cloneScVal(value) {
129
+ function cloneScVal(value, depth = 0) {
130
+ // Recursion is bounded by MAX_SCVAL_CLONE_DEPTH so a hand-crafted nested-vec
131
+ // payload cannot RangeError the JS stack during context building. Over-depth
132
+ // throws a ToolError-shaped error that the verifier's existing try/catch
133
+ // converts to a structured `{ok:false, error}` (not a thrown RangeError).
129
134
  if (value.type === 'vec') {
130
- return { type: 'vec', value: value.value.map(cloneScVal) };
135
+ if (depth >= MAX_SCVAL_CLONE_DEPTH) {
136
+ throw cloneDepthError(value);
137
+ }
138
+ return { type: 'vec', value: value.value.map((v) => cloneScVal(v, depth + 1)) };
131
139
  }
132
140
  return { ...value };
133
141
  }
142
+ function cloneDepthError(value) {
143
+ const err = new Error(`ScVal clone depth exceeds MAX_SCVAL_CLONE_DEPTH (${MAX_SCVAL_CLONE_DEPTH})`);
144
+ err.code = 'VERIFICATION_FAILED';
145
+ err.severity = 'error';
146
+ err.retryable = false;
147
+ err.depthContext = value.type;
148
+ throw err;
149
+ }
134
150
  function fingerprint(node) {
135
151
  return JSON.stringify(node, replacer);
136
152
  }
@@ -0,0 +1,38 @@
1
+ import type { IRPolicyRule } from '../../ir/types.ts';
2
+ import type { CustodyAdapter } from '../../seams/types.ts';
3
+ import type { Network, PredicateNode } from '../../types.ts';
4
+ /** [VERIFY] NOT a real deployed address. The interpreter is a per-network
5
+ * deploy artifact we do not have yet; install is a later phase. */
6
+ export declare const PLACEHOLDER_INTERPRETER_ADDRESS = "VERIFY-interpreter-address";
7
+ /** Wasm-level oracle defaults (mirrors INTERPRETER_INSTALL_PARAMS.md OracleParams).
8
+ * Per-policy overrides may TIGHTEN only - they may never exceed these. */
9
+ export declare const ORACLE_DEFAULTS: {
10
+ readonly maxStalenessSeconds: 600;
11
+ readonly maxDeviationBps: 200;
12
+ };
13
+ export interface InterpreterAdapterConfig {
14
+ network: Network;
15
+ /** Per-rule install nonce (first install = 1; replay-protected). */
16
+ installNonce: number;
17
+ /** The smart account this interpreter policy will be installed against. Used
18
+ * by the self-call gate: any `in`-allowlist containing this address is
19
+ * rejected as `SCOPE_SELF_CALL`. */
20
+ smartAccountAddress: string;
21
+ /** Optional per-policy oracle overrides. Must TIGHTEN vs ORACLE_DEFAULTS
22
+ * (i.e. <= each default). A widening value throws ORACLE_PARAMS_OUT_OF_RANGE
23
+ * at compile time. */
24
+ oracleParams?: {
25
+ maxStalenessSeconds?: number;
26
+ maxDeviationBps?: number;
27
+ };
28
+ }
29
+ export declare function createInterpreterAdapter(config: InterpreterAdapterConfig): CustodyAdapter;
30
+ /** Lower a single IR rule to the canonical pre-encoding `PredicateNode`. The
31
+ * orchestrator uses this to wire the self-verify + minimise pipeline: after
32
+ * `compile(ir)` succeeds (`covered === true`, `proposed` set), the
33
+ * orchestrator re-derives the PredicateNode via this helper to drive
34
+ * `minimize` and `runHarness` on the SAME shape the encoder saw. Pure and
35
+ * deterministic: same `rule + config` -> byte-identical PredicateNode. The
36
+ * `uncovered` list the adapter surfaces during compile is NOT re-derived
37
+ * here; callers that need it should use the result of `compile(ir)`. */
38
+ export declare function lowerRuleToPredicate(rule: IRPolicyRule, config: InterpreterAdapterConfig): PredicateNode;
@@ -0,0 +1,527 @@
1
+ "use strict";
2
+ // src/adapters/interpreter/adapter.ts - the interpreter-policy CustodyAdapter.
3
+ //
4
+ // Compiles a PolicyIR to a single interpreter `PolicyDocument` + `PolicyRef`
5
+ // carrying the canonical predicate encoding defined in
6
+ // `packages/policy-interpreter/INTERPRETER_INSTALL_PARAMS.md`. The adapter is
7
+ // the second backend (the OZ built-in adapter is the first); together they
8
+ // cover the canonical language subset. The compose step (P3) routes IR
9
+ // constructs between them.
10
+ //
11
+ // What it lowers (every IR construct the predicate DSL expresses):
12
+ // - IRLogic and/or -> PredicateNode and/or
13
+ // - IRLogic not -> PredicateNode not
14
+ // - IRCompare -> selector(left) vs literal(right), op carried over
15
+ // - IR `in` -> needle=selector, haystack=literals (PURESET
16
+ // membership; the haystack is always sorted by the
17
+ // encoder)
18
+ // - IR `eq_seq` -> eq(selectorLeaf, literal_vec([...])) - exact ordered
19
+ // sequence equality; element order is preserved
20
+ // verbatim (the encoder does NOT sort the vec)
21
+ // - IRSelector -> matching PredicateLeaf (see lowerSelector)
22
+ // - scope.contract -> sibling `call_contract == <contract>` (always
23
+ // emitted when set)
24
+ // - scope.method -> sibling `call_fn == <method>`
25
+ //
26
+ // Three fail-closed enforcement gates (per spec):
27
+ // - oracle_price leaves MUST sit directly under the top-level `and`; nesting
28
+ // under `not` / `or` / inside `in` throws ORACLE_LEAF_INVALID_POSITION.
29
+ // - oracleParams overrides may only TIGHTEN vs wasm defaults
30
+ // (maxStalenessSeconds <= 600, maxDeviationBps <= 200); a widening value
31
+ // throws ORACLE_PARAMS_OUT_OF_RANGE at compile time.
32
+ // - an `in` allowlist (or a `compare eq` vs an address, or any value in an
33
+ // `eq_seq`) that targets the smart account's own address throws
34
+ // SCOPE_SELF_CALL.
35
+ //
36
+ // Anything the adapter genuinely cannot express (EVM calldata / value, unix
37
+ // expiry, etc.) is named in `uncovered` rather than silently dropped.
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.ORACLE_DEFAULTS = exports.PLACEHOLDER_INTERPRETER_ADDRESS = void 0;
40
+ exports.createInterpreterAdapter = createInterpreterAdapter;
41
+ exports.lowerRuleToPredicate = lowerRuleToPredicate;
42
+ const encode_ts_1 = require("../../predicate/encode.js");
43
+ /** [VERIFY] NOT a real deployed address. The interpreter is a per-network
44
+ * deploy artifact we do not have yet; install is a later phase. */
45
+ exports.PLACEHOLDER_INTERPRETER_ADDRESS = 'VERIFY-interpreter-address';
46
+ /** Wasm-level oracle defaults (mirrors INTERPRETER_INSTALL_PARAMS.md OracleParams).
47
+ * Per-policy overrides may TIGHTEN only - they may never exceed these. */
48
+ exports.ORACLE_DEFAULTS = {
49
+ maxStalenessSeconds: 600,
50
+ maxDeviationBps: 200,
51
+ };
52
+ const CAPABILITIES = {
53
+ supportsSpendWindow: true,
54
+ supportsThreshold: false, // thresholds are the OZ adapter's job
55
+ supportsTimeExpiry: true, // via valid_until ledger expiry
56
+ supportsOraclePrice: true,
57
+ supportsInvocationCount: true,
58
+ supportsGeneralPredicate: true,
59
+ };
60
+ /** Parse confidence for a deterministic (non-decoded) input: full, with an
61
+ * empty unknown/opaque breakdown. A mandate needs no decoding, so the gate is
62
+ * not applicable and confidence is 1. */
63
+ const FULL_PARSE_CONFIDENCE = {
64
+ overall: 1,
65
+ knownContracts: [],
66
+ unknownContracts: [],
67
+ opaqueScVals: [],
68
+ thresholdUsed: 1,
69
+ };
70
+ function createInterpreterAdapter(config) {
71
+ return {
72
+ name: 'interpreter',
73
+ mode: 'enforce',
74
+ capabilities: () => ({ ...CAPABILITIES }),
75
+ compile: (ir) => compile(ir, config),
76
+ simulate: () => simulateStub(),
77
+ export: (ir) => canonicalStringify(ir),
78
+ };
79
+ }
80
+ /** Lower a single IR rule to the canonical pre-encoding `PredicateNode`. The
81
+ * orchestrator uses this to wire the self-verify + minimise pipeline: after
82
+ * `compile(ir)` succeeds (`covered === true`, `proposed` set), the
83
+ * orchestrator re-derives the PredicateNode via this helper to drive
84
+ * `minimize` and `runHarness` on the SAME shape the encoder saw. Pure and
85
+ * deterministic: same `rule + config` -> byte-identical PredicateNode. The
86
+ * `uncovered` list the adapter surfaces during compile is NOT re-derived
87
+ * here; callers that need it should use the result of `compile(ir)`. */
88
+ function lowerRuleToPredicate(rule, config) {
89
+ return lowerRule(rule, config).predicate;
90
+ }
91
+ function compile(ir, config) {
92
+ // Tighten-only oracle-params gate (config-time). Widening -> throw at compile
93
+ // time before any lowering happens, so a misuse fails closed loudly.
94
+ assertOracleParamsTighten(config.oracleParams);
95
+ const firstRule = ir.rules[0];
96
+ if (!firstRule) {
97
+ return { covered: false, uncovered: ['empty PolicyIR (no rules to compile)'] };
98
+ }
99
+ if (ir.rules.length > 1) {
100
+ return {
101
+ covered: false,
102
+ uncovered: [
103
+ `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)`,
104
+ ],
105
+ };
106
+ }
107
+ const lowered = lowerRule(firstRule, config);
108
+ if (lowered.uncovered.length > 0) {
109
+ return { covered: false, uncovered: lowered.uncovered };
110
+ }
111
+ const { encodedPredicate, predicateHash } = (0, encode_ts_1.encodePredicate)(lowered.predicate);
112
+ const policyDocument = {
113
+ grammarVersion: 1,
114
+ installNonce: config.installNonce,
115
+ encodedPredicate,
116
+ predicateHash,
117
+ ...(config.oracleParams ? { oracleParams: config.oracleParams } : {}),
118
+ };
119
+ const policyRef = {
120
+ kind: 'interpreter',
121
+ interpreterAddress: exports.PLACEHOLDER_INTERPRETER_ADDRESS,
122
+ predicateBlobBase64: encodedPredicate,
123
+ };
124
+ const proposed = {
125
+ contextRule: lowered.contextRule,
126
+ policyDocuments: [policyDocument],
127
+ policyRefs: [policyRef],
128
+ parseConfidence: { ...FULL_PARSE_CONFIDENCE },
129
+ warnings: [],
130
+ ambiguities: [],
131
+ };
132
+ return { covered: true, uncovered: [], proposed };
133
+ }
134
+ function lowerRule(rule, config) {
135
+ const uncovered = [];
136
+ // scope -> context rule + sibling predicates. contract/method each become
137
+ // their own `eq` leaf and are merged into the top-level and alongside the
138
+ // constraints. This keeps the top-level shape `and` (mandatory per the oracle
139
+ // position rule and the canonical hash stability for `and`).
140
+ const scopeContract = rule.scope.contract;
141
+ const scopeMethod = rule.scope.method;
142
+ if (rule.scope.chainId !== undefined) {
143
+ uncovered.push(`chainId \`${rule.scope.chainId}\` not bindable by the interpreter adapter (network is per-context, not per-rule)`);
144
+ }
145
+ if (rule.roles.length > 0) {
146
+ uncovered.push(`roles [${rule.roles.join(', ')}] dropped (role-to-signer mapping is a later phase; interpreter policies reference addresses, not role names)`);
147
+ }
148
+ if (rule.guard) {
149
+ uncovered.push(`guard: ${describeCondition(rule.guard)} (interpreter adapter currently treats guards as uncovered)`);
150
+ }
151
+ // expiry -> context rule validUntilLedger only. Unix-timestamp expiry is
152
+ // honoured by the interpreter (the `valid_until` selector), but we surface
153
+ // it here as Path-B because the IR contract already says "OZ context rules
154
+ // expire by ledger sequence" and we keep one expiry model for both adapters.
155
+ let validUntilLedger = null;
156
+ if (rule.expiry) {
157
+ if (rule.expiry.validUntilLedger !== undefined) {
158
+ validUntilLedger = rule.expiry.validUntilLedger;
159
+ }
160
+ else if (rule.expiry.validUntilUnixSeconds !== undefined) {
161
+ uncovered.push('time expiry given as a unix timestamp (interpreter adapter currently lowers expiry only via the OZ context-rule validUntilLedger; supply expiry.validUntilLedger)');
162
+ }
163
+ }
164
+ // approval is the OZ adapter's job (it lowers to simple_threshold /
165
+ // weighted_threshold primitives). Surface it here so a caller that drops it
166
+ // through to the interpreter adapter sees an explicit uncovered entry
167
+ // instead of a silently-dropped M-of-N gate.
168
+ if (rule.approval) {
169
+ uncovered.push(`approval threshold ${rule.approval.threshold} not emitted by the interpreter adapter (thresholds lower to OZ built-in primitives in the OZ adapter)`);
170
+ }
171
+ // Pre-scan constraints: anything the interpreter adapter cannot express is
172
+ // named in `uncovered` and skipped from the predicate lowering. We surface
173
+ // these BEFORE lowering so the predicate is built from only the
174
+ // expressible subset.
175
+ const expressibleConstraints = [];
176
+ for (const c of rule.constraints) {
177
+ const unsupp = unsupportedConstruct(c);
178
+ if (unsupp !== null) {
179
+ uncovered.push(unsupp);
180
+ }
181
+ else {
182
+ expressibleConstraints.push(c);
183
+ }
184
+ }
185
+ // Build the top-level `and` of scope + expressible constraints. The
186
+ // top-level MUST be `and` so oracle leaves sit directly under it - the
187
+ // position rule is enforced inside `lowerCondition`.
188
+ const topChildren = [];
189
+ if (scopeContract !== undefined) {
190
+ topChildren.push({
191
+ op: 'eq',
192
+ left: { kind: 'call_contract' },
193
+ right: { kind: 'literal_address', value: scopeContract },
194
+ });
195
+ }
196
+ if (scopeMethod !== undefined) {
197
+ topChildren.push({
198
+ op: 'eq',
199
+ left: { kind: 'call_fn' },
200
+ right: { kind: 'literal_symbol', value: scopeMethod },
201
+ });
202
+ }
203
+ for (const c of expressibleConstraints) {
204
+ topChildren.push(lowerCondition(c, config));
205
+ }
206
+ const predicate = topChildren.length === 1 && topChildren[0] !== undefined
207
+ ? topChildren[0]
208
+ : { op: 'and', children: topChildren };
209
+ const contextRule = {
210
+ contextRuleType: { kind: 'default' },
211
+ name: 'interpreter',
212
+ validUntilLedger,
213
+ signers: [],
214
+ policies: [],
215
+ };
216
+ return { predicate, contextRule, uncovered };
217
+ }
218
+ /** Detect IR constructs the interpreter adapter cannot express; return a
219
+ * human-readable descriptor (or null if the construct IS expressible). Used
220
+ * by `lowerRule` to populate `uncovered` before lowering. */
221
+ function unsupportedConstruct(cond) {
222
+ switch (cond.op) {
223
+ case 'in':
224
+ return cond.selector.kind === 'calldata' || cond.selector.kind === 'value'
225
+ ? `EVM \`${cond.selector.kind}\` selector on allowlist (predicate DSL)`
226
+ : null;
227
+ case 'eq_seq':
228
+ return cond.selector.kind === 'calldata' || cond.selector.kind === 'value'
229
+ ? `EVM \`${cond.selector.kind}\` selector on ordered-sequence equality (predicate DSL)`
230
+ : null;
231
+ case 'compare': {
232
+ const s = cond.compare.selector;
233
+ if (s.kind === 'calldata')
234
+ return 'EVM calldata comparison (predicate DSL)';
235
+ if (s.kind === 'value')
236
+ return 'tx.value comparison (predicate DSL)';
237
+ return null;
238
+ }
239
+ case 'and':
240
+ case 'or':
241
+ case 'not':
242
+ return null;
243
+ }
244
+ }
245
+ /** Lower one IR condition to a PredicateNode. Enforces the oracle position
246
+ * rule (oracle_price leaves must sit directly under the top-level `and`) and
247
+ * the self-call rule (`in` allowlist / `compare eq` vs the smart account
248
+ * address -> SCOPE_SELF_CALL). */
249
+ function lowerCondition(cond, config) {
250
+ switch (cond.op) {
251
+ case 'and':
252
+ return { op: 'and', children: cond.children.map((c) => lowerCondition(c, config)) };
253
+ case 'or':
254
+ // `or` at this depth: oracle_price anywhere inside is invalid (would
255
+ // not be a direct child of the top-level `and`).
256
+ assertNoOracleDescendants(cond);
257
+ return { op: 'or', children: cond.children.map((c) => lowerCondition(c, config)) };
258
+ case 'not':
259
+ // `not` at this depth: oracle_price anywhere inside is invalid.
260
+ assertNoOracleDescendants(cond);
261
+ return { op: 'not', child: lowerCondition(cond.child, config) };
262
+ case 'in': {
263
+ assertNoOracleDescendants(cond);
264
+ for (const v of cond.values) {
265
+ assertNotSelfCallAddress(v, config);
266
+ }
267
+ // `in` is PURE set membership; the encoder sorts the haystack. An exact
268
+ // ordered sequence (e.g. swap hop path) is expressed as `eq_seq` and
269
+ // lowers to `eq(selector, literal_vec)` instead - never to `in` with an
270
+ // `ordered` flag.
271
+ return {
272
+ op: 'in',
273
+ needle: lowerSelector(cond.selector),
274
+ haystack: cond.values.map((v) => literalFromScalar(v, selectorScalarType(cond.selector))),
275
+ };
276
+ }
277
+ case 'eq_seq': {
278
+ assertNoOracleDescendants(cond);
279
+ for (const v of cond.values) {
280
+ assertNotSelfCallAddress(v, config);
281
+ }
282
+ // Exact ordered sequence equality: the right-hand side is a literal_vec
283
+ // whose element order is preserved verbatim by the encoder. `eq` does
284
+ // deep equality at evaluate time - this is the ONLY way to express an
285
+ // exact ordered sequence in the predicate grammar (the path of a swap).
286
+ return {
287
+ op: 'eq',
288
+ left: lowerSelector(cond.selector),
289
+ right: {
290
+ kind: 'literal_vec',
291
+ elements: cond.values.map((v) => literalFromScalar(v, selectorScalarType(cond.selector))),
292
+ },
293
+ };
294
+ }
295
+ case 'compare': {
296
+ // Self-call on an address eq.
297
+ if (cond.compare.operator === 'eq' &&
298
+ cond.compare.selector.kind === 'arg' &&
299
+ cond.compare.selector.scalarType === 'address') {
300
+ assertNotSelfCallAddress(cond.compare.value, config);
301
+ }
302
+ return {
303
+ op: cond.compare.operator,
304
+ left: lowerSelector(cond.compare.selector),
305
+ right: literalFromIRCompare(cond.compare),
306
+ };
307
+ }
308
+ }
309
+ }
310
+ /** Lower an IR selector to the matching PredicateLeaf. */
311
+ function lowerSelector(s) {
312
+ switch (s.kind) {
313
+ case 'arg':
314
+ return { kind: 'call_arg', index: s.argIndex };
315
+ case 'amount':
316
+ return { kind: 'amount', token: s.token };
317
+ case 'window_spent':
318
+ return { kind: 'window_spent', token: s.token, windowSeconds: s.windowSeconds };
319
+ case 'invocation_count':
320
+ return { kind: 'invocation_count_in_window', windowSecs: s.windowSeconds };
321
+ case 'now':
322
+ return { kind: 'now' };
323
+ case 'valid_until':
324
+ return { kind: 'valid_until' };
325
+ case 'oracle_price':
326
+ return { kind: 'oracle_price', asset: s.asset };
327
+ case 'calldata':
328
+ case 'value':
329
+ // Unreachable: the caller flagged these as Path-B before reaching here.
330
+ throw toolError('SYNTHESIS_ERROR', `selector kind \`${s.kind}\` is not lowerable to a predicate leaf`);
331
+ }
332
+ }
333
+ /** Build a literal leaf from the right-hand side of an IRCompare, mapping the
334
+ * selector kind to the matching `literal_*` kind. The IR compare value is a
335
+ * raw string (i128-safe); the selector kind fixes the canonical wire type:
336
+ * - arg -> IR scalarType (set by the recorder/parser)
337
+ * - amount -> i128 (canonical Stellar token amount encoding)
338
+ * - window_spent -> i128 (canonical amount encoding)
339
+ * - oracle_price -> i128 (canonical price encoding)
340
+ * - invocation_count -> u32 (counts are small non-negative integers)
341
+ * - now / valid_until -> u64 (unix timestamps in seconds) */
342
+ function literalFromIRCompare(c) {
343
+ const scalarType = c.selector.kind === 'arg' ? c.selector.scalarType : literalScalarForSelector(c.selector.kind);
344
+ return literalFromScalar(c.value, scalarType);
345
+ }
346
+ function literalScalarForSelector(kind) {
347
+ switch (kind) {
348
+ case 'amount':
349
+ case 'window_spent':
350
+ case 'oracle_price':
351
+ return 'i128';
352
+ case 'invocation_count':
353
+ return 'u32';
354
+ case 'now':
355
+ case 'valid_until':
356
+ return 'u64';
357
+ case 'arg':
358
+ case 'calldata':
359
+ case 'value':
360
+ // arg -> caller handles scalarType; calldata/value -> unreachable (Path-B).
361
+ return 'i128';
362
+ }
363
+ }
364
+ /** Build a literal leaf from a raw string value + an IRScalarType hint. */
365
+ function literalFromScalar(value, scalarType) {
366
+ switch (scalarType) {
367
+ case 'address':
368
+ return { kind: 'literal_address', value };
369
+ case 'i128':
370
+ case 'u128':
371
+ return { kind: 'literal_i128', value };
372
+ case 'u32':
373
+ return { kind: 'literal_u32', value: Number.parseInt(value, 10) };
374
+ case 'u64':
375
+ case 'i64':
376
+ return { kind: 'literal_u64', value };
377
+ case 'symbol':
378
+ return { kind: 'literal_symbol', value };
379
+ case 'bytes':
380
+ return { kind: 'literal_bytes', value };
381
+ case 'bool':
382
+ throw toolError('MALFORMED_PREDICATE', `boolean literal not supported in v1 predicate grammar`);
383
+ }
384
+ }
385
+ /** Scalar type of an IRSelector for the purpose of building literal leaves
386
+ * (the right-hand side of `eq` / elements of an `in` haystack / elements of
387
+ * a `literal_vec`). Mirrors `literalScalarForSelector` for OZ extensions and
388
+ * uses the selector's own `scalarType` for `arg` selectors. */
389
+ function selectorScalarType(selector) {
390
+ if (selector.kind === 'arg')
391
+ return selector.scalarType;
392
+ return literalScalarForSelector(selector.kind);
393
+ }
394
+ /** Walk a condition tree and throw if any oracle_price leaf is found anywhere
395
+ * inside. Used by `lowerCondition` for `not`, `or`, and `in` - the three
396
+ * positions where oracle leaves would not be direct children of the
397
+ * top-level `and`. */
398
+ function assertNoOracleDescendants(node) {
399
+ if (containsOracle(node)) {
400
+ throw toolError('ORACLE_LEAF_INVALID_POSITION', 'oracle_price leaves must sit directly under the top-level `and`');
401
+ }
402
+ }
403
+ function containsOracle(node) {
404
+ switch (node.op) {
405
+ case 'compare':
406
+ return node.compare.selector.kind === 'oracle_price';
407
+ case 'in':
408
+ return node.selector.kind === 'oracle_price';
409
+ case 'eq_seq':
410
+ return node.selector.kind === 'oracle_price';
411
+ case 'not':
412
+ return containsOracle(node.child);
413
+ case 'and':
414
+ case 'or':
415
+ return node.children.some(containsOracle);
416
+ }
417
+ }
418
+ /** Reject a value that targets the smart account's own address (a self-call
419
+ * is a structural privilege-escalation hole the interpreter forbids). */
420
+ function assertNotSelfCallAddress(value, config) {
421
+ if (value === config.smartAccountAddress) {
422
+ throw toolError('SCOPE_SELF_CALL', `value \`${value}\` is the smart account's own address (self-call in an allowlist / compare is rejected)`);
423
+ }
424
+ }
425
+ /** Tighten-only oracle-params gate. Any widening value (above the wasm-level
426
+ * defaults) throws ORACLE_PARAMS_OUT_OF_RANGE at compile time. */
427
+ function assertOracleParamsTighten(params) {
428
+ if (!params)
429
+ return;
430
+ if (params.maxStalenessSeconds !== undefined &&
431
+ params.maxStalenessSeconds > exports.ORACLE_DEFAULTS.maxStalenessSeconds) {
432
+ throw toolError('ORACLE_PARAMS_OUT_OF_RANGE', `maxStalenessSeconds ${params.maxStalenessSeconds} exceeds default ${exports.ORACLE_DEFAULTS.maxStalenessSeconds}`);
433
+ }
434
+ if (params.maxDeviationBps !== undefined &&
435
+ params.maxDeviationBps > exports.ORACLE_DEFAULTS.maxDeviationBps) {
436
+ throw toolError('ORACLE_PARAMS_OUT_OF_RANGE', `maxDeviationBps ${params.maxDeviationBps} exceeds default ${exports.ORACLE_DEFAULTS.maxDeviationBps}`);
437
+ }
438
+ }
439
+ /** Human-readable descriptor for a construct the interpreter adapter cannot
440
+ * express. Mirrors the OZ adapter's `describeCondition` for parity. */
441
+ function describeCondition(cond) {
442
+ switch (cond.op) {
443
+ case 'in':
444
+ return `value allowlist on ${describeSelector(cond.selector)} (predicate DSL)`;
445
+ case 'eq_seq':
446
+ return `exact ordered sequence on ${describeSelector(cond.selector)} (predicate DSL)`;
447
+ case 'not':
448
+ return 'negated condition (predicate DSL)';
449
+ case 'and':
450
+ case 'or':
451
+ return `nested ${cond.op} condition (predicate DSL)`;
452
+ case 'compare': {
453
+ const s = cond.compare.selector;
454
+ switch (s.kind) {
455
+ case 'oracle_price':
456
+ return `oracle price condition on ${s.asset}`;
457
+ case 'invocation_count':
458
+ return `invocation-count window (${s.windowSeconds}s) condition`;
459
+ case 'window_spent':
460
+ return `spend-window comparison with operator '${cond.compare.operator}'`;
461
+ case 'amount':
462
+ return `per-call amount comparison on ${s.token}`;
463
+ case 'arg':
464
+ return `argument comparison on arg ${s.argIndex}`;
465
+ case 'calldata':
466
+ return 'EVM calldata comparison';
467
+ case 'value':
468
+ return 'tx.value comparison';
469
+ case 'now':
470
+ case 'valid_until':
471
+ return 'time comparison';
472
+ }
473
+ }
474
+ }
475
+ }
476
+ function describeSelector(s) {
477
+ switch (s.kind) {
478
+ case 'arg':
479
+ return `arg ${s.argIndex}`;
480
+ case 'amount':
481
+ return `amount(${s.token})`;
482
+ case 'window_spent':
483
+ return `window_spent(${s.token})`;
484
+ case 'oracle_price':
485
+ return `oracle_price(${s.asset})`;
486
+ case 'invocation_count':
487
+ return `invocation_count(${s.windowSeconds}s)`;
488
+ case 'calldata':
489
+ return `calldata[${s.offset}:${s.offset + s.length}]`;
490
+ default:
491
+ return s.kind;
492
+ }
493
+ }
494
+ function simulateStub() {
495
+ return {
496
+ backend: 'ts-model',
497
+ permitted: null,
498
+ evaluations: [],
499
+ notes: ['stub: real permit/deny semantics wiring is a later phase'],
500
+ };
501
+ }
502
+ /** Canonical JSON with recursively sorted object keys (stable across runs). */
503
+ function canonicalStringify(value) {
504
+ return JSON.stringify(sortKeys(value));
505
+ }
506
+ function sortKeys(value) {
507
+ if (Array.isArray(value))
508
+ return value.map(sortKeys);
509
+ if (value && typeof value === 'object') {
510
+ const out = {};
511
+ for (const key of Object.keys(value).sort()) {
512
+ out[key] = sortKeys(value[key]);
513
+ }
514
+ return out;
515
+ }
516
+ return value;
517
+ }
518
+ /** Build a synthetic Error carrying the ToolError code/severity/retryable
519
+ * fields the existing ToolError shape uses (the encoder pattern). Callers
520
+ * inspect `e.code` in catch blocks. */
521
+ function toolError(code, message) {
522
+ const err = new Error(message);
523
+ err.code = code;
524
+ err.severity = 'error';
525
+ err.retryable = false;
526
+ return err;
527
+ }
@@ -0,0 +1 @@
1
+ export { createInterpreterAdapter, type InterpreterAdapterConfig, ORACLE_DEFAULTS, PLACEHOLDER_INTERPRETER_ADDRESS, } from './adapter.ts';
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ // src/adapters/interpreter/index.ts - re-export the interpreter-policy CustodyAdapter.
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.PLACEHOLDER_INTERPRETER_ADDRESS = exports.ORACLE_DEFAULTS = exports.createInterpreterAdapter = void 0;
5
+ var adapter_ts_1 = require("./adapter.js");
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
+ Object.defineProperty(exports, "PLACEHOLDER_INTERPRETER_ADDRESS", { enumerable: true, get: function () { return adapter_ts_1.PLACEHOLDER_INTERPRETER_ADDRESS; } });
@@ -0,0 +1,20 @@
1
+ import type { CustodyAdapter } from '../../seams/types.ts';
2
+ import type { Network } from '../../types.ts';
3
+ /** OZ built-in policy contract instance addresses, per primitive. */
4
+ export interface OzPrimitiveInstances {
5
+ spending_limit: string;
6
+ simple_threshold: string;
7
+ weighted_threshold: string;
8
+ }
9
+ /** Per-network config for the OZ adapter. */
10
+ export interface OzAdapterConfig {
11
+ network: Network;
12
+ instances: OzPrimitiveInstances;
13
+ }
14
+ /** [VERIFY] NOT real deployed addresses. The OZ built-in policy instances are
15
+ * per-network deploy artifacts we do not have yet; install is a later phase.
16
+ * Injected so the adapter never invents a Stellar contract address. */
17
+ export declare const PLACEHOLDER_OZ_INSTANCES: OzPrimitiveInstances;
18
+ /** Week-1 OZ adapter config with placeholder instance addresses. */
19
+ export declare function placeholderOzConfig(network: Network): OzAdapterConfig;
20
+ export declare function createOzAdapter(config: OzAdapterConfig): CustodyAdapter;