@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.
- package/dist/adapters/interpreter/adapter.d.ts +2 -2
- package/dist/adapters/interpreter/adapter.js +11 -3
- package/dist/errors.d.ts +6 -1
- package/dist/install/authority-overlap.d.ts +101 -0
- package/dist/install/authority-overlap.js +239 -0
- package/dist/install/build-add-context-rule.d.ts +1 -1
- package/dist/install/index.d.ts +1 -0
- package/dist/install/index.js +4 -0
- package/dist/install/read-account-rules.d.ts +79 -0
- package/dist/install/read-account-rules.js +241 -0
- package/dist/predicate/decode.js +22 -1
- package/dist/predicate/encode.js +52 -5
- package/dist/predicate/from-json.js +14 -1
- package/dist/record/index.d.ts +10 -0
- package/dist/record/index.js +32 -1
- package/dist/record/rpc.d.ts +4 -0
- package/dist/record/rpc.js +4 -1
- package/dist/registry/identify.d.ts +10 -1
- package/dist/registry/identify.js +4 -1
- package/dist/registry/on-chain-spec.d.ts +37 -0
- package/dist/registry/on-chain-spec.js +152 -0
- package/dist/review-card/builder.js +40 -0
- package/dist/review-card/cross-check.js +34 -0
- package/dist/review-card/render-leaf.d.ts +1 -1
- package/dist/review-card/render-leaf.js +7 -0
- package/dist/run/index.d.ts +25 -4
- package/dist/run/index.js +108 -4
- package/dist/run/schemas.d.ts +350 -6
- package/dist/run/schemas.js +95 -6
- package/dist/simulate/deny-cases.js +11 -0
- package/dist/simulate/evaluate.js +86 -5
- package/dist/synth/declare.d.ts +43 -0
- package/dist/synth/declare.js +127 -0
- package/dist/synth/index.d.ts +1 -0
- package/dist/synth/index.js +1 -0
- package/dist/synth/synthesize-from-recording.js +1 -1
- package/dist/types.d.ts +21 -1
- package/dist/types.js +1 -1
- package/dist-cjs/adapters/interpreter/adapter.d.ts +2 -2
- package/dist-cjs/adapters/interpreter/adapter.js +11 -3
- package/dist-cjs/errors.d.ts +6 -1
- package/dist-cjs/install/authority-overlap.d.ts +101 -0
- package/dist-cjs/install/authority-overlap.js +248 -0
- package/dist-cjs/install/build-add-context-rule.d.ts +1 -1
- package/dist-cjs/install/index.d.ts +1 -0
- package/dist-cjs/install/index.js +13 -2
- package/dist-cjs/install/read-account-rules.d.ts +79 -0
- package/dist-cjs/install/read-account-rules.js +252 -0
- package/dist-cjs/predicate/decode.js +22 -1
- package/dist-cjs/predicate/encode.js +52 -5
- package/dist-cjs/predicate/from-json.js +14 -1
- package/dist-cjs/record/index.d.ts +10 -0
- package/dist-cjs/record/index.js +31 -0
- package/dist-cjs/record/rpc.d.ts +4 -0
- package/dist-cjs/record/rpc.js +7 -3
- package/dist-cjs/registry/identify.d.ts +10 -1
- package/dist-cjs/registry/identify.js +4 -0
- package/dist-cjs/registry/on-chain-spec.d.ts +37 -0
- package/dist-cjs/registry/on-chain-spec.js +159 -0
- package/dist-cjs/review-card/builder.js +40 -0
- package/dist-cjs/review-card/cross-check.js +34 -0
- package/dist-cjs/review-card/render-leaf.d.ts +1 -1
- package/dist-cjs/review-card/render-leaf.js +7 -0
- package/dist-cjs/run/index.d.ts +25 -4
- package/dist-cjs/run/index.js +108 -2
- package/dist-cjs/run/schemas.d.ts +350 -6
- package/dist-cjs/run/schemas.js +96 -7
- package/dist-cjs/simulate/deny-cases.js +11 -0
- package/dist-cjs/simulate/evaluate.js +86 -5
- package/dist-cjs/synth/declare.d.ts +43 -0
- package/dist-cjs/synth/declare.js +130 -0
- package/dist-cjs/synth/index.d.ts +1 -0
- package/dist-cjs/synth/index.js +3 -1
- package/dist-cjs/synth/synthesize-from-recording.js +1 -1
- package/dist-cjs/types.d.ts +21 -1
- package/dist-cjs/types.js +1 -1
- package/package.json +1 -1
- package/src/adapters/interpreter/adapter.ts +13 -5
- package/src/errors.ts +5 -0
- package/src/install/authority-overlap.ts +323 -0
- package/src/install/index.ts +20 -0
- package/src/install/read-account-rules.ts +313 -0
- package/src/predicate/decode.ts +22 -1
- package/src/predicate/encode.ts +55 -5
- package/src/predicate/from-json.ts +14 -1
- package/src/record/index.ts +59 -2
- package/src/record/rpc.ts +4 -1
- package/src/registry/identify.ts +4 -1
- package/src/registry/on-chain-spec.ts +168 -0
- package/src/review-card/builder.ts +45 -2
- package/src/review-card/cross-check.ts +35 -1
- package/src/review-card/render-leaf.ts +8 -1
- package/src/run/index.ts +125 -2
- package/src/run/schemas.ts +100 -6
- package/src/simulate/deny-cases.ts +12 -1
- package/src/simulate/evaluate.ts +101 -9
- package/src/synth/declare.ts +206 -0
- package/src/synth/index.ts +5 -0
- package/src/synth/synthesize-from-recording.ts +1 -1
- 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
|
+
}
|
package/src/synth/index.ts
CHANGED
|
@@ -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
|
|
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 =
|
|
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 }
|