@crediolabs/policy-synth 0.3.1 → 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 (100) 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.d.ts +101 -0
  5. package/dist/install/authority-overlap.js +239 -0
  6. package/dist/install/build-add-context-rule.d.ts +1 -1
  7. package/dist/install/index.d.ts +1 -0
  8. package/dist/install/index.js +4 -0
  9. package/dist/install/read-account-rules.d.ts +79 -0
  10. package/dist/install/read-account-rules.js +241 -0
  11. package/dist/predicate/decode.js +22 -1
  12. package/dist/predicate/encode.js +52 -5
  13. package/dist/predicate/from-json.js +14 -1
  14. package/dist/record/index.d.ts +10 -0
  15. package/dist/record/index.js +32 -1
  16. package/dist/record/rpc.d.ts +4 -0
  17. package/dist/record/rpc.js +4 -1
  18. package/dist/registry/identify.d.ts +10 -1
  19. package/dist/registry/identify.js +4 -1
  20. package/dist/registry/on-chain-spec.d.ts +37 -0
  21. package/dist/registry/on-chain-spec.js +152 -0
  22. package/dist/review-card/builder.js +40 -0
  23. package/dist/review-card/cross-check.js +34 -0
  24. package/dist/review-card/render-leaf.d.ts +1 -1
  25. package/dist/review-card/render-leaf.js +7 -0
  26. package/dist/run/index.d.ts +25 -4
  27. package/dist/run/index.js +108 -4
  28. package/dist/run/schemas.d.ts +350 -6
  29. package/dist/run/schemas.js +95 -6
  30. package/dist/simulate/deny-cases.js +11 -0
  31. package/dist/simulate/evaluate.js +86 -5
  32. package/dist/synth/declare.d.ts +43 -0
  33. package/dist/synth/declare.js +127 -0
  34. package/dist/synth/index.d.ts +1 -0
  35. package/dist/synth/index.js +1 -0
  36. package/dist/synth/synthesize-from-recording.js +1 -1
  37. package/dist/types.d.ts +21 -1
  38. package/dist/types.js +1 -1
  39. package/dist-cjs/adapters/interpreter/adapter.d.ts +2 -2
  40. package/dist-cjs/adapters/interpreter/adapter.js +11 -3
  41. package/dist-cjs/errors.d.ts +6 -1
  42. package/dist-cjs/install/authority-overlap.d.ts +101 -0
  43. package/dist-cjs/install/authority-overlap.js +248 -0
  44. package/dist-cjs/install/build-add-context-rule.d.ts +1 -1
  45. package/dist-cjs/install/index.d.ts +1 -0
  46. package/dist-cjs/install/index.js +13 -2
  47. package/dist-cjs/install/read-account-rules.d.ts +79 -0
  48. package/dist-cjs/install/read-account-rules.js +252 -0
  49. package/dist-cjs/predicate/decode.js +22 -1
  50. package/dist-cjs/predicate/encode.js +52 -5
  51. package/dist-cjs/predicate/from-json.js +14 -1
  52. package/dist-cjs/record/index.d.ts +10 -0
  53. package/dist-cjs/record/index.js +31 -0
  54. package/dist-cjs/record/rpc.d.ts +4 -0
  55. package/dist-cjs/record/rpc.js +7 -3
  56. package/dist-cjs/registry/identify.d.ts +10 -1
  57. package/dist-cjs/registry/identify.js +4 -0
  58. package/dist-cjs/registry/on-chain-spec.d.ts +37 -0
  59. package/dist-cjs/registry/on-chain-spec.js +159 -0
  60. package/dist-cjs/review-card/builder.js +40 -0
  61. package/dist-cjs/review-card/cross-check.js +34 -0
  62. package/dist-cjs/review-card/render-leaf.d.ts +1 -1
  63. package/dist-cjs/review-card/render-leaf.js +7 -0
  64. package/dist-cjs/run/index.d.ts +25 -4
  65. package/dist-cjs/run/index.js +108 -2
  66. package/dist-cjs/run/schemas.d.ts +350 -6
  67. package/dist-cjs/run/schemas.js +96 -7
  68. package/dist-cjs/simulate/deny-cases.js +11 -0
  69. package/dist-cjs/simulate/evaluate.js +86 -5
  70. package/dist-cjs/synth/declare.d.ts +43 -0
  71. package/dist-cjs/synth/declare.js +130 -0
  72. package/dist-cjs/synth/index.d.ts +1 -0
  73. package/dist-cjs/synth/index.js +3 -1
  74. package/dist-cjs/synth/synthesize-from-recording.js +1 -1
  75. package/dist-cjs/types.d.ts +21 -1
  76. package/dist-cjs/types.js +1 -1
  77. package/package.json +1 -1
  78. package/src/adapters/interpreter/adapter.ts +13 -5
  79. package/src/errors.ts +5 -0
  80. package/src/install/authority-overlap.ts +323 -0
  81. package/src/install/index.ts +20 -0
  82. package/src/install/read-account-rules.ts +313 -0
  83. package/src/predicate/decode.ts +22 -1
  84. package/src/predicate/encode.ts +55 -5
  85. package/src/predicate/from-json.ts +14 -1
  86. package/src/record/index.ts +59 -2
  87. package/src/record/rpc.ts +4 -1
  88. package/src/registry/identify.ts +4 -1
  89. package/src/registry/on-chain-spec.ts +168 -0
  90. package/src/review-card/builder.ts +45 -2
  91. package/src/review-card/cross-check.ts +35 -1
  92. package/src/review-card/render-leaf.ts +8 -1
  93. package/src/run/index.ts +125 -2
  94. package/src/run/schemas.ts +100 -6
  95. package/src/simulate/deny-cases.ts +12 -1
  96. package/src/simulate/evaluate.ts +101 -9
  97. package/src/synth/declare.ts +206 -0
  98. package/src/synth/index.ts +5 -0
  99. package/src/synth/synthesize-from-recording.ts +1 -1
  100. package/src/types.ts +16 -1
