@crediolabs/policy-synth 0.4.0 → 0.5.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 (66) hide show
  1. package/dist/adapters/interpreter/adapter.d.ts +2 -2
  2. package/dist/adapters/interpreter/adapter.js +11 -3
  3. package/dist/errors.d.ts +6 -1
  4. package/dist/install/authority-overlap.js +12 -0
  5. package/dist/install/build-add-context-rule.d.ts +1 -1
  6. package/dist/install/read-account-rules.d.ts +79 -0
  7. package/dist/install/read-account-rules.js +241 -0
  8. package/dist/predicate/decode.js +22 -1
  9. package/dist/predicate/encode.js +52 -5
  10. package/dist/predicate/from-json.js +14 -1
  11. package/dist/review-card/builder.js +40 -0
  12. package/dist/review-card/cross-check.js +34 -0
  13. package/dist/review-card/render-leaf.d.ts +1 -1
  14. package/dist/review-card/render-leaf.js +7 -0
  15. package/dist/run/index.js +51 -8
  16. package/dist/run/schemas.d.ts +45 -14
  17. package/dist/run/schemas.js +43 -6
  18. package/dist/simulate/deny-cases.js +11 -0
  19. package/dist/simulate/evaluate.js +86 -5
  20. package/dist/synth/declare.d.ts +13 -0
  21. package/dist/synth/declare.js +34 -5
  22. package/dist/synth/synthesize-from-recording.js +1 -1
  23. package/dist/types.d.ts +21 -1
  24. package/dist/types.js +1 -1
  25. package/dist-cjs/adapters/interpreter/adapter.d.ts +2 -2
  26. package/dist-cjs/adapters/interpreter/adapter.js +11 -3
  27. package/dist-cjs/errors.d.ts +6 -1
  28. package/dist-cjs/install/authority-overlap.js +12 -0
  29. package/dist-cjs/install/build-add-context-rule.d.ts +1 -1
  30. package/dist-cjs/install/read-account-rules.d.ts +79 -0
  31. package/dist-cjs/install/read-account-rules.js +252 -0
  32. package/dist-cjs/predicate/decode.js +22 -1
  33. package/dist-cjs/predicate/encode.js +52 -5
  34. package/dist-cjs/predicate/from-json.js +14 -1
  35. package/dist-cjs/review-card/builder.js +40 -0
  36. package/dist-cjs/review-card/cross-check.js +34 -0
  37. package/dist-cjs/review-card/render-leaf.d.ts +1 -1
  38. package/dist-cjs/review-card/render-leaf.js +7 -0
  39. package/dist-cjs/run/index.js +51 -8
  40. package/dist-cjs/run/schemas.d.ts +45 -14
  41. package/dist-cjs/run/schemas.js +43 -6
  42. package/dist-cjs/simulate/deny-cases.js +11 -0
  43. package/dist-cjs/simulate/evaluate.js +86 -5
  44. package/dist-cjs/synth/declare.d.ts +13 -0
  45. package/dist-cjs/synth/declare.js +34 -5
  46. package/dist-cjs/synth/synthesize-from-recording.js +1 -1
  47. package/dist-cjs/types.d.ts +21 -1
  48. package/dist-cjs/types.js +1 -1
  49. package/package.json +1 -1
  50. package/src/adapters/interpreter/adapter.ts +13 -5
  51. package/src/errors.ts +5 -0
  52. package/src/install/authority-overlap.ts +11 -0
  53. package/src/install/read-account-rules.ts +313 -0
  54. package/src/predicate/decode.ts +22 -1
  55. package/src/predicate/encode.ts +55 -5
  56. package/src/predicate/from-json.ts +14 -1
  57. package/src/review-card/builder.ts +45 -2
  58. package/src/review-card/cross-check.ts +35 -1
  59. package/src/review-card/render-leaf.ts +8 -1
  60. package/src/run/index.ts +54 -8
  61. package/src/run/schemas.ts +43 -6
  62. package/src/simulate/deny-cases.ts +12 -1
  63. package/src/simulate/evaluate.ts +101 -9
  64. package/src/synth/declare.ts +54 -5
  65. package/src/synth/synthesize-from-recording.ts +1 -1
  66. package/src/types.ts +16 -1
