@crediolabs/policy-synth 0.1.17 → 0.2.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/README.md +44 -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/authority-overlap.d.ts +134 -0
- package/dist/install/authority-overlap.js +0 -0
- package/dist/install/build-add-context-rule.d.ts +8 -0
- package/dist/install/build-add-context-rule.js +17 -46
- package/dist/install/build-install-policy.d.ts +12 -5
- package/dist/install/build-install-policy.js +32 -23
- package/dist/install/build-merge-policy.d.ts +70 -0
- package/dist/install/build-merge-policy.js +130 -0
- package/dist/install/get-interpreter-info.js +2 -2
- package/dist/install/index.d.ts +3 -1
- package/dist/install/index.js +8 -1
- package/dist/install/oz-auth.js +5 -3
- package/dist/install/plan-merge-policy.d.ts +49 -0
- package/dist/install/plan-merge-policy.js +86 -0
- package/dist/install/read-account-rules.d.ts +100 -0
- package/dist/install/read-account-rules.js +283 -0
- 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 +104 -18
- package/dist/run/index.js +359 -75
- package/dist/run/schemas.d.ts +479 -18
- package/dist/run/schemas.js +132 -24
- 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/authority-overlap.d.ts +134 -0
- package/dist-cjs/install/authority-overlap.js +0 -0
- package/dist-cjs/install/build-add-context-rule.d.ts +8 -0
- package/dist-cjs/install/build-add-context-rule.js +17 -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/build-merge-policy.d.ts +70 -0
- package/dist-cjs/install/build-merge-policy.js +134 -0
- package/dist-cjs/install/get-interpreter-info.js +2 -2
- package/dist-cjs/install/index.d.ts +3 -1
- package/dist-cjs/install/index.js +24 -3
- package/dist-cjs/install/oz-auth.js +5 -3
- package/dist-cjs/install/plan-merge-policy.d.ts +49 -0
- package/dist-cjs/install/plan-merge-policy.js +90 -0
- package/dist-cjs/install/read-account-rules.d.ts +100 -0
- package/dist-cjs/install/read-account-rules.js +296 -0
- 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 +104 -18
- package/dist-cjs/run/index.js +360 -74
- package/dist-cjs/run/schemas.d.ts +479 -18
- package/dist-cjs/run/schemas.js +133 -25
- 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 +1 -1
- 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/authority-overlap.ts +0 -0
- package/src/install/build-add-context-rule.ts +29 -56
- package/src/install/build-install-policy.ts +41 -23
- package/src/install/build-merge-policy.ts +219 -0
- package/src/install/get-interpreter-info.ts +2 -2
- package/src/install/index.ts +36 -2
- package/src/install/oz-auth.ts +5 -3
- package/src/install/plan-merge-policy.ts +133 -0
- package/src/install/read-account-rules.ts +376 -0
- 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 +500 -102
- package/src/run/schemas.ts +145 -24
- 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
|
@@ -1,34 +1,24 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// src/synth/evaluate.ts - the model evaluator.
|
|
2
|
+
// src/synth/evaluate.ts - the model evaluator (TS model of interpreter semantics).
|
|
3
3
|
//
|
|
4
|
-
// Pure function.
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
4
|
+
// Pure function. Determinism: same `(predicate, ctx)` -> byte-identical result,
|
|
5
|
+
// no clock, no randomness. Deny order (deny on FIRST violation, stable reason):
|
|
6
|
+
// 1. ledger expiry -> 'EXPIRED'
|
|
7
|
+
// 2. `now` past valid_until -> 'EXPIRED'
|
|
8
|
+
// 3. contract mismatch -> 'CONTRACT_SCOPE'
|
|
9
|
+
// 4. per-ScVal equality on fn/args; EXACT ordered vector
|
|
10
|
+
// equality; fail-closed on opaque args -> 'ARG_MISMATCH' / 'FN_MISMATCH'
|
|
11
|
+
// 5. `in` membership; empty haystack ALWAYS denies -> 'NOT_IN_ALLOWLIST'
|
|
12
|
+
// 6. amount / window_spent via BigInt -> 'AMOUNT_BOUND'
|
|
13
|
+
// 7. invocation_count_in_window -> 'FREQUENCY'
|
|
14
|
+
// 8. oracle_price: fatal -> throw OracleError, deny with
|
|
15
|
+
// underlying ORACLE_* reason -> 'ORACLE_*'
|
|
16
|
+
// 9. boolean nodes (and/or/not)
|
|
17
|
+
// 10. signer threshold gate -> 'THRESHOLD_NOT_MET'
|
|
18
|
+
// 11. otherwise permit.
|
|
8
19
|
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
// 2. a `now` leaf compared past `valid_until` semantics -> 'EXPIRED'
|
|
12
|
-
// 3. a `call_contract` eq whose literal != ctx.contract -> 'CONTRACT_SCOPE'
|
|
13
|
-
// 4. per-ScVal equality on `call_fn` / `call_arg[i]`;
|
|
14
|
-
// `eq(selector, literal_vec)` = EXACT ordered vector equality
|
|
15
|
-
// (compare element-by-element, deny if length or any differs);
|
|
16
|
-
// fail-closed on opaque/undecodable args -> 'ARG_MISMATCH' / 'FN_MISMATCH'
|
|
17
|
-
// 5. `in` membership: needle NOT in haystack; an EMPTY
|
|
18
|
-
// haystack ALWAYS denies -> 'NOT_IN_ALLOWLIST'
|
|
19
|
-
// 6. amount / window_spent comparisons via BigInt -> 'AMOUNT_BOUND'
|
|
20
|
-
// 7. `invocation_count_in_window` compare -> 'FREQUENCY'
|
|
21
|
-
// 8. `oracle_price`: missing/stale/deviation/etc. is FATAL -
|
|
22
|
-
// throw `OracleError`, catch at the top, deny with the
|
|
23
|
-
// underlying `ORACLE_*` reason -> 'ORACLE_*'
|
|
24
|
-
// 9. boolean nodes: `and` = all children permit;
|
|
25
|
-
// `or` = any child permits; `not` = invert a non-oracle child
|
|
26
|
-
// 10. signer threshold gate (when `signerWeights` provided,
|
|
27
|
-
// no positive-weight signer -> deny 'THRESHOLD_NOT_MET')
|
|
28
|
-
// 11. otherwise `{ permit: true }`.
|
|
29
|
-
//
|
|
30
|
-
// Amounts: BigInt on decimal strings (never JS `number`). Oracle errors
|
|
31
|
-
// modelled by throwing (never a boolean-false that `not`/`or` could mask).
|
|
20
|
+
// Amounts: BigInt on decimal strings. Oracle errors thrown (never
|
|
21
|
+
// boolean-false `not`/`or` could mask).
|
|
32
22
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
23
|
exports.evaluate = evaluate;
|
|
34
24
|
const predicate_literals_ts_1 = require("./predicate-literals.js");
|
|
@@ -38,8 +28,8 @@ const NORMALISED_DECIMALS = 9;
|
|
|
38
28
|
/** Mirrors MAX_ORACLE_THRESHOLD_DECIMALS in dsl.rs. */
|
|
39
29
|
const MAX_ORACLE_THRESHOLD_DECIMALS = 18;
|
|
40
30
|
/** Internal fatal thrown by the oracle path; caught at the top of `evaluate`
|
|
41
|
-
* and converted to the matching `ORACLE_*` deny reason. NOT a
|
|
42
|
-
*
|
|
31
|
+
* and converted to the matching `ORACLE_*` deny reason. NOT a boolean-false
|
|
32
|
+
* that `not` / `or` could mask. */
|
|
43
33
|
class OracleError extends Error {
|
|
44
34
|
code;
|
|
45
35
|
constructor(code) {
|
|
@@ -93,12 +83,9 @@ function walk(node, ctx) {
|
|
|
93
83
|
for (const child of node.children) {
|
|
94
84
|
const r = walk(child, ctx);
|
|
95
85
|
if (!r.permit) {
|
|
96
|
-
// deny-on-first: short-circuit
|
|
97
|
-
// child, so the reason we surface is from that child.
|
|
86
|
+
// deny-on-first: short-circuit on the failing child.
|
|
98
87
|
return r;
|
|
99
88
|
}
|
|
100
|
-
// keep a reference to the last permit so unused-variable analysis
|
|
101
|
-
// doesn't complain; this is purely structural.
|
|
102
89
|
lastDeny = r;
|
|
103
90
|
}
|
|
104
91
|
return lastDeny ?? { permit: true };
|
|
@@ -114,11 +101,9 @@ function walk(node, ctx) {
|
|
|
114
101
|
return lastDeny ?? { permit: false, reason: 'NOT_IN_ALLOWLIST' };
|
|
115
102
|
}
|
|
116
103
|
case 'not': {
|
|
117
|
-
// `not` structurally inverts the child
|
|
118
|
-
// oracle leaf (compile-time rule: no oracle leaf under not/or).
|
|
119
|
-
//
|
|
120
|
-
// `evaluate` re-throws and surfaces the ORACLE_* deny. A non-oracle
|
|
121
|
-
// deny is inverted to a permit, and a permit is inverted to a deny.
|
|
104
|
+
// `not` structurally inverts the child unless the child contains an
|
|
105
|
+
// oracle leaf (compile-time rule: no oracle leaf under not/or). A
|
|
106
|
+
// child `OracleError` is re-thrown to the catch at the top of `evaluate`.
|
|
122
107
|
const r = walk(node.child, ctx);
|
|
123
108
|
if (r.permit)
|
|
124
109
|
return { permit: false, reason: 'FN_MISMATCH' };
|
|
@@ -139,17 +124,14 @@ function evalCompare(op, left, right, ctx) {
|
|
|
139
124
|
// --- step 2: `now` vs `valid_until` semantics ---
|
|
140
125
|
if (left.kind === 'now' && right.kind === 'valid_until') {
|
|
141
126
|
const expired = op === 'gt' || op === 'gte' ? ctx.nowSeconds >= 0 : ctx.nowSeconds < 0;
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
// `nowSeconds` so the only true-positive expired path is the
|
|
146
|
-
// `gt`/`gte` shapes callers actually write.
|
|
127
|
+
// `valid_until` is modelled as a synthetic future timestamp far past
|
|
128
|
+
// `nowSeconds` so the only true-positive expired path is the `gt`/`gte`
|
|
129
|
+
// shapes callers actually write.
|
|
147
130
|
if (op === 'gt' || op === 'gte') {
|
|
148
131
|
if (ctx.nowSeconds > 0)
|
|
149
132
|
return { permit: false, reason: 'EXPIRED' };
|
|
150
133
|
}
|
|
151
134
|
else if (op === 'lt' || op === 'lte') {
|
|
152
|
-
// permits when valid_until is in the future
|
|
153
135
|
return { permit: true };
|
|
154
136
|
}
|
|
155
137
|
return expired ? { permit: false, reason: 'EXPIRED' } : { permit: true };
|
|
@@ -181,11 +163,6 @@ function evalCompare(op, left, right, ctx) {
|
|
|
181
163
|
return evalScaledArgCompare(op, left, right, ctx);
|
|
182
164
|
}
|
|
183
165
|
const actual = ctx.args[left.index];
|
|
184
|
-
// An ordered comparison (lt/lte/gt/gte) reads the arg as an integer and
|
|
185
|
-
// compares it to a numeric literal via BigInt (e.g. a SoroSwap input-amount
|
|
186
|
-
// cap `call_arg[0] <= limit`). The per-ScVal-type equality semantics live
|
|
187
|
-
// in evalArgEq (`eq` only); ordered ops are numeric-only and fail closed on
|
|
188
|
-
// a non-numeric arg or literal.
|
|
189
166
|
if (op !== 'eq')
|
|
190
167
|
return evalArgOrderedCompare(op, actual, right);
|
|
191
168
|
return evalArgEq(op, actual, right, ctx);
|
|
@@ -195,11 +172,11 @@ function evalCompare(op, left, right, ctx) {
|
|
|
195
172
|
if (left.kind === 'call_arg_scaled') {
|
|
196
173
|
return evalScaledArgCompare(op, right, left, ctx);
|
|
197
174
|
}
|
|
198
|
-
// --- step 4c: call_arg_len:
|
|
199
|
-
// Fails closed on a non-vec
|
|
175
|
+
// --- step 4c: call_arg_len: length of a vec-typed argument as u32.
|
|
176
|
+
// Fails closed on a non-vec / absent arg or a non-u32 literal.
|
|
200
177
|
if (left.kind === 'call_arg_len') {
|
|
201
178
|
const actual = ctx.args[left.index];
|
|
202
|
-
if (
|
|
179
|
+
if (actual?.type !== 'vec')
|
|
203
180
|
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
204
181
|
if (right.kind !== 'literal_u32')
|
|
205
182
|
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
@@ -207,16 +184,14 @@ function evalCompare(op, left, right, ctx) {
|
|
|
207
184
|
? { permit: true }
|
|
208
185
|
: { permit: false, reason: 'ARG_MISMATCH' };
|
|
209
186
|
}
|
|
210
|
-
// --- step 4d: call_arg_field:
|
|
211
|
-
//
|
|
212
|
-
// out-of-range element, a missing field, a non-map element, or a type
|
|
213
|
-
// mismatch between the field ScVal and the literal leaf.
|
|
187
|
+
// --- step 4d: call_arg_field: value of a field in the map at element i of
|
|
188
|
+
// the vec at argument index. Fails closed on shape / type / range issues.
|
|
214
189
|
if (left.kind === 'call_arg_field') {
|
|
215
190
|
const actual = ctx.args[left.index];
|
|
216
|
-
if (
|
|
191
|
+
if (actual?.type !== 'vec')
|
|
217
192
|
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
218
193
|
const element = actual.value[left.element];
|
|
219
|
-
if (
|
|
194
|
+
if (element?.type !== 'map')
|
|
220
195
|
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
221
196
|
if (!Array.isArray(element.value))
|
|
222
197
|
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
@@ -229,7 +204,7 @@ function evalCompare(op, left, right, ctx) {
|
|
|
229
204
|
}
|
|
230
205
|
// --- step 6: AMOUNT_BOUND ---
|
|
231
206
|
if (left.kind === 'amount' && op !== 'eq') {
|
|
232
|
-
return evalAmountCompare(op, left.token, right, ctx);
|
|
207
|
+
return evalAmountCompare(op, left.token, right, ctx, 'amountByToken');
|
|
233
208
|
}
|
|
234
209
|
if (left.kind === 'window_spent' && op !== 'eq') {
|
|
235
210
|
return evalWindowSpentCompare(op, left.token, right, ctx);
|
|
@@ -250,7 +225,7 @@ function evalCompare(op, left, right, ctx) {
|
|
|
250
225
|
* sequence: compare element-by-element in order; deny if length or any
|
|
251
226
|
* element differs. Opaque args (`type: 'other'`) fail closed. */
|
|
252
227
|
function evalArgEq(op, actual, right, ctx) {
|
|
253
|
-
// eq(call_arg[i], literal_vec) -> EXACT ordered vector equality
|
|
228
|
+
// eq(call_arg[i], literal_vec) -> EXACT ordered vector equality.
|
|
254
229
|
if (op === 'eq' && right.kind === 'literal_vec') {
|
|
255
230
|
if (actual?.type !== 'vec')
|
|
256
231
|
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
@@ -258,7 +233,6 @@ function evalArgEq(op, actual, right, ctx) {
|
|
|
258
233
|
? { permit: true }
|
|
259
234
|
: { permit: false, reason: 'ARG_MISMATCH' };
|
|
260
235
|
}
|
|
261
|
-
// eq(call_arg[i], literal_address) -> address compare
|
|
262
236
|
if (op === 'eq' && right.kind === 'literal_address') {
|
|
263
237
|
if (!actual)
|
|
264
238
|
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
@@ -268,7 +242,6 @@ function evalArgEq(op, actual, right, ctx) {
|
|
|
268
242
|
? { permit: true }
|
|
269
243
|
: { permit: false, reason: 'ARG_MISMATCH' };
|
|
270
244
|
}
|
|
271
|
-
// eq(call_arg[i], literal_i128) -> BigInt compare
|
|
272
245
|
if (op === 'eq' && right.kind === 'literal_i128') {
|
|
273
246
|
if (actual?.type !== 'i128')
|
|
274
247
|
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
@@ -276,7 +249,6 @@ function evalArgEq(op, actual, right, ctx) {
|
|
|
276
249
|
? { permit: true }
|
|
277
250
|
: { permit: false, reason: 'ARG_MISMATCH' };
|
|
278
251
|
}
|
|
279
|
-
// eq(call_arg[i], literal_symbol)
|
|
280
252
|
if (op === 'eq' && right.kind === 'literal_symbol') {
|
|
281
253
|
if (!actual)
|
|
282
254
|
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
@@ -284,7 +256,6 @@ function evalArgEq(op, actual, right, ctx) {
|
|
|
284
256
|
? { permit: true }
|
|
285
257
|
: { permit: false, reason: 'ARG_MISMATCH' };
|
|
286
258
|
}
|
|
287
|
-
// eq(call_arg[i], literal_u32)
|
|
288
259
|
if (op === 'eq' && right.kind === 'literal_u32') {
|
|
289
260
|
if (!actual)
|
|
290
261
|
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
@@ -292,7 +263,6 @@ function evalArgEq(op, actual, right, ctx) {
|
|
|
292
263
|
? { permit: true }
|
|
293
264
|
: { permit: false, reason: 'ARG_MISMATCH' };
|
|
294
265
|
}
|
|
295
|
-
// eq(call_arg[i], literal_u64)
|
|
296
266
|
if (op === 'eq' && right.kind === 'literal_u64') {
|
|
297
267
|
if (!actual)
|
|
298
268
|
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
@@ -300,7 +270,6 @@ function evalArgEq(op, actual, right, ctx) {
|
|
|
300
270
|
? { permit: true }
|
|
301
271
|
: { permit: false, reason: 'ARG_MISMATCH' };
|
|
302
272
|
}
|
|
303
|
-
// eq(call_arg[i], literal_bytes)
|
|
304
273
|
if (op === 'eq' && right.kind === 'literal_bytes') {
|
|
305
274
|
if (!actual)
|
|
306
275
|
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
@@ -313,20 +282,14 @@ function evalArgEq(op, actual, right, ctx) {
|
|
|
313
282
|
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
314
283
|
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
315
284
|
}
|
|
316
|
-
/** Step 4b': slippage-floor comparison. Mirrors the Rust `eval_scaled_arg_compare
|
|
317
|
-
*
|
|
318
|
-
* zero
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
* `left` is whichever side of the compare is NOT the scaled leaf. A
|
|
324
|
-
* scaled-on-scaled compare denies `ARG_MISMATCH` (pipelining two scaled
|
|
325
|
-
* leaves has no definable semantics). The other operand must be a
|
|
326
|
-
* numeric shape (`call_arg` carrying a number, or a numeric literal);
|
|
327
|
-
* anything else is `ARG_MISMATCH`. */
|
|
285
|
+
/** Step 4b': slippage-floor comparison. Mirrors the Rust `eval_scaled_arg_compare`:
|
|
286
|
+
* the scaled leaf is `args[index] * num / den` (truncating toward zero). On
|
|
287
|
+
* overflow or divide-by-zero deny with `ARITHMETIC_OVERFLOW`; a failed bound
|
|
288
|
+
* denies with `SLIPPAGE_FLOOR` (the dedicated reason) rather than the generic
|
|
289
|
+
* `ARG_MISMATCH`. A scaled-on-scaled compare denies `ARG_MISMATCH`
|
|
290
|
+
* (no definable semantics). The non-scaled operand must be numeric
|
|
291
|
+
* (`call_arg` carrying a number, or a numeric literal); else `ARG_MISMATCH`. */
|
|
328
292
|
function evalScaledArgCompare(op, left, right, ctx) {
|
|
329
|
-
// Identify which side is scaled, and pull the other side as a number.
|
|
330
293
|
let scaled;
|
|
331
294
|
let other;
|
|
332
295
|
let scaledOnRight;
|
|
@@ -341,14 +304,12 @@ function evalScaledArgCompare(op, left, right, ctx) {
|
|
|
341
304
|
scaledOnRight = true;
|
|
342
305
|
}
|
|
343
306
|
else {
|
|
344
|
-
//
|
|
345
|
-
// dispatcher; the contract returns ARG_MISMATCH to fail closed.
|
|
307
|
+
// Programming error in the dispatcher; fail closed.
|
|
346
308
|
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
347
309
|
}
|
|
348
|
-
//
|
|
349
|
-
//
|
|
350
|
-
//
|
|
351
|
-
// not be read.
|
|
310
|
+
// Out-of-bounds or non-numeric arg fails closed as ARG_MISMATCH (not
|
|
311
|
+
// SLIPPAGE_FLOOR) - a violated floor is the wrong code when the operand
|
|
312
|
+
// itself could not be read.
|
|
352
313
|
if (scaled.index >= ctx.args.length) {
|
|
353
314
|
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
354
315
|
}
|
|
@@ -365,16 +326,11 @@ function evalScaledArgCompare(op, left, right, ctx) {
|
|
|
365
326
|
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
366
327
|
}
|
|
367
328
|
// Install refuses `den == 0` and `num <= 0` / `den <= 0`. The runtime
|
|
368
|
-
// check is
|
|
369
|
-
//
|
|
329
|
+
// check is defensive belt-and-braces so a validator regression cannot
|
|
330
|
+
// panic the frame on a divide-by-zero.
|
|
370
331
|
if (den === 0n)
|
|
371
332
|
return { permit: false, reason: 'ARITHMETIC_OVERFLOW' };
|
|
372
333
|
const product = input * num;
|
|
373
|
-
// BigInt overflow is silent in JS (it does not throw on multiplication).
|
|
374
|
-
// We can detect over-range by dividing and checking the result against
|
|
375
|
-
// i128 bounds, but the more practical check is whether the result
|
|
376
|
-
// divides cleanly. We rely on BigInt's arbitrary precision and then
|
|
377
|
-
// surface ARITHMETIC_OVERFLOW if the result is outside i128.
|
|
378
334
|
let scaledValue;
|
|
379
335
|
try {
|
|
380
336
|
// BigInt division truncates toward zero (matches Rust `i128::checked_div`).
|
|
@@ -383,18 +339,13 @@ function evalScaledArgCompare(op, left, right, ctx) {
|
|
|
383
339
|
catch {
|
|
384
340
|
return { permit: false, reason: 'ARITHMETIC_OVERFLOW' };
|
|
385
341
|
}
|
|
386
|
-
// i128 range check:
|
|
387
|
-
//
|
|
342
|
+
// i128 range check: the contract would have wrapped on i128 arithmetic;
|
|
343
|
+
// surface the same deny.
|
|
388
344
|
const I128_MAX = (1n << 127n) - 1n;
|
|
389
345
|
const I128_MIN = -(1n << 127n);
|
|
390
346
|
if (scaledValue > I128_MAX || scaledValue < I128_MIN) {
|
|
391
347
|
return { permit: false, reason: 'ARITHMETIC_OVERFLOW' };
|
|
392
348
|
}
|
|
393
|
-
// Resolving the operand when the scaled side is on the right: the
|
|
394
|
-
// operand is `left`; the scaled value is the RHS. The AST order is
|
|
395
|
-
// `left <op> scaled`, so the comparator applies to `other <op> scaled`.
|
|
396
|
-
// When the scaled side is on the left, the order is `scaled <op> other`,
|
|
397
|
-
// i.e. `scaled <op> other_val`.
|
|
398
349
|
let otherVal;
|
|
399
350
|
if (other.kind === 'call_arg') {
|
|
400
351
|
otherVal = argNumericBigInt(ctx.args[other.index]);
|
|
@@ -462,7 +413,6 @@ function compareVecExact(actual, expected, ctx) {
|
|
|
462
413
|
function evalIn(needle, haystack, ctx) {
|
|
463
414
|
if (haystack.length === 0)
|
|
464
415
|
return { permit: false, reason: 'NOT_IN_ALLOWLIST' };
|
|
465
|
-
// Resolve the needle's ScVal against the candidate call.
|
|
466
416
|
const actual = resolveLeaf(needle, ctx);
|
|
467
417
|
if (!actual || actual.type === 'other')
|
|
468
418
|
return { permit: false, reason: 'NOT_IN_ALLOWLIST' };
|
|
@@ -489,10 +439,10 @@ function resolveLeaf(leaf, ctx) {
|
|
|
489
439
|
return undefined;
|
|
490
440
|
case 'call_arg_field': {
|
|
491
441
|
const actual = ctx.args[leaf.index];
|
|
492
|
-
if (
|
|
442
|
+
if (actual?.type !== 'vec')
|
|
493
443
|
return undefined;
|
|
494
444
|
const element = actual.value[leaf.element];
|
|
495
|
-
if (
|
|
445
|
+
if (element?.type !== 'map')
|
|
496
446
|
return undefined;
|
|
497
447
|
if (!Array.isArray(element.value))
|
|
498
448
|
return undefined;
|
|
@@ -523,25 +473,21 @@ function resolveLeaf(leaf, ctx) {
|
|
|
523
473
|
return undefined;
|
|
524
474
|
}
|
|
525
475
|
}
|
|
526
|
-
/** Step 6: amount compare on BigInt.
|
|
527
|
-
|
|
476
|
+
/** Step 6: amount / window_spent compare on BigInt. Both leaves compare a
|
|
477
|
+
* per-token BigInt record (current vs. rolling) to an i128 literal; the
|
|
478
|
+
* compare + denial reason are identical, so a single helper handles both. */
|
|
479
|
+
function evalAmountCompare(op, token, right, ctx, record) {
|
|
528
480
|
const literal = right.kind === 'literal_i128' ? right.value : null;
|
|
529
481
|
if (literal === null)
|
|
530
482
|
return { permit: false, reason: 'AMOUNT_BOUND' };
|
|
531
|
-
const actual = ctx
|
|
483
|
+
const actual = ctx[record][token] ?? '0';
|
|
532
484
|
return bigintCmp(op, actual, literal)
|
|
533
485
|
? { permit: true }
|
|
534
486
|
: { permit: false, reason: 'AMOUNT_BOUND' };
|
|
535
487
|
}
|
|
536
488
|
/** Step 6: window_spent compare on BigInt. */
|
|
537
489
|
function evalWindowSpentCompare(op, token, right, ctx) {
|
|
538
|
-
|
|
539
|
-
if (literal === null)
|
|
540
|
-
return { permit: false, reason: 'AMOUNT_BOUND' };
|
|
541
|
-
const actual = ctx.windowSpentByToken[token] ?? '0';
|
|
542
|
-
return bigintCmp(op, actual, literal)
|
|
543
|
-
? { permit: true }
|
|
544
|
-
: { permit: false, reason: 'AMOUNT_BOUND' };
|
|
490
|
+
return evalAmountCompare(op, token, right, ctx, 'windowSpentByToken');
|
|
545
491
|
}
|
|
546
492
|
/** Step 7: invocation_count_in_window compare. */
|
|
547
493
|
function evalFrequencyCompare(op, windowSecs, right, ctx) {
|
|
@@ -565,24 +511,28 @@ function evalOracleCompare(op, asset, right, ctx) {
|
|
|
565
511
|
throw new OracleError(mapped);
|
|
566
512
|
}
|
|
567
513
|
// Mirrors eval_oracle_compare in dsl.rs. The threshold MUST declare its
|
|
568
|
-
// decimal basis: prices are on the normalised 9-dp basis, and
|
|
569
|
-
//
|
|
570
|
-
//
|
|
514
|
+
// decimal basis: prices are on the normalised 9-dp basis, and a bare literal
|
|
515
|
+
// is refused (not assumed) because a raw 14-dp threshold that assumed the
|
|
516
|
+
// 9-dp basis would permit everything.
|
|
571
517
|
if (right.kind !== 'oracle_threshold')
|
|
572
518
|
throw new OracleError('ORACLE_DECIMALS_MISMATCH');
|
|
573
519
|
if (right.decimals > MAX_ORACLE_THRESHOLD_DECIMALS) {
|
|
574
520
|
throw new OracleError('ORACLE_THRESHOLD_DECIMALS_OUT_OF_RANGE');
|
|
575
521
|
}
|
|
576
|
-
// Scale BOTH sides up to the wider basis
|
|
577
|
-
//
|
|
522
|
+
// Scale BOTH sides up to the wider basis; dividing the threshold down
|
|
523
|
+
// truncates and moves the permit boundary.
|
|
578
524
|
const decimals = BigInt(right.decimals);
|
|
579
525
|
const normalised = BigInt(NORMALISED_DECIMALS);
|
|
580
526
|
const common = decimals > normalised ? decimals : normalised;
|
|
581
527
|
const priceScaled = BigInt(entry.price) * 10n ** (common - normalised);
|
|
582
528
|
const literalScaled = BigInt(right.value) * 10n ** (common - decimals);
|
|
529
|
+
// A violated oracle threshold is a stateful bound (Rust dsl.rs:276 returns
|
|
530
|
+
// `DenyReason::StatefulBound`, deny code #104). The TS evaluator must mirror
|
|
531
|
+
// the same reason so a future TS/Rust reason divergence fails CI; the
|
|
532
|
+
// cross-layer harness in this file now asserts reason codes match.
|
|
583
533
|
return bigintCmp(op, String(priceScaled), String(literalScaled))
|
|
584
534
|
? { permit: true }
|
|
585
|
-
: { permit: false, reason: '
|
|
535
|
+
: { permit: false, reason: 'STATEFUL_BOUND' };
|
|
586
536
|
}
|
|
587
537
|
/** BigInt compare helper. `eq` is also supported by callers (selector-vs-literal
|
|
588
538
|
* equal checks), but the dedicated arg path uses it via `BigInt(actual) === BigInt(right)`. */
|
|
@@ -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;
|
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.runHarness = runHarness;
|
|
4
4
|
const evaluate_ts_1 = require("./evaluate.js");
|
|
5
|
-
/** Evaluate the intended call and every generated denial without side effects.
|
|
5
|
+
/** Evaluate the intended call and every generated denial without side effects.
|
|
6
|
+
* Asserts both the boolean decision AND, for deny cases that carry an
|
|
7
|
+
* `expectedReason`, the concrete reason the TS evaluator emits. The reason
|
|
8
|
+
* is what the Rust interpreter returns; the cross-layer conformance test in
|
|
9
|
+
* `contracts/policy-interpreter/tests/conformance` mirrors the same contract
|
|
10
|
+
* for the verifyLive path. */
|
|
6
11
|
function runHarness(predicate, cases) {
|
|
7
12
|
const failures = [];
|
|
8
13
|
const permitResult = (0, evaluate_ts_1.evaluate)(predicate, cases.permit);
|
|
@@ -23,6 +28,22 @@ function runHarness(predicate, cases) {
|
|
|
23
28
|
got: 'permit',
|
|
24
29
|
reason: 'DENY_CASE_FAILURE',
|
|
25
30
|
});
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
// Reason-code assertion (cross-layer contract). When a deny case carries
|
|
34
|
+
// an expected reason, the TS evaluator MUST emit that exact string; a
|
|
35
|
+
// divergence means the TS model and the Rust interpreter disagree on
|
|
36
|
+
// what the user sees in CI - the harness fails so the diff is visible
|
|
37
|
+
// here, not at runtime in production.
|
|
38
|
+
if (deny.expectedReason !== undefined && result.reason !== deny.expectedReason) {
|
|
39
|
+
failures.push({
|
|
40
|
+
dimension: deny.dimension,
|
|
41
|
+
expected: 'deny',
|
|
42
|
+
got: 'deny',
|
|
43
|
+
reason: 'REASON_MISMATCH',
|
|
44
|
+
expectedReason: deny.expectedReason,
|
|
45
|
+
actualReason: result.reason,
|
|
46
|
+
});
|
|
26
47
|
}
|
|
27
48
|
}
|
|
28
49
|
return failures.length === 0 ? { ok: true } : { ok: false, failures };
|
|
@@ -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
|
+
}
|