@@ -0,0 +1,206 @@
1
+ // src/synth/declare.ts - build a predicate from a DECLARED constraint.
2
+ //
3
+ // The second synthesis front-end, beside `synthesizeFromRecording`. That one
4
+ // infers a predicate from a transaction that happened; this one takes the
5
+ // constraint the user states outright. No transaction, no decoding, no
6
+ // inference, no RPC: the same declaration always lowers to a byte-identical
7
+ // predicate.
8
+ //
9
+ // This is the surviving half of the removed `MandateSpec`. Two of that type's
10
+ // five fields lowered to things that do not exist - `spendingLimit` became a
11
+ // `window_spent` compare the interpreter cannot evaluate (it is handed one
12
+ // call and keeps no state), and `approvalThreshold` needed OZ built-in policy
13
+ // contracts that were never deployed. Both are deliberately absent here, and
14
+ // the per-call `maxAmount` bound below is the honest replacement for the
15
+ // first: it constrains the amount in THIS call rather than implying a rolling
16
+ // total nothing tracks.
17
+ //
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))
24
+
25
+ import type { ToolError } from '../errors.ts'
26
+ import type { PredicateLeaf, PredicateNode } from '../types.ts'
27
+ import { isStellarAddress } from './address.ts'
28
+
29
+ /** Argument positions of the SEP-41 `transfer(from, to, amount)` shape. A
30
+ * declaration that names a different method almost certainly has different
31
+ * positions, which is why using either default emits a warning naming the
32
+ * index it assumed - a bound on the wrong argument constrains something the
33
+ * user did not mean and fails silently. */
34
+ const SEP41_RECIPIENT_ARG = 1
35
+ const SEP41_AMOUNT_ARG = 2
36
+
37
+ export interface PolicyDeclaration {
38
+ /** Method to pin. Required: a predicate with no selector leaf constrains
39
+ * nothing and the contract refuses it at install. */
40
+ fn: string
41
+ /** Contract to pin, already resolved to a `C...` address. */
42
+ contract?: string
43
+ /** Upper bound on the call's amount argument, in the token's SMALLEST
44
+ * unit as an unsigned decimal string (25 XLM = "250000000"). */
45
+ maxAmount?: string
46
+ /** Which argument carries the amount. Defaults to the SEP-41 position. */
47
+ amountArgIndex?: number
48
+ /** Recipient allowlist. */
49
+ recipients?: string[]
50
+ /** Which argument carries the recipient. Defaults to the SEP-41 position. */
51
+ recipientArgIndex?: number
52
+ /** A cap of "0" denies every call, so it is refused unless asked for
53
+ * explicitly. A rule that permits nothing is a plausible thing to want and
54
+ * an implausible thing to want by accident. */
55
+ allowZeroCap?: boolean
56
+ /** Minimum output as a ratio of the call's own input: the output argument
57
+ * must be at least `input * num / den`.
58
+ *
59
+ * A swap's acceptable output depends on the size of the trade, so a fixed
60
+ * floor would pin the policy to one trade size. The ratio is DECLARED, never
61
+ * inferred from a recording: a recorded rate is a price at one moment, and
62
+ * freezing it as policy would deny ordinary trades later. */
63
+ minOutputRatio?: { num: string; den: string; inputArgIndex: number; outputArgIndex: number }
64
+ }
65
+
66
+ export interface DeclaredPredicate {
67
+ predicate: PredicateNode
68
+ /** Assumptions the caller should check. Never empty when an argument index
69
+ * was defaulted rather than supplied. */
70
+ warnings: string[]
71
+ }
72
+
73
+ /** Lower a declared constraint to a grammar-3 predicate. Pure and total:
74
+ * the same declaration always produces the same predicate. */
75
+ export function declarePredicate(d: PolicyDeclaration): DeclaredPredicate {
76
+ if (!d.fn || d.fn.trim() === '') {
77
+ throw declareError('SYNTHESIS_ERROR', 'a declaration needs `fn`: the method to pin')
78
+ }
79
+ const warnings: string[] = []
80
+ const children: PredicateNode[] = [
81
+ { op: 'eq', left: { kind: 'call_fn' }, right: { kind: 'literal_symbol', value: d.fn } },
82
+ ]
83
+
84
+ if (d.contract !== undefined) {
85
+ if (!isStellarAddress(d.contract) || !d.contract.startsWith('C')) {
86
+ throw declareError(
87
+ 'SYNTHESIS_ERROR',
88
+ `contract must be a Stellar contract address (C...), got ${d.contract}`
89
+ )
90
+ }
91
+ children.push({
92
+ op: 'eq',
93
+ left: { kind: 'call_contract' },
94
+ right: { kind: 'literal_address', value: d.contract },
95
+ })
96
+ }
97
+
98
+ if (d.recipients !== undefined) {
99
+ if (d.recipients.length === 0) {
100
+ throw declareError(
101
+ 'SYNTHESIS_ERROR',
102
+ 'recipients was supplied but empty; an empty `in` haystack is refused at decode. Omit it to leave recipients unconstrained.'
103
+ )
104
+ }
105
+ for (const r of d.recipients) {
106
+ if (!isStellarAddress(r)) {
107
+ throw declareError('SYNTHESIS_ERROR', `recipient is not a Stellar address: ${r}`)
108
+ }
109
+ }
110
+ const idx = d.recipientArgIndex ?? SEP41_RECIPIENT_ARG
111
+ if (d.recipientArgIndex === undefined) {
112
+ warnings.push(
113
+ `recipient allowlist bound to call_arg(${idx}), the SEP-41 \`transfer\` position. If \`${d.fn}\` carries the recipient elsewhere this constrains the wrong argument - pass recipientArgIndex.`
114
+ )
115
+ }
116
+ children.push({
117
+ op: 'in',
118
+ needle: { kind: 'call_arg', index: idx },
119
+ haystack: d.recipients.map((value): PredicateLeaf => ({ kind: 'literal_address', value })),
120
+ })
121
+ }
122
+
123
+ if (d.maxAmount !== undefined) {
124
+ if (!/^[0-9]+$/.test(d.maxAmount)) {
125
+ throw declareError(
126
+ 'SYNTHESIS_ERROR',
127
+ `maxAmount must be an unsigned integer in the token's smallest unit, got "${d.maxAmount}" (25 XLM = "250000000")`
128
+ )
129
+ }
130
+ if (d.maxAmount === '0' && d.allowZeroCap !== true) {
131
+ throw declareError(
132
+ 'SYNTHESIS_ERROR',
133
+ 'maxAmount "0" denies every call: no amount satisfies the bound. Set allowZeroCap to declare that deliberately.'
134
+ )
135
+ }
136
+ const idx = d.amountArgIndex ?? SEP41_AMOUNT_ARG
137
+ if (d.amountArgIndex === undefined) {
138
+ warnings.push(
139
+ `amount cap bound to call_arg(${idx}), the SEP-41 \`transfer\` position. If \`${d.fn}\` carries the amount elsewhere this caps the wrong argument - pass amountArgIndex.`
140
+ )
141
+ }
142
+ children.push({
143
+ op: 'lte',
144
+ left: { kind: 'call_arg', index: idx },
145
+ right: { kind: 'literal_i128', value: d.maxAmount },
146
+ })
147
+ }
148
+
149
+ if (d.minOutputRatio !== undefined) {
150
+ const { num, den, inputArgIndex, outputArgIndex } = d.minOutputRatio
151
+ if (!/^[0-9]+$/.test(num) || !/^[0-9]+$/.test(den)) {
152
+ throw declareError(
153
+ 'SYNTHESIS_ERROR',
154
+ `minOutputRatio num/den must be unsigned integers, got "${num}"/"${den}" (a 1% slippage tolerance is num "99", den "100")`
155
+ )
156
+ }
157
+ // Both are refused on chain at install (INVALID_SCALED_RATIO). Refusing
158
+ // here too means the caller learns before a transaction is built.
159
+ if (den === '0') {
160
+ throw declareError('SYNTHESIS_ERROR', 'minOutputRatio.den is zero: the ratio has no value')
161
+ }
162
+ if (num === '0') {
163
+ throw declareError(
164
+ 'SYNTHESIS_ERROR',
165
+ 'minOutputRatio.num is zero: the floor would be zero, which constrains nothing. Omit it instead.'
166
+ )
167
+ }
168
+ if (inputArgIndex === outputArgIndex) {
169
+ throw declareError(
170
+ 'SYNTHESIS_ERROR',
171
+ `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.`
172
+ )
173
+ }
174
+ if (BigInt(num) > BigInt(den)) {
175
+ // Demanding MORE out than went in is not slippage protection; it is a
176
+ // rule that denies every honest trade. Loud beats a policy that never
177
+ // permits.
178
+ warnings.push(
179
+ `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.`
180
+ )
181
+ }
182
+ children.push({
183
+ op: 'gte',
184
+ left: { kind: 'call_arg', index: outputArgIndex },
185
+ right: { kind: 'call_arg_scaled', index: inputArgIndex, num, den },
186
+ })
187
+ }
188
+
189
+ // A single conjunct is emitted bare. `and` with one child encodes to
190
+ // different bytes than the child alone, and the extra node buys nothing.
191
+ const predicate: PredicateNode =
192
+ children.length === 1 ? (children[0] as PredicateNode) : { op: 'and', children }
193
+ return { predicate, warnings }
194
+ }
195
+
196
+ function declareError(code: ToolError['code'], message: string): ToolError {
197
+ const err = new Error(message) as Error & {
198
+ code: ToolError['code']
199
+ severity: string
200
+ retryable: boolean
201
+ }
202
+ err.code = code
203
+ err.severity = 'error'
204
+ err.retryable = false
205
+ throw err
206
+ }
@@ -9,6 +9,11 @@ export {
9
9
  type ComposeResult,
10
10
  composeFromRecording,
11
11
  } from './compose-from-recording.ts'
