@crediolabs/policy-synth 0.1.16 → 0.1.18
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/README.md +43 -246
- package/dist/adapters/interpreter/adapter.d.ts +6 -7
- package/dist/adapters/interpreter/adapter.js +28 -48
- package/dist/adapters/oz/adapter.js +12 -13
- package/dist/codegen/compile-gate.js +5 -0
- package/dist/codegen/template.js +17 -0
- package/dist/install/build-add-context-rule.js +16 -45
- package/dist/install/build-install-policy.d.ts +12 -5
- package/dist/install/build-install-policy.js +32 -23
- package/dist/install/get-interpreter-info.js +2 -2
- package/dist/install/index.d.ts +1 -0
- package/dist/install/index.js +15 -0
- package/dist/install/oz-auth.js +5 -3
- package/dist/predicate/decode.js +1 -1
- package/dist/predicate/encode.js +171 -10
- package/dist/record/decode.js +5 -6
- package/dist/registry/protocols.d.ts +1 -1
- package/dist/registry/protocols.js +1 -1
- package/dist/review-card/builder.d.ts +6 -0
- package/dist/review-card/builder.js +9 -1
- package/dist/run/index.d.ts +14 -13
- package/dist/run/index.js +80 -67
- package/dist/run/schemas.d.ts +193 -11
- package/dist/run/schemas.js +59 -17
- package/dist/synth/compose-from-recording.d.ts +10 -15
- package/dist/synth/compose-from-recording.js +79 -124
- package/dist/synth/deny-cases.d.ts +5 -0
- package/dist/synth/deny-cases.js +52 -13
- package/dist/synth/evaluate.js +69 -119
- package/dist/synth/harness.d.ts +13 -1
- package/dist/synth/harness.js +22 -1
- package/dist/synth/index.d.ts +1 -1
- package/dist/synth/synthesize-from-recording.d.ts +31 -21
- package/dist/synth/synthesize-from-recording.js +129 -155
- package/dist-cjs/adapters/interpreter/adapter.d.ts +6 -7
- package/dist-cjs/adapters/interpreter/adapter.js +28 -48
- package/dist-cjs/adapters/oz/adapter.js +12 -13
- package/dist-cjs/codegen/compile-gate.js +5 -0
- package/dist-cjs/codegen/template.js +17 -0
- package/dist-cjs/install/build-add-context-rule.js +16 -45
- package/dist-cjs/install/build-install-policy.d.ts +12 -5
- package/dist-cjs/install/build-install-policy.js +32 -23
- package/dist-cjs/install/get-interpreter-info.js +2 -2
- package/dist-cjs/install/index.d.ts +1 -0
- package/dist-cjs/install/index.js +21 -0
- package/dist-cjs/install/oz-auth.js +5 -3
- package/dist-cjs/predicate/decode.js +1 -1
- package/dist-cjs/predicate/encode.js +171 -10
- package/dist-cjs/record/decode.js +5 -6
- package/dist-cjs/registry/protocols.d.ts +1 -1
- package/dist-cjs/registry/protocols.js +1 -1
- package/dist-cjs/review-card/builder.d.ts +6 -0
- package/dist-cjs/review-card/builder.js +9 -1
- package/dist-cjs/run/index.d.ts +14 -13
- package/dist-cjs/run/index.js +80 -67
- package/dist-cjs/run/schemas.d.ts +193 -11
- package/dist-cjs/run/schemas.js +59 -17
- package/dist-cjs/synth/compose-from-recording.d.ts +10 -15
- package/dist-cjs/synth/compose-from-recording.js +79 -124
- package/dist-cjs/synth/deny-cases.d.ts +5 -0
- package/dist-cjs/synth/deny-cases.js +52 -13
- package/dist-cjs/synth/evaluate.js +69 -119
- package/dist-cjs/synth/harness.d.ts +13 -1
- package/dist-cjs/synth/harness.js +22 -1
- package/dist-cjs/synth/index.d.ts +1 -1
- package/dist-cjs/synth/synthesize-from-recording.d.ts +31 -21
- package/dist-cjs/synth/synthesize-from-recording.js +129 -153
- package/package.json +11 -4
- package/src/adapters/interpreter/adapter.ts +28 -48
- package/src/adapters/oz/adapter.ts +12 -13
- package/src/codegen/compile-gate.ts +5 -0
- package/src/codegen/template.ts +17 -0
- package/src/install/build-add-context-rule.ts +17 -55
- package/src/install/build-install-policy.ts +41 -23
- package/src/install/get-interpreter-info.ts +2 -2
- package/src/install/index.ts +21 -0
- package/src/install/oz-auth.ts +5 -3
- package/src/predicate/decode.ts +1 -1
- package/src/predicate/encode.ts +176 -10
- package/src/record/decode.ts +5 -6
- package/src/registry/protocols.ts +1 -1
- package/src/review-card/builder.ts +17 -1
- package/src/run/index.ts +117 -91
- package/src/run/schemas.ts +64 -16
- package/src/synth/compose-from-recording.ts +87 -132
- package/src/synth/deny-cases.ts +59 -13
- package/src/synth/evaluate.ts +70 -118
- package/src/synth/harness.ts +29 -1
- package/src/synth/index.ts +1 -0
- package/src/synth/synthesize-from-recording.ts +209 -192
- package/src/contracts/policy-template/OZ_POLICY_TRAIT.md +0 -196
package/dist/synth/evaluate.js
CHANGED
|
@@ -1,33 +1,23 @@
|
|
|
1
|
-
// src/synth/evaluate.ts - the model evaluator.
|
|
1
|
+
// src/synth/evaluate.ts - the model evaluator (TS model of interpreter semantics).
|
|
2
2
|
//
|
|
3
|
-
// Pure function.
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// Pure function. Determinism: same `(predicate, ctx)` -> byte-identical result,
|
|
4
|
+
// no clock, no randomness. Deny order (deny on FIRST violation, stable reason):
|
|
5
|
+
// 1. ledger expiry -> 'EXPIRED'
|
|
6
|
+
// 2. `now` past valid_until -> 'EXPIRED'
|
|
7
|
+
// 3. contract mismatch -> 'CONTRACT_SCOPE'
|
|
8
|
+
// 4. per-ScVal equality on fn/args; EXACT ordered vector
|
|
9
|
+
// equality; fail-closed on opaque args -> 'ARG_MISMATCH' / 'FN_MISMATCH'
|
|
10
|
+
// 5. `in` membership; empty haystack ALWAYS denies -> 'NOT_IN_ALLOWLIST'
|
|
11
|
+
// 6. amount / window_spent via BigInt -> 'AMOUNT_BOUND'
|
|
12
|
+
// 7. invocation_count_in_window -> 'FREQUENCY'
|
|
13
|
+
// 8. oracle_price: fatal -> throw OracleError, deny with
|
|
14
|
+
// underlying ORACLE_* reason -> 'ORACLE_*'
|
|
15
|
+
// 9. boolean nodes (and/or/not)
|
|
16
|
+
// 10. signer threshold gate -> 'THRESHOLD_NOT_MET'
|
|
17
|
+
// 11. otherwise permit.
|
|
7
18
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// 2. a `now` leaf compared past `valid_until` semantics -> 'EXPIRED'
|
|
11
|
-
// 3. a `call_contract` eq whose literal != ctx.contract -> 'CONTRACT_SCOPE'
|
|
12
|
-
// 4. per-ScVal equality on `call_fn` / `call_arg[i]`;
|
|
13
|
-
// `eq(selector, literal_vec)` = EXACT ordered vector equality
|
|
14
|
-
// (compare element-by-element, deny if length or any differs);
|
|
15
|
-
// fail-closed on opaque/undecodable args -> 'ARG_MISMATCH' / 'FN_MISMATCH'
|
|
16
|
-
// 5. `in` membership: needle NOT in haystack; an EMPTY
|
|
17
|
-
// haystack ALWAYS denies -> 'NOT_IN_ALLOWLIST'
|
|
18
|
-
// 6. amount / window_spent comparisons via BigInt -> 'AMOUNT_BOUND'
|
|
19
|
-
// 7. `invocation_count_in_window` compare -> 'FREQUENCY'
|
|
20
|
-
// 8. `oracle_price`: missing/stale/deviation/etc. is FATAL -
|
|
21
|
-
// throw `OracleError`, catch at the top, deny with the
|
|
22
|
-
// underlying `ORACLE_*` reason -> 'ORACLE_*'
|
|
23
|
-
// 9. boolean nodes: `and` = all children permit;
|
|
24
|
-
// `or` = any child permits; `not` = invert a non-oracle child
|
|
25
|
-
// 10. signer threshold gate (when `signerWeights` provided,
|
|
26
|
-
// no positive-weight signer -> deny 'THRESHOLD_NOT_MET')
|
|
27
|
-
// 11. otherwise `{ permit: true }`.
|
|
28
|
-
//
|
|
29
|
-
// Amounts: BigInt on decimal strings (never JS `number`). Oracle errors
|
|
30
|
-
// modelled by throwing (never a boolean-false that `not`/`or` could mask).
|
|
19
|
+
// Amounts: BigInt on decimal strings. Oracle errors thrown (never
|
|
20
|
+
// boolean-false `not`/`or` could mask).
|
|
31
21
|
import { literalNumericBigInt } from "./predicate-literals.js";
|
|
32
22
|
/** Oracle prices normalise to this many decimals; mirrors NORMALISED_DECIMALS
|
|
33
23
|
* in oracle.rs. A threshold on any other basis must say so. */
|
|
@@ -35,8 +25,8 @@ const NORMALISED_DECIMALS = 9;
|
|
|
35
25
|
/** Mirrors MAX_ORACLE_THRESHOLD_DECIMALS in dsl.rs. */
|
|
36
26
|
const MAX_ORACLE_THRESHOLD_DECIMALS = 18;
|
|
37
27
|
/** Internal fatal thrown by the oracle path; caught at the top of `evaluate`
|
|
38
|
-
* and converted to the matching `ORACLE_*` deny reason. NOT a
|
|
39
|
-
*
|
|
28
|
+
* and converted to the matching `ORACLE_*` deny reason. NOT a boolean-false
|
|
29
|
+
* that `not` / `or` could mask. */
|
|
40
30
|
class OracleError extends Error {
|
|
41
31
|
code;
|
|
42
32
|
constructor(code) {
|
|
@@ -90,12 +80,9 @@ function walk(node, ctx) {
|
|
|
90
80
|
for (const child of node.children) {
|
|
91
81
|
const r = walk(child, ctx);
|
|
92
82
|
if (!r.permit) {
|
|
93
|
-
// deny-on-first: short-circuit
|
|
94
|
-
// child, so the reason we surface is from that child.
|
|
83
|
+
// deny-on-first: short-circuit on the failing child.
|
|
95
84
|
return r;
|
|
96
85
|
}
|
|
97
|
-
// keep a reference to the last permit so unused-variable analysis
|
|
98
|
-
// doesn't complain; this is purely structural.
|
|
99
86
|
lastDeny = r;
|
|
100
87
|
}
|
|
101
88
|
return lastDeny ?? { permit: true };
|
|
@@ -111,11 +98,9 @@ function walk(node, ctx) {
|
|
|
111
98
|
return lastDeny ?? { permit: false, reason: 'NOT_IN_ALLOWLIST' };
|
|
112
99
|
}
|
|
113
100
|
case 'not': {
|
|
114
|
-
// `not` structurally inverts the child
|
|
115
|
-
// oracle leaf (compile-time rule: no oracle leaf under not/or).
|
|
116
|
-
//
|
|
117
|
-
// `evaluate` re-throws and surfaces the ORACLE_* deny. A non-oracle
|
|
118
|
-
// deny is inverted to a permit, and a permit is inverted to a deny.
|
|
101
|
+
// `not` structurally inverts the child unless the child contains an
|
|
102
|
+
// oracle leaf (compile-time rule: no oracle leaf under not/or). A
|
|
103
|
+
// child `OracleError` is re-thrown to the catch at the top of `evaluate`.
|
|
119
104
|
const r = walk(node.child, ctx);
|
|
120
105
|
if (r.permit)
|
|
121
106
|
return { permit: false, reason: 'FN_MISMATCH' };
|
|
@@ -136,17 +121,14 @@ function evalCompare(op, left, right, ctx) {
|
|
|
136
121
|
// --- step 2: `now` vs `valid_until` semantics ---
|
|
137
122
|
if (left.kind === 'now' && right.kind === 'valid_until') {
|
|
138
123
|
const expired = op === 'gt' || op === 'gte' ? ctx.nowSeconds >= 0 : ctx.nowSeconds < 0;
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
//
|
|
142
|
-
// `nowSeconds` so the only true-positive expired path is the
|
|
143
|
-
// `gt`/`gte` shapes callers actually write.
|
|
124
|
+
// `valid_until` is modelled as a synthetic future timestamp far past
|
|
125
|
+
// `nowSeconds` so the only true-positive expired path is the `gt`/`gte`
|
|
126
|
+
// shapes callers actually write.
|
|
144
127
|
if (op === 'gt' || op === 'gte') {
|
|
145
128
|
if (ctx.nowSeconds > 0)
|
|
146
129
|
return { permit: false, reason: 'EXPIRED' };
|
|
147
130
|
}
|
|
148
131
|
else if (op === 'lt' || op === 'lte') {
|
|
149
|
-
// permits when valid_until is in the future
|
|
150
132
|
return { permit: true };
|
|
151
133
|
}
|
|
152
134
|
return expired ? { permit: false, reason: 'EXPIRED' } : { permit: true };
|
|
@@ -178,11 +160,6 @@ function evalCompare(op, left, right, ctx) {
|
|
|
178
160
|
return evalScaledArgCompare(op, left, right, ctx);
|
|
179
161
|
}
|
|
180
162
|
const actual = ctx.args[left.index];
|
|
181
|
-
// An ordered comparison (lt/lte/gt/gte) reads the arg as an integer and
|
|
182
|
-
// compares it to a numeric literal via BigInt (e.g. a SoroSwap input-amount
|
|
183
|
-
// cap `call_arg[0] <= limit`). The per-ScVal-type equality semantics live
|
|
184
|
-
// in evalArgEq (`eq` only); ordered ops are numeric-only and fail closed on
|
|
185
|
-
// a non-numeric arg or literal.
|
|
186
163
|
if (op !== 'eq')
|
|
187
164
|
return evalArgOrderedCompare(op, actual, right);
|
|
188
165
|
return evalArgEq(op, actual, right, ctx);
|
|
@@ -192,11 +169,11 @@ function evalCompare(op, left, right, ctx) {
|
|
|
192
169
|
if (left.kind === 'call_arg_scaled') {
|
|
193
170
|
return evalScaledArgCompare(op, right, left, ctx);
|
|
194
171
|
}
|
|
195
|
-
// --- step 4c: call_arg_len:
|
|
196
|
-
// Fails closed on a non-vec
|
|
172
|
+
// --- step 4c: call_arg_len: length of a vec-typed argument as u32.
|
|
173
|
+
// Fails closed on a non-vec / absent arg or a non-u32 literal.
|
|
197
174
|
if (left.kind === 'call_arg_len') {
|
|
198
175
|
const actual = ctx.args[left.index];
|
|
199
|
-
if (
|
|
176
|
+
if (actual?.type !== 'vec')
|
|
200
177
|
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
201
178
|
if (right.kind !== 'literal_u32')
|
|
202
179
|
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
@@ -204,16 +181,14 @@ function evalCompare(op, left, right, ctx) {
|
|
|
204
181
|
? { permit: true }
|
|
205
182
|
: { permit: false, reason: 'ARG_MISMATCH' };
|
|
206
183
|
}
|
|
207
|
-
// --- step 4d: call_arg_field:
|
|
208
|
-
//
|
|
209
|
-
// out-of-range element, a missing field, a non-map element, or a type
|
|
210
|
-
// mismatch between the field ScVal and the literal leaf.
|
|
184
|
+
// --- step 4d: call_arg_field: value of a field in the map at element i of
|
|
185
|
+
// the vec at argument index. Fails closed on shape / type / range issues.
|
|
211
186
|
if (left.kind === 'call_arg_field') {
|
|
212
187
|
const actual = ctx.args[left.index];
|
|
213
|
-
if (
|
|
188
|
+
if (actual?.type !== 'vec')
|
|
214
189
|
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
215
190
|
const element = actual.value[left.element];
|
|
216
|
-
if (
|
|
191
|
+
if (element?.type !== 'map')
|
|
217
192
|
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
218
193
|
if (!Array.isArray(element.value))
|
|
219
194
|
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
@@ -226,7 +201,7 @@ function evalCompare(op, left, right, ctx) {
|
|
|
226
201
|
}
|
|
227
202
|
// --- step 6: AMOUNT_BOUND ---
|
|
228
203
|
if (left.kind === 'amount' && op !== 'eq') {
|
|
229
|
-
return evalAmountCompare(op, left.token, right, ctx);
|
|
204
|
+
return evalAmountCompare(op, left.token, right, ctx, 'amountByToken');
|
|
230
205
|
}
|
|
231
206
|
if (left.kind === 'window_spent' && op !== 'eq') {
|
|
232
207
|
return evalWindowSpentCompare(op, left.token, right, ctx);
|
|
@@ -247,7 +222,7 @@ function evalCompare(op, left, right, ctx) {
|
|
|
247
222
|
* sequence: compare element-by-element in order; deny if length or any
|
|
248
223
|
* element differs. Opaque args (`type: 'other'`) fail closed. */
|
|
249
224
|
function evalArgEq(op, actual, right, ctx) {
|
|
250
|
-
// eq(call_arg[i], literal_vec) -> EXACT ordered vector equality
|
|
225
|
+
// eq(call_arg[i], literal_vec) -> EXACT ordered vector equality.
|
|
251
226
|
if (op === 'eq' && right.kind === 'literal_vec') {
|
|
252
227
|
if (actual?.type !== 'vec')
|
|
253
228
|
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
@@ -255,7 +230,6 @@ function evalArgEq(op, actual, right, ctx) {
|
|
|
255
230
|
? { permit: true }
|
|
256
231
|
: { permit: false, reason: 'ARG_MISMATCH' };
|
|
257
232
|
}
|
|
258
|
-
// eq(call_arg[i], literal_address) -> address compare
|
|
259
233
|
if (op === 'eq' && right.kind === 'literal_address') {
|
|
260
234
|
if (!actual)
|
|
261
235
|
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
@@ -265,7 +239,6 @@ function evalArgEq(op, actual, right, ctx) {
|
|
|
265
239
|
? { permit: true }
|
|
266
240
|
: { permit: false, reason: 'ARG_MISMATCH' };
|
|
267
241
|
}
|
|
268
|
-
// eq(call_arg[i], literal_i128) -> BigInt compare
|
|
269
242
|
if (op === 'eq' && right.kind === 'literal_i128') {
|
|
270
243
|
if (actual?.type !== 'i128')
|
|
271
244
|
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
@@ -273,7 +246,6 @@ function evalArgEq(op, actual, right, ctx) {
|
|
|
273
246
|
? { permit: true }
|
|
274
247
|
: { permit: false, reason: 'ARG_MISMATCH' };
|
|
275
248
|
}
|
|
276
|
-
// eq(call_arg[i], literal_symbol)
|
|
277
249
|
if (op === 'eq' && right.kind === 'literal_symbol') {
|
|
278
250
|
if (!actual)
|
|
279
251
|
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
@@ -281,7 +253,6 @@ function evalArgEq(op, actual, right, ctx) {
|
|
|
281
253
|
? { permit: true }
|
|
282
254
|
: { permit: false, reason: 'ARG_MISMATCH' };
|
|
283
255
|
}
|
|
284
|
-
// eq(call_arg[i], literal_u32)
|
|
285
256
|
if (op === 'eq' && right.kind === 'literal_u32') {
|
|
286
257
|
if (!actual)
|
|
287
258
|
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
@@ -289,7 +260,6 @@ function evalArgEq(op, actual, right, ctx) {
|
|
|
289
260
|
? { permit: true }
|
|
290
261
|
: { permit: false, reason: 'ARG_MISMATCH' };
|
|
291
262
|
}
|
|
292
|
-
// eq(call_arg[i], literal_u64)
|
|
293
263
|
if (op === 'eq' && right.kind === 'literal_u64') {
|
|
294
264
|
if (!actual)
|
|
295
265
|
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
@@ -297,7 +267,6 @@ function evalArgEq(op, actual, right, ctx) {
|
|
|
297
267
|
? { permit: true }
|
|
298
268
|
: { permit: false, reason: 'ARG_MISMATCH' };
|
|
299
269
|
}
|
|
300
|
-
// eq(call_arg[i], literal_bytes)
|
|
301
270
|
if (op === 'eq' && right.kind === 'literal_bytes') {
|
|
302
271
|
if (!actual)
|
|
303
272
|
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
@@ -310,20 +279,14 @@ function evalArgEq(op, actual, right, ctx) {
|
|
|
310
279
|
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
311
280
|
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
312
281
|
}
|
|
313
|
-
/** Step 4b': slippage-floor comparison. Mirrors the Rust `eval_scaled_arg_compare
|
|
314
|
-
*
|
|
315
|
-
* zero
|
|
316
|
-
*
|
|
317
|
-
*
|
|
318
|
-
*
|
|
319
|
-
*
|
|
320
|
-
* `left` is whichever side of the compare is NOT the scaled leaf. A
|
|
321
|
-
* scaled-on-scaled compare denies `ARG_MISMATCH` (pipelining two scaled
|
|
322
|
-
* leaves has no definable semantics). The other operand must be a
|
|
323
|
-
* numeric shape (`call_arg` carrying a number, or a numeric literal);
|
|
324
|
-
* anything else is `ARG_MISMATCH`. */
|
|
282
|
+
/** Step 4b': slippage-floor comparison. Mirrors the Rust `eval_scaled_arg_compare`:
|
|
283
|
+
* the scaled leaf is `args[index] * num / den` (truncating toward zero). On
|
|
284
|
+
* overflow or divide-by-zero deny with `ARITHMETIC_OVERFLOW`; a failed bound
|
|
285
|
+
* denies with `SLIPPAGE_FLOOR` (the dedicated reason) rather than the generic
|
|
286
|
+
* `ARG_MISMATCH`. A scaled-on-scaled compare denies `ARG_MISMATCH`
|
|
287
|
+
* (no definable semantics). The non-scaled operand must be numeric
|
|
288
|
+
* (`call_arg` carrying a number, or a numeric literal); else `ARG_MISMATCH`. */
|
|
325
289
|
function evalScaledArgCompare(op, left, right, ctx) {
|
|
326
|
-
// Identify which side is scaled, and pull the other side as a number.
|
|
327
290
|
let scaled;
|
|
328
291
|
let other;
|
|
329
292
|
let scaledOnRight;
|
|
@@ -338,14 +301,12 @@ function evalScaledArgCompare(op, left, right, ctx) {
|
|
|
338
301
|
scaledOnRight = true;
|
|
339
302
|
}
|
|
340
303
|
else {
|
|
341
|
-
//
|
|
342
|
-
// dispatcher; the contract returns ARG_MISMATCH to fail closed.
|
|
304
|
+
// Programming error in the dispatcher; fail closed.
|
|
343
305
|
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
344
306
|
}
|
|
345
|
-
//
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
// not be read.
|
|
307
|
+
// Out-of-bounds or non-numeric arg fails closed as ARG_MISMATCH (not
|
|
308
|
+
// SLIPPAGE_FLOOR) - a violated floor is the wrong code when the operand
|
|
309
|
+
// itself could not be read.
|
|
349
310
|
if (scaled.index >= ctx.args.length) {
|
|
350
311
|
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
351
312
|
}
|
|
@@ -362,16 +323,11 @@ function evalScaledArgCompare(op, left, right, ctx) {
|
|
|
362
323
|
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
363
324
|
}
|
|
364
325
|
// Install refuses `den == 0` and `num <= 0` / `den <= 0`. The runtime
|
|
365
|
-
// check is
|
|
366
|
-
//
|
|
326
|
+
// check is defensive belt-and-braces so a validator regression cannot
|
|
327
|
+
// panic the frame on a divide-by-zero.
|
|
367
328
|
if (den === 0n)
|
|
368
329
|
return { permit: false, reason: 'ARITHMETIC_OVERFLOW' };
|
|
369
330
|
const product = input * num;
|
|
370
|
-
// BigInt overflow is silent in JS (it does not throw on multiplication).
|
|
371
|
-
// We can detect over-range by dividing and checking the result against
|
|
372
|
-
// i128 bounds, but the more practical check is whether the result
|
|
373
|
-
// divides cleanly. We rely on BigInt's arbitrary precision and then
|
|
374
|
-
// surface ARITHMETIC_OVERFLOW if the result is outside i128.
|
|
375
331
|
let scaledValue;
|
|
376
332
|
try {
|
|
377
333
|
// BigInt division truncates toward zero (matches Rust `i128::checked_div`).
|
|
@@ -380,18 +336,13 @@ function evalScaledArgCompare(op, left, right, ctx) {
|
|
|
380
336
|
catch {
|
|
381
337
|
return { permit: false, reason: 'ARITHMETIC_OVERFLOW' };
|
|
382
338
|
}
|
|
383
|
-
// i128 range check:
|
|
384
|
-
//
|
|
339
|
+
// i128 range check: the contract would have wrapped on i128 arithmetic;
|
|
340
|
+
// surface the same deny.
|
|
385
341
|
const I128_MAX = (1n << 127n) - 1n;
|
|
386
342
|
const I128_MIN = -(1n << 127n);
|
|
387
343
|
if (scaledValue > I128_MAX || scaledValue < I128_MIN) {
|
|
388
344
|
return { permit: false, reason: 'ARITHMETIC_OVERFLOW' };
|
|
389
345
|
}
|
|
390
|
-
// Resolving the operand when the scaled side is on the right: the
|
|
391
|
-
// operand is `left`; the scaled value is the RHS. The AST order is
|
|
392
|
-
// `left <op> scaled`, so the comparator applies to `other <op> scaled`.
|
|
393
|
-
// When the scaled side is on the left, the order is `scaled <op> other`,
|
|
394
|
-
// i.e. `scaled <op> other_val`.
|
|
395
346
|
let otherVal;
|
|
396
347
|
if (other.kind === 'call_arg') {
|
|
397
348
|
otherVal = argNumericBigInt(ctx.args[other.index]);
|
|
@@ -459,7 +410,6 @@ function compareVecExact(actual, expected, ctx) {
|
|
|
459
410
|
function evalIn(needle, haystack, ctx) {
|
|
460
411
|
if (haystack.length === 0)
|
|
461
412
|
return { permit: false, reason: 'NOT_IN_ALLOWLIST' };
|
|
462
|
-
// Resolve the needle's ScVal against the candidate call.
|
|
463
413
|
const actual = resolveLeaf(needle, ctx);
|
|
464
414
|
if (!actual || actual.type === 'other')
|
|
465
415
|
return { permit: false, reason: 'NOT_IN_ALLOWLIST' };
|
|
@@ -486,10 +436,10 @@ function resolveLeaf(leaf, ctx) {
|
|
|
486
436
|
return undefined;
|
|
487
437
|
case 'call_arg_field': {
|
|
488
438
|
const actual = ctx.args[leaf.index];
|
|
489
|
-
if (
|
|
439
|
+
if (actual?.type !== 'vec')
|
|
490
440
|
return undefined;
|
|
491
441
|
const element = actual.value[leaf.element];
|
|
492
|
-
if (
|
|
442
|
+
if (element?.type !== 'map')
|
|
493
443
|
return undefined;
|
|
494
444
|
if (!Array.isArray(element.value))
|
|
495
445
|
return undefined;
|
|
@@ -520,25 +470,21 @@ function resolveLeaf(leaf, ctx) {
|
|
|
520
470
|
return undefined;
|
|
521
471
|
}
|
|
522
472
|
}
|
|
523
|
-
/** Step 6: amount compare on BigInt.
|
|
524
|
-
|
|
473
|
+
/** Step 6: amount / window_spent compare on BigInt. Both leaves compare a
|
|
474
|
+
* per-token BigInt record (current vs. rolling) to an i128 literal; the
|
|
475
|
+
* compare + denial reason are identical, so a single helper handles both. */
|
|
476
|
+
function evalAmountCompare(op, token, right, ctx, record) {
|
|
525
477
|
const literal = right.kind === 'literal_i128' ? right.value : null;
|
|
526
478
|
if (literal === null)
|
|
527
479
|
return { permit: false, reason: 'AMOUNT_BOUND' };
|
|
528
|
-
const actual = ctx
|
|
480
|
+
const actual = ctx[record][token] ?? '0';
|
|
529
481
|
return bigintCmp(op, actual, literal)
|
|
530
482
|
? { permit: true }
|
|
531
483
|
: { permit: false, reason: 'AMOUNT_BOUND' };
|
|
532
484
|
}
|
|
533
485
|
/** Step 6: window_spent compare on BigInt. */
|
|
534
486
|
function evalWindowSpentCompare(op, token, right, ctx) {
|
|
535
|
-
|
|
536
|
-
if (literal === null)
|
|
537
|
-
return { permit: false, reason: 'AMOUNT_BOUND' };
|
|
538
|
-
const actual = ctx.windowSpentByToken[token] ?? '0';
|
|
539
|
-
return bigintCmp(op, actual, literal)
|
|
540
|
-
? { permit: true }
|
|
541
|
-
: { permit: false, reason: 'AMOUNT_BOUND' };
|
|
487
|
+
return evalAmountCompare(op, token, right, ctx, 'windowSpentByToken');
|
|
542
488
|
}
|
|
543
489
|
/** Step 7: invocation_count_in_window compare. */
|
|
544
490
|
function evalFrequencyCompare(op, windowSecs, right, ctx) {
|
|
@@ -562,24 +508,28 @@ function evalOracleCompare(op, asset, right, ctx) {
|
|
|
562
508
|
throw new OracleError(mapped);
|
|
563
509
|
}
|
|
564
510
|
// Mirrors eval_oracle_compare in dsl.rs. The threshold MUST declare its
|
|
565
|
-
// decimal basis: prices are on the normalised 9-dp basis, and
|
|
566
|
-
//
|
|
567
|
-
//
|
|
511
|
+
// decimal basis: prices are on the normalised 9-dp basis, and a bare literal
|
|
512
|
+
// is refused (not assumed) because a raw 14-dp threshold that assumed the
|
|
513
|
+
// 9-dp basis would permit everything.
|
|
568
514
|
if (right.kind !== 'oracle_threshold')
|
|
569
515
|
throw new OracleError('ORACLE_DECIMALS_MISMATCH');
|
|
570
516
|
if (right.decimals > MAX_ORACLE_THRESHOLD_DECIMALS) {
|
|
571
517
|
throw new OracleError('ORACLE_THRESHOLD_DECIMALS_OUT_OF_RANGE');
|
|
572
518
|
}
|
|
573
|
-
// Scale BOTH sides up to the wider basis
|
|
574
|
-
//
|
|
519
|
+
// Scale BOTH sides up to the wider basis; dividing the threshold down
|
|
520
|
+
// truncates and moves the permit boundary.
|
|
575
521
|
const decimals = BigInt(right.decimals);
|
|
576
522
|
const normalised = BigInt(NORMALISED_DECIMALS);
|
|
577
523
|
const common = decimals > normalised ? decimals : normalised;
|
|
578
524
|
const priceScaled = BigInt(entry.price) * 10n ** (common - normalised);
|
|
579
525
|
const literalScaled = BigInt(right.value) * 10n ** (common - decimals);
|
|
526
|
+
// A violated oracle threshold is a stateful bound (Rust dsl.rs:276 returns
|
|
527
|
+
// `DenyReason::StatefulBound`, deny code #104). The TS evaluator must mirror
|
|
528
|
+
// the same reason so a future TS/Rust reason divergence fails CI; the
|
|
529
|
+
// cross-layer harness in this file now asserts reason codes match.
|
|
580
530
|
return bigintCmp(op, String(priceScaled), String(literalScaled))
|
|
581
531
|
? { permit: true }
|
|
582
|
-
: { permit: false, reason: '
|
|
532
|
+
: { permit: false, reason: 'STATEFUL_BOUND' };
|
|
583
533
|
}
|
|
584
534
|
/** BigInt compare helper. `eq` is also supported by callers (selector-vs-literal
|
|
585
535
|
* equal checks), but the dedicated arg path uses it via `BigInt(actual) === BigInt(right)`. */
|
package/dist/synth/harness.d.ts
CHANGED
|
@@ -5,6 +5,13 @@ export interface HarnessFailure {
|
|
|
5
5
|
expected: 'permit' | 'deny';
|
|
6
6
|
got: 'permit' | 'deny';
|
|
7
7
|
reason?: string;
|
|
8
|
+
/** When `expectedReason` is set on the deny case and the case actually
|
|
9
|
+
* denies, this carries the canonical reason the TS evaluator MUST emit
|
|
10
|
+
* to stay in lockstep with the Rust interpreter (the cross-layer
|
|
11
|
+
* reason-code contract). A future TS/Rust reason divergence surfaces
|
|
12
|
+
* here in CI. */
|
|
13
|
+
expectedReason?: string;
|
|
14
|
+
actualReason?: string;
|
|
8
15
|
}
|
|
9
16
|
export type HarnessResult = {
|
|
10
17
|
ok: true;
|
|
@@ -12,5 +19,10 @@ export type HarnessResult = {
|
|
|
12
19
|
ok: false;
|
|
13
20
|
failures: HarnessFailure[];
|
|
14
21
|
};
|
|
15
|
-
/** Evaluate the intended call and every generated denial without side effects.
|
|
22
|
+
/** Evaluate the intended call and every generated denial without side effects.
|
|
23
|
+
* Asserts both the boolean decision AND, for deny cases that carry an
|
|
24
|
+
* `expectedReason`, the concrete reason the TS evaluator emits. The reason
|
|
25
|
+
* is what the Rust interpreter returns; the cross-layer conformance test in
|
|
26
|
+
* `contracts/policy-interpreter/tests/conformance` mirrors the same contract
|
|
27
|
+
* for the verifyLive path. */
|
|
16
28
|
export declare function runHarness(predicate: PredicateNode, cases: GeneratedCases): HarnessResult;
|
package/dist/synth/harness.js
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { evaluate } from "./evaluate.js";
|
|
2
|
-
/** Evaluate the intended call and every generated denial without side effects.
|
|
2
|
+
/** Evaluate the intended call and every generated denial without side effects.
|
|
3
|
+
* Asserts both the boolean decision AND, for deny cases that carry an
|
|
4
|
+
* `expectedReason`, the concrete reason the TS evaluator emits. The reason
|
|
5
|
+
* is what the Rust interpreter returns; the cross-layer conformance test in
|
|
6
|
+
* `contracts/policy-interpreter/tests/conformance` mirrors the same contract
|
|
7
|
+
* for the verifyLive path. */
|
|
3
8
|
export function runHarness(predicate, cases) {
|
|
4
9
|
const failures = [];
|
|
5
10
|
const permitResult = evaluate(predicate, cases.permit);
|
|
@@ -20,6 +25,22 @@ export function runHarness(predicate, cases) {
|
|
|
20
25
|
got: 'permit',
|
|
21
26
|
reason: 'DENY_CASE_FAILURE',
|
|
22
27
|
});
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
// Reason-code assertion (cross-layer contract). When a deny case carries
|
|
31
|
+
// an expected reason, the TS evaluator MUST emit that exact string; a
|
|
32
|
+
// divergence means the TS model and the Rust interpreter disagree on
|
|
33
|
+
// what the user sees in CI - the harness fails so the diff is visible
|
|
34
|
+
// here, not at runtime in production.
|
|
35
|
+
if (deny.expectedReason !== undefined && result.reason !== deny.expectedReason) {
|
|
36
|
+
failures.push({
|
|
37
|
+
dimension: deny.dimension,
|
|
38
|
+
expected: 'deny',
|
|
39
|
+
got: 'deny',
|
|
40
|
+
reason: 'REASON_MISMATCH',
|
|
41
|
+
expectedReason: deny.expectedReason,
|
|
42
|
+
actualReason: result.reason,
|
|
43
|
+
});
|
|
23
44
|
}
|
|
24
45
|
}
|
|
25
46
|
return failures.length === 0 ? { ok: true } : { ok: false, failures };
|
package/dist/synth/index.d.ts
CHANGED
|
@@ -7,4 +7,4 @@ export { type IntentFacts, lower } from './lower.ts';
|
|
|
7
7
|
export { minimize } from './minimize.ts';
|
|
8
8
|
export { type DecideScopeOptions, decideScope, type ScopeDecision, scopeToContextRuleType, } from './scope.ts';
|
|
9
9
|
export { synthesizeFromMandate } from './synthesize-from-mandate.ts';
|
|
10
|
-
export { type SynthesizeFromRecordingOptions, synthesizeFromRecording, } from './synthesize-from-recording.ts';
|
|
10
|
+
export { type __TestInterpreterAdapterOptions, type SynthesizeFromRecordingOptions, synthesizeFromRecording, } from './synthesize-from-recording.ts';
|
|
@@ -20,21 +20,18 @@ export interface InterpreterAdapterOptions {
|
|
|
20
20
|
maxStalenessSeconds?: number;
|
|
21
21
|
maxDeviationBps?: number;
|
|
22
22
|
};
|
|
23
|
-
/** @internal Test-only seam. When set, the orchestrator uses this
|
|
24
|
-
* PredicateNode as the starting point for self-verify + minimise instead
|
|
25
|
-
* of calling the interpreter adapter's compile(). Production callers MUST
|
|
26
|
-
* NOT set this; the field exists so the self-verify pipeline can be
|
|
27
|
-
* exercised on hand-crafted predicates (redundant conjuncts for
|
|
28
|
-
* minimise, deliberately over-broad leaves for DENY_CASE_FAILURE). The
|
|
29
|
-
* `smartAccountAddress` + `installNonce` + `oracleParams` are still
|
|
30
|
-
* carried through so the emitted PolicyRef + PolicyDocument match the
|
|
31
|
-
* recorded call shape. */
|
|
32
|
-
__testPredicateNode?: PredicateNode;
|
|
33
23
|
}
|
|
34
|
-
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* `
|
|
24
|
+
/** PRIVATE test-only extension of `InterpreterAdapterOptions` for the
|
|
25
|
+
* `__testPredicateNode` seam. Exported under a `__` prefix so production
|
|
26
|
+
* callers can grep for it and see it is a test hook. The seam is read in
|
|
27
|
+
* `synthesizeFromRecordingInner` only when `process.env.NODE_ENV === 'test'`
|
|
28
|
+
* - a hard RUNTIME guard throws otherwise so a misconfigured production
|
|
29
|
+
* caller cannot bypass the recording -> interpreter adapter compile path. */
|
|
30
|
+
export type __TestInterpreterAdapterOptions = InterpreterAdapterOptions & {
|
|
31
|
+
/** @internal Test-only seam. Throws at runtime when NODE_ENV !== 'test'. */
|
|
32
|
+
__testPredicateNode?: PredicateNode;
|
|
33
|
+
};
|
|
34
|
+
/** Top-level orchestrator inputs. */
|
|
38
35
|
export interface SynthesizeFromRecordingOptions {
|
|
39
36
|
network: Network;
|
|
40
37
|
userResponses?: ComposeUserResponses;
|
|
@@ -42,13 +39,8 @@ export interface SynthesizeFromRecordingOptions {
|
|
|
42
39
|
threshold: number;
|
|
43
40
|
};
|
|
44
41
|
interpreter?: InterpreterAdapterOptions;
|
|
45
|
-
/** --explain opt-in.
|
|
46
|
-
*
|
|
47
|
-
* to the success envelope so the CLI can render a human-readable
|
|
48
|
-
* review card. Absent or false -> the success envelope is unchanged
|
|
49
|
-
* (byte-identical to today). The flag is ADDITIVE: the existing
|
|
50
|
-
* ProposedPolicy fields (encodedPredicate, predicateHash, etc.) are
|
|
51
|
-
* never altered by enabling explain. */
|
|
42
|
+
/** --explain opt-in. The flag is ADDITIVE: existing ProposedPolicy fields are
|
|
43
|
+
* never altered by enabling explain. Absent -> success envelope is byte-identical. */
|
|
52
44
|
explain?: true;
|
|
53
45
|
}
|
|
54
46
|
export declare function synthesizeFromRecording(tx: RecordedTransaction, opts: SynthesizeFromRecordingOptions, ozConfig: OzAdapterConfig): ToolResponse<ProposedPolicy> & {
|
|
@@ -71,3 +63,21 @@ export declare function synthesizeFromRecording(tx: RecordedTransaction, opts: S
|
|
|
71
63
|
* failure that the envelope converts to `{ok:false, error}`. */
|
|
72
64
|
declare function throwToolError(code: ToolError['code'], message: string): never;
|
|
73
65
|
export { throwToolError };
|
|
66
|
+
/** Diagnostic reported by `visitOracleLeaves` when an oracle comparison
|
|
67
|
+
* cannot be normalised to oracle-on-left. Currently only "oracle-on-right
|
|
68
|
+
* with a non-literal LHS" - the other branches already produce a parseable
|
|
69
|
+
* threshold and are returned to the caller. */
|
|
70
|
+
export interface OracleNormalisationWarning {
|
|
71
|
+
/** The dimension name (currently always "oracle_normalisation_dropped"). */
|
|
72
|
+
dimension: 'oracle_normalisation_dropped';
|
|
73
|
+
/** The operator as written. */
|
|
74
|
+
op: 'eq' | 'lt' | 'lte' | 'gt' | 'gte';
|
|
75
|
+
/** The asset the oracle leaf is bound to. */
|
|
76
|
+
asset: string;
|
|
77
|
+
/** The non-literal RHS (oracle-on-left) or LHS (oracle-on-right) leaf
|
|
78
|
+
* whose shape stopped the normalisation. Surfaced for diagnostics;
|
|
79
|
+
* intentionally a partial view - the full leaf is the caller's job. */
|
|
80
|
+
otherKind: string;
|
|
81
|
+
/** Human-readable message (matches the warn-on-drop text). */
|
|
82
|
+
message: string;
|
|
83
|
+
}
|