@@ -1,4 +1,4 @@
1
- // src/simulate/evaluate.ts - TypeScript reference evaluator for grammar version 3.
1
+ // src/simulate/evaluate.ts - TypeScript reference evaluator for grammar version 4.
2
2
  //
3
3
  // Pure function. Determinism: same `(predicate, ctx)` -> byte-identical result,
4
4
  // no clock, no randomness. Deny order (deny on FIRST violation, stable reason):
@@ -8,12 +8,13 @@
8
8
  // 3. `in` membership; empty haystack ALWAYS denies -> 'NOT_IN_ALLOWLIST'
9
9
  // 4. otherwise permit.
10
10
  //
11
- // Grammar version 3 nodes: and, eq, lte
12
- // Grammar version 3 leaves: call_contract, call_fn, call_arg(i),
11
+ // Grammar version 4 nodes: and, or, eq, lt, lte, gt, gte, in
12
+ // Grammar version 4 leaves: call_contract, call_fn, call_arg(i),
13
13
  // call_arg_len(i), call_arg_field(i, element, field),
14
+ // call_arg_scaled(i, num, den),
14
15
  // literal_address, literal_i128, literal_symbol, literal_u32, literal_vec
15
- // Grammar version 3 deny reasons: ARG_MISMATCH, CONTRACT_SCOPE,
16
- // UNSUPPORTED_NODE, NOT_IN_ALLOWLIST
16
+ // Grammar version 4 deny reasons: ARG_MISMATCH, CONTRACT_SCOPE,
17
+ // ARITHMETIC_OVERFLOW, UNSUPPORTED_NODE, NOT_IN_ALLOWLIST, SLIPPAGE_FLOOR
17
18
  /** Evaluate a `PredicateNode` against the candidate call described by `ctx`.
18
19
  * Pure function. Returns `{ permit: true }` or `{ permit: false; reason }`. */
19
20
  export function evaluate(predicate, ctx) {
@@ -33,8 +34,25 @@ function walk(node, ctx) {
33
34
  }
34
35
  return lastDeny ?? { permit: true };
35
36
  }
37
+ // Permits on the first branch that holds; when none does, reports the
38
+ // FIRST branch's reason. Mirrors `Node::Or` in the Rust evaluator, which
39
+ // the conformance suite pins.
40
+ case 'or': {
41
+ let firstDeny = null;
42
+ for (const child of node.children) {
43
+ const r = walk(child, ctx);
44
+ if (r.permit)
45
+ return r;
46
+ if (firstDeny === null)
47
+ firstDeny = r;
48
+ }
49
+ return firstDeny ?? { permit: false, reason: 'UNSUPPORTED_NODE' };
50
+ }
36
51
  case 'eq':
52
+ case 'lt':
37
53
  case 'lte':
54
+ case 'gt':
55
+ case 'gte':
38
56
  return evalCompare(node.op, node.left, node.right, ctx);
39
57
  case 'in':
40
58
  return evalIn(node.needle, node.haystack, ctx);
@@ -42,6 +60,16 @@ function walk(node, ctx) {
42
60
  }
43
61
  /** Comparison leaf evaluation. */