12
+ export {
13
+ type DeclaredPredicate,
14
+ declarePredicate,
15
+ type PolicyDeclaration,
16
+ } from './declare.ts'
12
17
  export { type IntentFacts, lower } from './lower.ts'
13
18
  export {
14
19
  type DecideScopeOptions,
@@ -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/src/types.ts CHANGED
@@ -105,7 +105,7 @@ export type PolicyRef = {
105
105
  * `contracts/policy-interpreter/src/version.rs`. Every value this package puts on
106
106
  * the wire derives from here; `grammar-version-parity.test.ts` pins it to the
107
107
  * contract so a skew cannot pass a green test run. */
108
- export const GRAMMAR_VERSION = 3 as const
108
+ export const GRAMMAR_VERSION = 4 as const
109
109
 
110
110
  /** A serialised predicate document stored against a (smart_account, rule_id) pair. */
111
111
  export interface PolicyDocument {
@@ -128,8 +128,15 @@ export interface PolicyDocument {
128
128
  /** The versioned predicate AST (interpreter side). Tagged union. */
129
129
  export type PredicateNode =
130
130
  | { op: 'and'; children: PredicateNode[] }
131
+ // Permits when ANY child permits. Children are canonically sorted like
132
+ // `and`, so authoring order is NOT preserved; when every branch denies, the
133
+ // contract reports the first branch's reason in wire order.
134
+ | { op: 'or'; children: PredicateNode[] }
131
135
  | { op: 'eq'; left: PredicateLeaf; right: PredicateLeaf }
136
+ | { op: 'lt'; left: PredicateLeaf; right: PredicateLeaf }
132
137
  | { op: 'lte'; left: PredicateLeaf; right: PredicateLeaf }
138
+ | { op: 'gt'; left: PredicateLeaf; right: PredicateLeaf }
139
+ | { op: 'gte'; left: PredicateLeaf; right: PredicateLeaf }
133
140
  | {
134
141
  op: 'in'
135
142
  needle: PredicateLeaf
@@ -150,6 +157,14 @@ export type PredicateLeaf =
150
157
  // recorded ScVal type. A non-vec arg, missing element, missing field, or
151
158
  // type mismatch all DENY (fail-closed).
152
159
  | { kind: 'call_arg_field'; index: number; element: number; field: string }
160
+ // `args[index] * num / den`, truncating toward zero. The one leaf whose
161
+ // value is COMPUTED from the call rather than read from it, and the only
162
+ // selector allowed on the right of a comparison - which is what lets a swap
163
+ // bound its output against its own input instead of against a constant that
164
+ // would pin the policy to one trade size. `num`/`den` are i128 decimal
165
+ // strings. Install refuses a zero or non-positive ratio (214), because a
166
+ // negative ratio inverts the comparison.
167
+ | { kind: 'call_arg_scaled'; index: number; num: string; den: string }
153
168
  // Literal leaves: bare ScVal on the wire (no selector-tuple wrapper). Right-hand side of
154
169
  // comparisons, elements of `in` haystacks, and operands to future arithmetic nodes.
155
170
  | { kind: 'literal_address'; value: string }