44
62
  function evalCompare(op, left, right, ctx) {
63
+ // Scaled operands first, so the dedicated reasons reach the caller instead
64
+ // of a generic mismatch. Right-hand dispatch leads because
65
+ // `out >= in * num / den` is the canonical swap form. Mirrors the order in
66
+ // `eval_compare` on the Rust side.
67
+ if (right.kind === 'call_arg_scaled') {
68
+ return evalScaledCompare(op, left, right, true, ctx);
69
+ }
70
+ if (left.kind === 'call_arg_scaled') {
71
+ return evalScaledCompare(op, right, left, false, ctx);
72
+ }
45
73
  // CONTRACT_SCOPE on call_contract eq
46
74
  if (left.kind === 'call_contract' && op === 'eq') {
47
75
  if (right.kind !== 'literal_address')
@@ -96,6 +124,53 @@ function evalCompare(op, left, right, ctx) {
96
124
  // Unknown leaf/op combination - structural fail-closed.
97
125
  return { permit: false, reason: 'UNSUPPORTED_NODE' };
98
126
  }
127
+ /** i128 bounds. The contract computes in i128 and denies on overflow, so the
128
+ * reference has to draw the same line or the two layers disagree on inputs
129
+ * near the boundary. */
130
+ const I128_MIN = -(2n ** 127n);
131
+ const I128_MAX = 2n ** 127n - 1n;
132
+ /** Comparison where one side is `call_arg_scaled`. Mirrors
133
+ * `eval_scaled_arg_compare`: `args[index] * num / den` truncating toward
134
+ * zero, ARITHMETIC_OVERFLOW on arithmetic that does not fit or a zero
135
+ * denominator, SLIPPAGE_FLOOR on a comparison that simply fails. */
136
+ function evalScaledCompare(op, other, scaled, scaledOnRight, ctx) {
137
+ // Chaining two computed operands has no meaning a review card could state.
138
+ if (other.kind === 'call_arg_scaled')
139
+ return { permit: false, reason: 'UNSUPPORTED_NODE' };
140
+ // Could not READ the operand is a different failure from read-and-missed.
141
+ const input = argNumericBigInt(ctx.args[scaled.index]);
142
+ if (input === null)
143
+ return { permit: false, reason: 'ARG_MISMATCH' };
144
+ let num;
145
+ let den;
146
+ try {
147
+ num = BigInt(scaled.num);
148
+ den = BigInt(scaled.den);
149
+ }
150
+ catch {
151
+ return { permit: false, reason: 'ARG_MISMATCH' };
152
+ }
153
+ if (den === 0n)
154
+ return { permit: false, reason: 'ARITHMETIC_OVERFLOW' };
155
+ const product = input * num;
156
+ if (product < I128_MIN || product > I128_MAX) {
157
+ return { permit: false, reason: 'ARITHMETIC_OVERFLOW' };
158
+ }
159
+ // BigInt division already truncates toward zero, matching i128 semantics.
160
+ const quotient = product / den;
161
+ if (quotient < I128_MIN || quotient > I128_MAX) {
162
+ return { permit: false, reason: 'ARITHMETIC_OVERFLOW' };
163
+ }
164
+ const otherVal = other.kind === 'call_arg'
165
+ ? argNumericBigInt(ctx.args[other.index])
166
+ : literalNumericBigInt(other);
167
+ if (otherVal === null)
168
+ return { permit: false, reason: 'ARG_MISMATCH' };
169
+ const [a, b] = scaledOnRight ? [otherVal, quotient] : [quotient, otherVal];
170
+ return bigintCmp(op, a.toString(), b.toString())
171
+ ? { permit: true }
172
+ : { permit: false, reason: 'SLIPPAGE_FLOOR' };
173
+ }
99
174
  /** Per-ScVal equality. Handles literal_vec as an EXACT ordered sequence:
100
175
  * compare element-by-element in order; deny if length or any element differs.
101
176
  * Opaque args (`type: 'other'`) fail closed. */
@@ -265,7 +340,13 @@ function bigintCmp(op, aStr, bStr) {
265
340
  switch (op) {
266
341
  case 'eq':
267
342
  return a === b;
343
+ case 'lt':
344
+ return a < b;
268
345
  case 'lte':
269
346
  return a <= b;
347
+ case 'gt':
348
+ return a > b;
349
+ case 'gte':
350
+ return a >= b;
270
351
  }
271
352
  }
@@ -18,6 +18,19 @@ export interface PolicyDeclaration {
18
18
  * explicitly. A rule that permits nothing is a plausible thing to want and
19
19
  * an implausible thing to want by accident. */
20
20
  allowZeroCap?: boolean;
21
+ /** Minimum output as a ratio of the call's own input: the output argument
22
+ * must be at least `input * num / den`.
23
+ *
24
+ * A swap's acceptable output depends on the size of the trade, so a fixed
25
+ * floor would pin the policy to one trade size. The ratio is DECLARED, never
26
+ * inferred from a recording: a recorded rate is a price at one moment, and
27
+ * freezing it as policy would deny ordinary trades later. */
28
+ minOutputRatio?: {
29
+ num: string;
30
+ den: string;
31
+ inputArgIndex: number;
32
+ outputArgIndex: number;
33
+ };
21
34
  }
22
35
  export interface DeclaredPredicate {
23
36
  predicate: PredicateNode;
@@ -15,11 +15,12 @@
15
15
  // first: it constrains the amount in THIS call rather than implying a rolling
16
16
  // total nothing tracks.
17
17
  //
18
- // What a declaration can say maps one-to-one onto grammar 3:
19
- // fn -> eq(call_fn, literal_symbol)
20
- // contract -> eq(call_contract, literal_address)
21
- // maxAmount -> lte(call_arg(i), literal_i128)
22
- // recipients -> in(call_arg(j), [literal_address, ...])
18
+ // What a declaration can say maps one-to-one onto grammar 4:
19
+ // fn -> eq(call_fn, literal_symbol)
20
+ // contract -> eq(call_contract, literal_address)
21
+ // maxAmount -> lte(call_arg(i), literal_i128)
22
+ // recipients -> in(call_arg(j), [literal_address, ...])
23
+ // minOutputRatio-> gte(call_arg(out), call_arg_scaled(in, num, den))
23
24
  import { isStellarAddress } from "./address.js";
24
25
  /** Argument positions of the SEP-41 `transfer(from, to, amount)` shape. A
25
26
  * declaration that names a different method almost certainly has different
@@ -84,6 +85,34 @@ export function declarePredicate(d) {
84
85
  right: { kind: 'literal_i128', value: d.maxAmount },
85
86
  });
86
87
  }
88
+ if (d.minOutputRatio !== undefined) {
89
+ const { num, den, inputArgIndex, outputArgIndex } = d.minOutputRatio;
90
+ if (!/^[0-9]+$/.test(num) || !/^[0-9]+$/.test(den)) {
91
+ throw declareError('SYNTHESIS_ERROR', `minOutputRatio num/den must be unsigned integers, got "${num}"/"${den}" (a 1% slippage tolerance is num "99", den "100")`);
92
+ }
93
+ // Both are refused on chain at install (INVALID_SCALED_RATIO). Refusing
94
+ // here too means the caller learns before a transaction is built.
95
+ if (den === '0') {
96
+ throw declareError('SYNTHESIS_ERROR', 'minOutputRatio.den is zero: the ratio has no value');
97
+ }
98
+ if (num === '0') {
99
+ throw declareError('SYNTHESIS_ERROR', 'minOutputRatio.num is zero: the floor would be zero, which constrains nothing. Omit it instead.');
100
+ }
101
+ if (inputArgIndex === outputArgIndex) {
102
+ throw declareError('SYNTHESIS_ERROR', `minOutputRatio bounds arg[${inputArgIndex}] against itself, which is true for any ratio at or below 1 and false above it - never a slippage floor. Pass the distinct input and output positions.`);
103
+ }
104
+ if (BigInt(num) > BigInt(den)) {
105
+ // Demanding MORE out than went in is not slippage protection; it is a
106
+ // rule that denies every honest trade. Loud beats a policy that never
107
+ // permits.
108
+ warnings.push(`minOutputRatio ${num}/${den} is above 1: it requires the output to EXCEED the input, which no ordinary swap satisfies. Check the ratio is not inverted.`);
109
+ }
110
+ children.push({
111
+ op: 'gte',
112
+ left: { kind: 'call_arg', index: outputArgIndex },
113
+ right: { kind: 'call_arg_scaled', index: inputArgIndex, num, den },
114
+ });
115
+ }
87
116
  // A single conjunct is emitted bare. `and` with one child encodes to
88
117
  // different bytes than the child alone, and the extra node buys nothing.
89
118
  const predicate = children.length === 1 ? children[0] : { op: 'and', children };
@@ -1,7 +1,7 @@
1
1
  // src/synth/synthesize-from-recording.ts - recording-path orchestrator.
2
2
  //
3
3
  // `synthesizeFromRecording` INFERS a bounded policy from a `RecordedTransaction`
4
- // via the `PolicyIR` + interpreter adapter pair.
4
+ // via the composer + interpreter adapter pair.
5
5
  // Flow: validate -> parseConfidence gate -> lower -> decideScope -> composeFromRecording
6
6
  // -> interpreter compile + self-verify.
7
7
  // Same (tx, opts) -> byte-identical ProposedPolicy (no randomness, clock, globals).
package/dist/types.d.ts CHANGED
@@ -124,7 +124,7 @@ export type PolicyRef = {
124
124
  * `contracts/policy-interpreter/src/version.rs`. Every value this package puts on
125
125
  * the wire derives from here; `grammar-version-parity.test.ts` pins it to the
126
126
  * contract so a skew cannot pass a green test run. */
127
- export declare const GRAMMAR_VERSION: 3;
127
+ export declare const GRAMMAR_VERSION: 4;
128
128
  /** A serialised predicate document stored against a (smart_account, rule_id) pair. */
129
129
  export interface PolicyDocument {
130
130
  /** Grammar version baked into the interpreter wasm. Fail-closed strict match at install
@@ -146,14 +146,29 @@ export interface PolicyDocument {
146
146
  export type PredicateNode = {
147
147
  op: 'and';
148
148
  children: PredicateNode[];
149
+ } | {
150
+ op: 'or';
151
+ children: PredicateNode[];
149
152
  } | {
150
153
  op: 'eq';
151
154
  left: PredicateLeaf;
152
155
  right: PredicateLeaf;
156
+ } | {
157
+ op: 'lt';
158
+ left: PredicateLeaf;
159
+ right: PredicateLeaf;
153
160
  } | {
154
161
  op: 'lte';
155
162
  left: PredicateLeaf;
156
163
  right: PredicateLeaf;
164
+ } | {
165
+ op: 'gt';
166
+ left: PredicateLeaf;
167
+ right: PredicateLeaf;
168
+ } | {
169
+ op: 'gte';
170
+ left: PredicateLeaf;
171
+ right: PredicateLeaf;
157
172
  } | {
158
173
  op: 'in';
159
174
  needle: PredicateLeaf;
@@ -174,6 +189,11 @@ export type PredicateLeaf = {
174
189
  index: number;
175
190
  element: number;
176
191
  field: string;
192
+ } | {
193
+ kind: 'call_arg_scaled';
194
+ index: number;
195
+ num: string;
196
+ den: string;
177
197
  } | {
178
198
  kind: 'literal_address';
179
199
  value: string;
package/dist/types.js CHANGED
@@ -52,4 +52,4 @@ export const PREDICATE_CAPS = {
52
52
  * `contracts/policy-interpreter/src/version.rs`. Every value this package puts on
53
53
  * the wire derives from here; `grammar-version-parity.test.ts` pins it to the
54
54
  * contract so a skew cannot pass a green test run. */
55
- export const GRAMMAR_VERSION = 3;
55
+ export const GRAMMAR_VERSION = 4;
@@ -12,9 +12,9 @@ export interface InterpreterAdapterConfig {
12
12
  * rejected as `SCOPE_SELF_CALL`. */
13
13
  smartAccountAddress: string;
14
14
  }
15
- /** The result of compiling a PolicyIR for this backend. */
15
+ /** The result of compiling a `ComposedRule` for this backend. */
16
16
  export interface CompileResult {
17
- /** false => some IR construct this backend cannot express (see `uncovered`). */
17
+ /** false => some constraint this backend cannot express (see `uncovered`). */
18
18
  covered: boolean;
19
19
  /** Human-readable list of unsupported constructs. */
20
20
  uncovered: string[];
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
- // src/adapters/interpreter/adapter.ts - the interpreter-policy CustodyAdapter.
2
+ // src/adapters/interpreter/adapter.ts - the interpreter-policy adapter.
3
3
  //
4
- // Compiles a PolicyIR to a single interpreter `PolicyDocument` + `PolicyRef`
4
+ // Compiles a `ComposedRule` to a single interpreter `PolicyDocument` + `PolicyRef`
5
5
  // carrying the canonical predicate encoding from `predicate/encode.ts`. This is
6
6
  // the only backend; the compose step lowers every constraint to it.
7
7
  //
@@ -138,7 +138,11 @@ function assertNoSelfCall(node, config) {
138
138
  }
139
139
  };
140
140
  switch (node.op) {
141
+ // Both boolean nodes recurse. `or` is listed explicitly rather than left
142
+ // to a default branch: this is a security check, and a new node kind must
143
+ // fail to compile here rather than silently skip it.
141
144
  case 'and':
145
+ case 'or':
142
146
  for (const c of node.children)
143
147
  assertNoSelfCall(c, config);
144
148
  return;
@@ -147,7 +151,11 @@ function assertNoSelfCall(node, config) {
147
151
  for (const h of node.haystack)
148
152
  checkLeaf(h);
149
153
  return;
150
- default:
154
+ case 'eq':
155
+ case 'lt':
156
+ case 'lte':
157
+ case 'gt':
158
+ case 'gte':
151
159
  checkLeaf(node.left);
152
160
  checkLeaf(node.right);
153
161
  }
@@ -1,4 +1,9 @@
1
- export type ErrorCode = 'RECORDING_FAILED' | 'RECORDING_VALIDATION_FAILED' | 'SCOPE_UNRESOLVED' | 'SYNTHESIS_ERROR' | 'MALFORMED_PREDICATE' | 'SIMULATION_ERROR' | 'VERIFICATION_FAILED' | 'DENY_CASE_FAILURE' | 'PERMIT_CASE_FAILED' | 'SUMMARY_DRIFT' | 'INSTALL_BUILD_FAILED' | 'INSTALL_CONFIRM_MISSING' | 'INSTALL_CONFIRM_EXPIRED' | 'REVOKE_BUILD_FAILED' | 'REVOKE_CONFIRM_MISSING' | 'USER_REJECTED_SIGN' | 'WALLET_TIMEOUT' | 'WALLET_UNAVAILABLE' | 'PREDICATE_TOO_LARGE' | 'PREDICATE_TOO_DEEP' | 'TOO_MANY_LEAVES' | 'IN_OPERAND_LIMIT' | 'POLICY_CAP_EXCEEDED' | 'WASM_TOO_LARGE' | 'MASTER_AUTH_REQUIRED' | 'NONCE_REPLAY' | 'VERSION_MISMATCH' | 'ARITHMETIC_OVERFLOW' | 'AMOUNT_OVERFLOW' | 'RULE_SIGNERS_CHANGED' | 'SCOPE_SELF_CALL' | 'ARG_MISMATCH' | 'CONTRACT_SCOPE' | 'UNSUPPORTED_NODE' | 'STATEFUL_BOUND' | 'NOT_IN_ALLOWLIST' | 'COMPILE_OK' | 'COMPILE_GATE_FAILED';
1
+ export type ErrorCode = 'RECORDING_FAILED' | 'RECORDING_VALIDATION_FAILED' | 'SCOPE_UNRESOLVED' | 'SYNTHESIS_ERROR' | 'MALFORMED_PREDICATE' | 'SIMULATION_ERROR' | 'VERIFICATION_FAILED' | 'DENY_CASE_FAILURE' | 'PERMIT_CASE_FAILED' | 'SUMMARY_DRIFT' | 'INSTALL_BUILD_FAILED' | 'INSTALL_CONFIRM_MISSING' | 'INSTALL_CONFIRM_EXPIRED' | 'REVOKE_BUILD_FAILED' | 'REVOKE_CONFIRM_MISSING' | 'USER_REJECTED_SIGN' | 'WALLET_TIMEOUT' | 'WALLET_UNAVAILABLE' | 'PREDICATE_TOO_LARGE' | 'PREDICATE_TOO_DEEP' | 'TOO_MANY_LEAVES' | 'IN_OPERAND_LIMIT' | 'POLICY_CAP_EXCEEDED' | 'WASM_TOO_LARGE' | 'MASTER_AUTH_REQUIRED' | 'NONCE_REPLAY' | 'VERSION_MISMATCH' | 'ARITHMETIC_OVERFLOW' | 'AMOUNT_OVERFLOW' | 'RULE_SIGNERS_CHANGED' | 'SCOPE_SELF_CALL' | 'ARG_MISMATCH' | 'CONTRACT_SCOPE' | 'UNSUPPORTED_NODE' | 'STATEFUL_BOUND' | 'NOT_IN_ALLOWLIST'
2
+ /** A comparison against a `call_arg_scaled` operand failed (contract 107). */
3
+ | 'SLIPPAGE_FLOOR'
4
+ /** A `call_arg_scaled` ratio is zero or non-positive. Refused at install
5
+ * (contract 214) and mirrored at encode. */
6
+ | 'INVALID_SCALED_RATIO' | 'COMPILE_OK' | 'COMPILE_GATE_FAILED';
2
7
  export interface ToolError {
3
8
  code: ErrorCode;
4
9
  message: string;
@@ -134,6 +134,18 @@ function permittedSelectors(node) {
134
134
  acc = intersectSelectors(acc, permittedSelectors(child));
135
135
  return acc;
136
136
  }
137
+ case 'or': {
138
+ // Any branch may hold, so the permitted set is the UNION. A union of
139
+ // over-approximations is still an over-approximation, so this keeps the
140
+ // fail-safe direction while staying tighter than the wildcard the
141
+ // default branch would give. Precision matters here: `or` is how a
142
+ // policy says "pair A or pair B", and widening that to the wildcard
143
+ // would report an overlap against every rule on the account.
144
+ const acc = [];
145
+ for (const child of node.children)
146
+ acc.push(...permittedSelectors(child));
147
+ return dedupe(acc);
148
+ }
137
149
  case 'eq': {
138
150
  const sel = selectorFromEq(node.left, node.right);
139
151
  return sel === null ? [WILDCARD] : [sel];
@@ -23,7 +23,7 @@ export interface BuildAddContextRuleArgs {
23
23
  * before the chain does; the contract will refuse a mismatch again. */
24
24
  grammarVersion?: number;
25
25
  }
26
- export declare const DEFAULT_GRAMMAR_VERSION: 3;
26
+ export declare const DEFAULT_GRAMMAR_VERSION: 4;
27
27
  /** The verb `add_context_rule` takes on the wire. */
28
28
  export declare const ADD_CONTEXT_RULE_SYMBOL: "add_context_rule";
29
29
  /** Tuple of `ScVal` arguments to pass to `Operation.invokeHostFunction` for
@@ -0,0 +1,79 @@
1
+ import { rpc, xdr } from '@stellar/stellar-sdk';
2
+ import type { SignerDraft } from '../types.ts';
3
+ import type { ContextType, ObservedRule } from './authority-overlap.ts';
4
+ /** `storage.rs` - the third element of the persistent doc key tuple. */
5
+ export declare const K_DOC = 1;
6
+ /** Persistent-storage key for a rule's stored document:
7
+ * `(account, rule_id, K_DOC)`. */
8
+ export declare function docKeyScVal(smartAccount: string, ruleId: number): xdr.ScVal;
9
+ /** Ledger key for the interpreter's persistent entry holding that document. */
10
+ export declare function docLedgerKey(interpreter: string, smartAccount: string, ruleId: number): xdr.LedgerKey;
11
+ /** OZ `ContextRuleType`. An unrecognised tag is reported as `default`, which
12
+ * is the widest reading and therefore the safe one: it makes the rule look
13
+ * like it could serve any call, so overlap is over-reported, never missed. */
14
+ export declare function decodeContextType(v: xdr.ScVal | undefined): ContextType;
15
+ /** OZ `Signer::Delegated(Address) | Signer::External(Address, Bytes)`. */
16
+ export declare function decodeSigner(v: xdr.ScVal): SignerDraft | undefined;
17
+ /** A full OZ `ContextRule` as returned by `get_context_rule(id)`.
18
+ * `predicate` is filled in separately from the ledger entry. */
19
+ export declare function decodeContextRule(v: xdr.ScVal): ObservedRule | undefined;
20
+ /** The interpreter's `StoredDoc { predicate_bytes }`. */
21
+ export declare function decodeStoredPredicateBytes(v: xdr.ScVal): Buffer | undefined;
22
+ /** The three reads the scan needs. Kept as an interface so the collection
23
+ * below is testable without a network. */
24
+ export interface AccountRuleReader {
25
+ /** OZ `get_context_rules_count()`. */
26
+ getContextRuleCount(smartAccount: string): Promise<number>;
27
+ /** OZ `get_context_rule(id)`. Undefined when the id is absent. */
28
+ getContextRule(smartAccount: string, ruleId: number): Promise<xdr.ScVal | undefined>;
29
+ /** The interpreter's persistent `StoredDoc` entry, read as a ledger entry.
30
+ * Undefined when no document is stored for that rule. */
31
+ getStoredDoc(interpreter: string, smartAccount: string, ruleId: number): Promise<xdr.ScVal | undefined>;
32
+ }
33
+ /** How far the id scan will probe before giving up. OZ imposes no per-account
34
+ * rule cap, so there is no exact bound to derive; this one is far above any
35
+ * realistic account and keeps a malformed `Count` from spinning forever. */
36
+ export declare const MAX_RULE_ID_SCAN = 512;
37
+ export interface CollectedRules {
38
+ rules: ObservedRule[];
39
+ /** Rule ids whose stored predicate could not be read even though the
40
+ * interpreter is attached. Such a rule is reported without a predicate,
41
+ * which classifies it as opaque rather than as safely narrow. */
42
+ unreadablePredicateRuleIds: number[];
43
+ /** True when the scan stopped before accounting for every live rule. The
44
+ * result is then a SUBSET of the account's rules, so an empty overlap list
45
+ * proves nothing and the caller must not present it as safety. */
46
+ incomplete: boolean;
47
+ }
48
+ /**
49
+ * Every context rule on the account, with predicates filled in for the rules
50
+ * our interpreter polices.
51
+ *
52
+ * Rule ids are NOT contiguous. OZ assigns them from a monotonic `NextId` and
53
+ * decrements `Count` on removal without ever reusing an id, so after any
54
+ * removal `Count < NextId` and the live ids have gaps. Iterating `0..Count-1`
55
+ * would silently skip live rules at higher ids, and a skipped rule is a missed
56
+ * overlap - the one error that reports safety which does not exist. Instead the
57
+ * scan walks ids upward until it has accounted for `Count` live rules.
58
+ *
59
+ * A rule whose predicate cannot be read is deliberately left without one. That
60
+ * demotes it to the `foreign` class, so the scan reports it as opaque instead
61
+ * of assuming it is narrow.
62
+ */
63
+ export declare function collectObservedRules(args: {
64
+ reader: AccountRuleReader;
65
+ smartAccount: string;
66
+ interpreterAddress: string;
67
+ maxRuleIdScan?: number;
68
+ }): Promise<CollectedRules>;
69
+ /**
70
+ * An `AccountRuleReader` over a live RPC server.
71
+ *
72
+ * The two OZ getters are read-only simulations: the source account is
73
+ * constructed locally because a simulation never checks its sequence number,
74
+ * and asking the network for a random key would 404.
75
+ *
76
+ * The stored document is fetched as a ledger entry rather than a contract
77
+ * call, because the interpreter publishes no getter for it.
78
+ */
79
+ export declare function accountRuleReaderFromServer(server: rpc.Server, networkPassphrase: string): AccountRuleReader;