@crediolabs/policy-synth 0.1.18 → 0.3.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 +6 -5
- package/dist/adapters/interpreter/adapter.d.ts +19 -25
- package/dist/adapters/interpreter/adapter.js +54 -487
- package/dist/adapters/interpreter/index.d.ts +1 -1
- package/dist/adapters/interpreter/index.js +1 -1
- package/dist/adapters/oz/adapter.js +1 -15
- package/dist/errors.d.ts +1 -1
- package/dist/index.d.ts +1 -4
- package/dist/index.js +1 -4
- package/dist/install/build-add-context-rule.d.ts +3 -11
- package/dist/install/build-add-context-rule.js +2 -97
- package/dist/install/build-install-policy.d.ts +4 -53
- package/dist/install/build-install-policy.js +44 -123
- package/dist/ir/index.d.ts +1 -1
- package/dist/ir/types.d.ts +7 -76
- package/dist/ir/types.js +0 -2
- package/dist/predicate/decode.d.ts +1 -1
- package/dist/predicate/decode.js +2 -70
- package/dist/predicate/encode.js +45 -229
- package/dist/predicate/from-json.js +1 -42
- package/dist/record/decode.js +3 -8
- package/dist/review-card/builder.d.ts +4 -3
- package/dist/review-card/builder.js +18 -162
- package/dist/review-card/cross-check.js +2 -105
- package/dist/review-card/render-leaf.d.ts +4 -0
- package/dist/review-card/render-leaf.js +47 -0
- package/dist/run/index.d.ts +35 -53
- package/dist/run/index.js +115 -156
- package/dist/run/schemas.d.ts +168 -1650
- package/dist/run/schemas.js +36 -199
- package/dist/seams/types.d.ts +0 -2
- package/dist/simulate/deny-cases.d.ts +26 -0
- package/dist/simulate/deny-cases.js +282 -0
- package/dist/simulate/evaluate.d.ts +18 -0
- package/dist/simulate/evaluate.js +271 -0
- package/dist/simulate/index.d.ts +4 -0
- package/dist/simulate/index.js +8 -0
- package/dist/synth/compose-from-recording.d.ts +24 -51
- package/dist/synth/compose-from-recording.js +120 -282
- package/dist/synth/deny-cases.d.ts +5 -7
- package/dist/synth/deny-cases.js +23 -373
- package/dist/synth/evaluate.d.ts +0 -21
- package/dist/synth/evaluate.js +56 -336
- package/dist/synth/index.d.ts +0 -5
- package/dist/synth/index.js +0 -5
- package/dist/synth/permit-context.d.ts +2 -9
- package/dist/synth/permit-context.js +1 -82
- package/dist/synth/synthesize-from-recording.d.ts +1 -29
- package/dist/synth/synthesize-from-recording.js +41 -356
- package/dist/types.d.ts +8 -60
- package/dist/types.js +5 -2
- package/dist/verify/simulate.d.ts +1 -14
- package/dist/verify/simulate.js +2 -88
- package/dist/verify/verify.d.ts +1 -7
- package/dist/verify/verify.js +0 -8
- package/dist-cjs/adapters/interpreter/adapter.d.ts +19 -25
- package/dist-cjs/adapters/interpreter/adapter.js +56 -489
- package/dist-cjs/adapters/interpreter/index.d.ts +1 -1
- package/dist-cjs/adapters/interpreter/index.js +1 -2
- package/dist-cjs/adapters/oz/adapter.js +1 -15
- package/dist-cjs/errors.d.ts +1 -1
- package/dist-cjs/index.d.ts +1 -4
- package/dist-cjs/index.js +1 -4
- package/dist-cjs/install/build-add-context-rule.d.ts +3 -11
- package/dist-cjs/install/build-add-context-rule.js +1 -96
- package/dist-cjs/install/build-install-policy.d.ts +4 -53
- package/dist-cjs/install/build-install-policy.js +44 -122
- package/dist-cjs/ir/index.d.ts +1 -1
- package/dist-cjs/ir/types.d.ts +7 -76
- package/dist-cjs/ir/types.js +0 -2
- package/dist-cjs/predicate/decode.d.ts +1 -1
- package/dist-cjs/predicate/decode.js +2 -70
- package/dist-cjs/predicate/encode.js +45 -229
- package/dist-cjs/predicate/from-json.js +1 -42
- package/dist-cjs/record/decode.js +3 -8
- package/dist-cjs/review-card/builder.d.ts +4 -3
- package/dist-cjs/review-card/builder.js +23 -167
- package/dist-cjs/review-card/cross-check.js +7 -110
- package/dist-cjs/review-card/render-leaf.d.ts +4 -0
- package/dist-cjs/review-card/render-leaf.js +52 -0
- package/dist-cjs/run/index.d.ts +35 -53
- package/dist-cjs/run/index.js +116 -162
- package/dist-cjs/run/schemas.d.ts +168 -1650
- package/dist-cjs/run/schemas.js +37 -200
- package/dist-cjs/seams/types.d.ts +0 -2
- package/dist-cjs/simulate/deny-cases.d.ts +26 -0
- package/dist-cjs/simulate/deny-cases.js +286 -0
- package/dist-cjs/simulate/evaluate.d.ts +18 -0
- package/dist-cjs/simulate/evaluate.js +274 -0
- package/dist-cjs/simulate/index.d.ts +4 -0
- package/dist-cjs/simulate/index.js +13 -0
- package/dist-cjs/synth/compose-from-recording.d.ts +24 -51
- package/dist-cjs/synth/compose-from-recording.js +120 -282
- package/dist-cjs/synth/deny-cases.d.ts +5 -7
- package/dist-cjs/synth/deny-cases.js +23 -374
- package/dist-cjs/synth/evaluate.d.ts +0 -21
- package/dist-cjs/synth/evaluate.js +57 -337
- package/dist-cjs/synth/index.d.ts +0 -5
- package/dist-cjs/synth/index.js +1 -11
- package/dist-cjs/synth/permit-context.d.ts +2 -9
- package/dist-cjs/synth/permit-context.js +1 -82
- package/dist-cjs/synth/synthesize-from-recording.d.ts +1 -29
- package/dist-cjs/synth/synthesize-from-recording.js +39 -354
- package/dist-cjs/types.d.ts +8 -60
- package/dist-cjs/types.js +6 -3
- package/dist-cjs/verify/simulate.d.ts +1 -14
- package/dist-cjs/verify/simulate.js +2 -88
- package/dist-cjs/verify/verify.d.ts +1 -7
- package/dist-cjs/verify/verify.js +0 -8
- package/package.json +4 -4
- package/src/adapters/interpreter/adapter.ts +76 -572
- package/src/errors.ts +0 -19
- package/src/index.ts +1 -4
- package/src/install/build-add-context-rule.ts +4 -127
- package/src/install/build-install-policy.ts +93 -214
- package/src/predicate/decode.ts +2 -70
- package/src/predicate/encode.ts +49 -261
- package/src/predicate/from-json.ts +1 -43
- package/src/record/decode.ts +3 -8
- package/src/review-card/builder.ts +19 -168
- package/src/review-card/cross-check.ts +3 -105
- package/src/review-card/render-leaf.ts +48 -0
- package/src/run/index.ts +130 -186
- package/src/run/schemas.ts +38 -214
- package/src/simulate/deny-cases.ts +334 -0
- package/src/simulate/evaluate.ts +284 -0
- package/src/simulate/index.ts +11 -0
- package/src/synth/compose-from-recording.ts +148 -347
- package/src/synth/index.ts +0 -13
- package/src/synth/synthesize-from-recording.ts +43 -456
- package/src/types.ts +13 -49
- package/dist/install/build-install-predicate.d.ts +0 -96
- package/dist/install/build-install-predicate.js +0 -444
- package/dist-cjs/install/build-install-predicate.d.ts +0 -96
- package/dist-cjs/install/build-install-predicate.js +0 -479
- package/src/adapters/interpreter/index.ts +0 -8
- package/src/adapters/oz/adapter.ts +0 -376
- package/src/adapters/oz/index.ts +0 -9
- package/src/codegen/compile-gate.ts +0 -167
- package/src/codegen/index.ts +0 -17
- package/src/codegen/template.ts +0 -165
- package/src/ir/index.ts +0 -13
- package/src/ir/types.ts +0 -132
- package/src/mandate/index.ts +0 -4
- package/src/mandate/to-ir.ts +0 -71
- package/src/mandate/types.ts +0 -21
- package/src/seams/index.ts +0 -11
- package/src/seams/types.ts +0 -81
- package/src/synth/deny-cases.ts +0 -663
- package/src/synth/evaluate.ts +0 -613
- package/src/synth/harness.ts +0 -68
- package/src/synth/minimize.ts +0 -48
- package/src/synth/permit-context.ts +0 -136
- package/src/synth/predicate-literals.ts +0 -27
- package/src/synth/synthesize-from-mandate.ts +0 -82
- package/src/verify/envelope.ts +0 -28
- package/src/verify/index.ts +0 -5
- package/src/verify/simulate.ts +0 -311
- package/src/verify/verify.ts +0 -243
|
@@ -6,38 +6,26 @@
|
|
|
6
6
|
// summary being REPRODUCIBLE + TESTABLE + NON-HALLUCINABLE, so this module is
|
|
7
7
|
// pure: same inputs -> byte-identical output, no clock, no randomness, no I/O.
|
|
8
8
|
//
|
|
9
|
-
// The builder walks
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
// 2. The interpreter `PredicateNode`. One string per constraint leaf,
|
|
18
|
-
// rendered by enclosing-comparison kind. Templates (Task 7b):
|
|
19
|
-
// - invocation_count_in_window < N -> At most N calls per <window> seconds
|
|
20
|
-
// - call_arg[i] in [list] -> Recipient/arg must be one of [list]
|
|
21
|
-
// - eq(call_arg[i], literal_vec[...]) -> Path must be exactly [list]
|
|
22
|
-
// - oracle_price(asset) OP price -> Only when oracle_price(asset) OP price
|
|
23
|
-
// - call_fn == x -> Function must be x
|
|
24
|
-
// - call_contract == c -> Contract must be c
|
|
25
|
-
// - amount <= v -> Amount <= v
|
|
9
|
+
// The builder walks the interpreter `PredicateNode` and emits ONE constraint
|
|
10
|
+
// string per leaf, in a fixed deterministic order. Templates (Task 7b):
|
|
11
|
+
// - call_arg[i] in [list] -> Recipient/arg must be one of [list]
|
|
12
|
+
// - eq(call_arg[i], literal_vec[...]) -> Path must be exactly [list]
|
|
13
|
+
// - call_fn == x -> Function must be x
|
|
14
|
+
// - call_contract == c -> Contract must be c
|
|
15
|
+
// - amount <= v -> Amount <= v
|
|
26
16
|
//
|
|
27
17
|
// The content hash is a stable sha256 hex of a canonical JSON of
|
|
28
18
|
// { ruleName, plainEnglish, constraints, expiry, backend } - identical
|
|
29
19
|
// inputs (incl. the context-rule expiry and simulation backend) -> identical
|
|
30
20
|
// hash. There is no clock; the hash never includes a timestamp.
|
|
31
21
|
import { createHash } from 'node:crypto';
|
|
22
|
+
import { comparisonOpText, renderHaystackElement, renderVecElement } from "./render-leaf.js";
|
|
32
23
|
/** Build a deterministic review-card summary from a policy + context rule +
|
|
33
24
|
* simulation result. Pure: same inputs -> byte-identical output. */
|
|
34
|
-
export function buildReviewCardSummary(predicate,
|
|
25
|
+
export function buildReviewCardSummary(predicate, contextRule,
|
|
26
|
+
/** Evaluator identity folded into the content hash. */
|
|
27
|
+
backend) {
|
|
35
28
|
const constraints = [];
|
|
36
|
-
for (const ref of policyRefs) {
|
|
37
|
-
const line = renderOzPrimitive(ref);
|
|
38
|
-
if (line !== null)
|
|
39
|
-
constraints.push(line);
|
|
40
|
-
}
|
|
41
29
|
if (predicate !== null) {
|
|
42
30
|
walkPredicate(predicate, (node) => {
|
|
43
31
|
const line = renderConstraint(node);
|
|
@@ -55,7 +43,6 @@ export function buildReviewCardSummary(predicate, policyRefs, contextRule, simul
|
|
|
55
43
|
const signerNote = contextRule.signers.length >= 2
|
|
56
44
|
? 'any ONE signer may authorise a permitted op under this rule (OZ any-of-N semantic)'
|
|
57
45
|
: null;
|
|
58
|
-
const backend = simulation.backend;
|
|
59
46
|
const contentHash = computeContentHash({
|
|
60
47
|
ruleName,
|
|
61
48
|
plainEnglish,
|
|
@@ -66,25 +53,6 @@ export function buildReviewCardSummary(predicate, policyRefs, contextRule, simul
|
|
|
66
53
|
});
|
|
67
54
|
return { ruleName, plainEnglish, constraints, expiry, signerNote, backend, contentHash };
|
|
68
55
|
}
|
|
69
|
-
/** Render the OZ built-in primitive summary line. Only `spending_limit` is
|
|
70
|
-
* quoted by the review card (it is the only primitive that defines a
|
|
71
|
-
* transactional bound). Other primitives (threshold) are signer-config
|
|
72
|
-
* concerns handled by the OZ adapter's own `uncovered` machinery.
|
|
73
|
-
* Spending_limit takes `period_ledgers` on-chain (~5s/ledger); the card
|
|
74
|
-
* states the window in seconds so the user reads it consistently with the
|
|
75
|
-
* interpreter templates. */
|
|
76
|
-
function renderOzPrimitive(ref) {
|
|
77
|
-
if (ref.kind !== 'oz_builtin')
|
|
78
|
-
return null;
|
|
79
|
-
const primitive = ref.primitive;
|
|
80
|
-
if (primitive.primitive !== 'spending_limit')
|
|
81
|
-
return null;
|
|
82
|
-
const params = primitive.params;
|
|
83
|
-
const limit = params.spending_limit ?? '0';
|
|
84
|
-
const periodLedgers = params.period_ledgers ?? 0;
|
|
85
|
-
const windowSecs = periodLedgers * 5;
|
|
86
|
-
return `spending_limit(${limit}, ${windowSecs})`;
|
|
87
|
-
}
|
|
88
56
|
/** Walk every comparison / membership node of the predicate and invoke
|
|
89
57
|
* `visit` on each. The walk is depth-first, left-to-right, so the
|
|
90
58
|
* constraint list is stable across runs. Pure boolean nodes contribute no
|
|
@@ -109,21 +77,14 @@ export function describePredicate(predicate) {
|
|
|
109
77
|
function walkPredicate(node, visit) {
|
|
110
78
|
switch (node.op) {
|
|
111
79
|
case 'and':
|
|
112
|
-
case 'or':
|
|
113
80
|
for (const child of node.children)
|
|
114
81
|
walkPredicate(child, visit);
|
|
115
82
|
return;
|
|
116
|
-
case 'not':
|
|
117
|
-
walkPredicate(node.child, visit);
|
|
118
|
-
return;
|
|
119
83
|
case 'in':
|
|
120
84
|
visit(node);
|
|
121
85
|
return;
|
|
122
86
|
case 'eq':
|
|
123
|
-
case 'lt':
|
|
124
87
|
case 'lte':
|
|
125
|
-
case 'gt':
|
|
126
|
-
case 'gte':
|
|
127
88
|
visit(node);
|
|
128
89
|
return;
|
|
129
90
|
}
|
|
@@ -131,18 +92,13 @@ function walkPredicate(node, visit) {
|
|
|
131
92
|
/** Render ONE constraint sentence for ONE interpreter predicate node. The
|
|
132
93
|
* shape of the output is pinned by Task 7b so the test suite can assert
|
|
133
94
|
* byte-for-byte equality. Returns `null` when the node is a structural
|
|
134
|
-
* boolean (`and`
|
|
95
|
+
* boolean (`and`) - that is not a constraint leaf. */
|
|
135
96
|
function renderConstraint(node) {
|
|
136
97
|
switch (node.op) {
|
|
137
98
|
case 'and':
|
|
138
|
-
case 'or':
|
|
139
|
-
case 'not':
|
|
140
99
|
return null;
|
|
141
100
|
case 'eq':
|
|
142
|
-
case 'lt':
|
|
143
101
|
case 'lte':
|
|
144
|
-
case 'gt':
|
|
145
|
-
case 'gte':
|
|
146
102
|
return renderComparison(node);
|
|
147
103
|
case 'in':
|
|
148
104
|
return renderMembership(node);
|
|
@@ -172,8 +128,8 @@ function renderComparison(node) {
|
|
|
172
128
|
}
|
|
173
129
|
// eq/lte(call_arg_field[i, el, field], <literal>) -> arg[i] element[el].field = <value>
|
|
174
130
|
// The structured-argument bind pins a scalar field inside a vec element.
|
|
175
|
-
// For
|
|
176
|
-
//
|
|
131
|
+
// For lte the line reads "OP <value>" so the comparison kind is visible;
|
|
132
|
+
// eq reads "= <value>".
|
|
177
133
|
if (left.kind === 'call_arg_field') {
|
|
178
134
|
const head = `arg[${left.index}] element[${left.element}].${left.field}`;
|
|
179
135
|
const sep = node.op === 'eq' ? '=' : comparisonOpText(node.op);
|
|
@@ -181,10 +137,6 @@ function renderComparison(node) {
|
|
|
181
137
|
return `${head} ${sep} ${right.value}`;
|
|
182
138
|
if (right.kind === 'literal_symbol')
|
|
183
139
|
return `${head} ${sep} ${right.value}`;
|
|
184
|
-
if (right.kind === 'literal_bytes')
|
|
185
|
-
return `${head} ${sep} ${right.value}`;
|
|
186
|
-
if (right.kind === 'literal_u64')
|
|
187
|
-
return `${head} ${sep} ${right.value}`;
|
|
188
140
|
if (right.kind === 'literal_i128')
|
|
189
141
|
return `${head} ${sep} ${right.value}`;
|
|
190
142
|
if (right.kind === 'literal_u32')
|
|
@@ -193,22 +145,10 @@ function renderComparison(node) {
|
|
|
193
145
|
return `${head} ${sep} [${right.elements.map(renderVecElement).join(', ')}]`;
|
|
194
146
|
}
|
|
195
147
|
}
|
|
196
|
-
// The
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
|
|
200
|
-
const allowed = node.op === 'lt' ? right.value : right.value + 1;
|
|
201
|
-
return `At most ${allowed} calls per ${left.windowSecs} seconds`;
|
|
202
|
-
}
|
|
203
|
-
// OP(call_arg[i], <scalar literal>) -> arg[i] OP <value>
|
|
204
|
-
//
|
|
205
|
-
// The per-call cap. It has to be here because the `amount` template below
|
|
206
|
-
// is unreachable for an interpreter policy: `amount` is deliberately not in
|
|
207
|
-
// the contract's grammar (dsl.rs - the interpreter sees one authorized
|
|
208
|
-
// call, not the transaction's token movements), so a predicate using it is
|
|
209
|
-
// refused at install. A bound on the call's own amount ARGUMENT is how a
|
|
210
|
-
// cap is actually written, and it was rendering nothing at all - the card
|
|
211
|
-
// silently understated the policy.
|
|
148
|
+
// The per-call cap: OP(call_arg[i], <scalar literal>) -> arg[i] OP <value>.
|
|
149
|
+
// The interpreter is passed one authorized call, not the transaction's token
|
|
150
|
+
// movements, so a bound on the call's own amount ARGUMENT is how a cap is
|
|
151
|
+
// written.
|
|
212
152
|
//
|
|
213
153
|
// Placed after the literal_vec case above so an exact-sequence `eq` still
|
|
214
154
|
// reads as a path rather than as a comparison.
|
|
@@ -217,28 +157,12 @@ function renderComparison(node) {
|
|
|
217
157
|
const sep = node.op === 'eq' ? '=' : comparisonOpText(node.op);
|
|
218
158
|
if (right.kind === 'literal_i128')
|
|
219
159
|
return `${head} ${sep} ${right.value}`;
|
|
220
|
-
if (right.kind === 'literal_u64')
|
|
221
|
-
return `${head} ${sep} ${right.value}`;
|
|
222
160
|
if (right.kind === 'literal_u32')
|
|
223
161
|
return `${head} ${sep} ${right.value}`;
|
|
224
162
|
if (right.kind === 'literal_address')
|
|
225
163
|
return `${head} ${sep} ${right.value}`;
|
|
226
164
|
if (right.kind === 'literal_symbol')
|
|
227
165
|
return `${head} ${sep} ${right.value}`;
|
|
228
|
-
if (right.kind === 'literal_bytes')
|
|
229
|
-
return `${head} ${sep} ${right.value}`;
|
|
230
|
-
// call_arg_scaled is the slippage floor and reads as itself.
|
|
231
|
-
if (right.kind === 'call_arg_scaled') {
|
|
232
|
-
return `${head} >= arg[${right.index}] * ${right.num}/${right.den}`;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
// amount <= v -> Amount <= v
|
|
236
|
-
if (left.kind === 'amount' && right.kind === 'literal_i128') {
|
|
237
|
-
return `Amount <= ${right.value}`;
|
|
238
|
-
}
|
|
239
|
-
// oracle_price(asset) OP price -> Only when oracle_price(asset) OP price
|
|
240
|
-
if (left.kind === 'oracle_price' && right.kind === 'oracle_threshold') {
|
|
241
|
-
return `Only when oracle_price(${left.asset}) ${comparisonOpText(node.op)} ${right.value} (${right.decimals} dp)`;
|
|
242
166
|
}
|
|
243
167
|
// Any other comparison shape is a structural fail-closed: do not surface
|
|
244
168
|
// a misleading line. Cross-check still requires every leaf produce a
|
|
@@ -255,74 +179,6 @@ function renderMembership(node) {
|
|
|
255
179
|
}
|
|
256
180
|
return null;
|
|
257
181
|
}
|
|
258
|
-
function renderVecElement(leaf) {
|
|
259
|
-
switch (leaf.kind) {
|
|
260
|
-
case 'literal_address':
|
|
261
|
-
return leaf.value;
|
|
262
|
-
case 'literal_i128':
|
|
263
|
-
return leaf.value;
|
|
264
|
-
case 'literal_symbol':
|
|
265
|
-
return leaf.value;
|
|
266
|
-
case 'literal_u32':
|
|
267
|
-
return String(leaf.value);
|
|
268
|
-
case 'literal_u64':
|
|
269
|
-
return leaf.value;
|
|
270
|
-
case 'literal_bytes':
|
|
271
|
-
return leaf.value;
|
|
272
|
-
case 'literal_vec':
|
|
273
|
-
return `[${leaf.elements.map(renderVecElement).join(', ')}]`;
|
|
274
|
-
case 'call_contract':
|
|
275
|
-
case 'call_fn':
|
|
276
|
-
case 'call_arg':
|
|
277
|
-
case 'call_arg_len':
|
|
278
|
-
case 'call_arg_field':
|
|
279
|
-
case 'call_arg_scaled':
|
|
280
|
-
case 'amount':
|
|
281
|
-
case 'window_spent':
|
|
282
|
-
case 'now':
|
|
283
|
-
case 'valid_until':
|
|
284
|
-
case 'invocation_count_in_window':
|
|
285
|
-
case 'oracle_price':
|
|
286
|
-
return `<${leaf.kind}>`;
|
|
287
|
-
// Show the declared basis, not just the digits. A threshold on the wrong
|
|
288
|
-
// basis is the one policy error the contract cannot detect, so the review
|
|
289
|
-
// card is where a human has to be able to see it.
|
|
290
|
-
case 'oracle_threshold':
|
|
291
|
-
return `${leaf.value} (${leaf.decimals} dp)`;
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
function renderHaystackElement(leaf) {
|
|
295
|
-
if (leaf.kind === 'literal_address')
|
|
296
|
-
return leaf.value;
|
|
297
|
-
if (leaf.kind === 'literal_i128')
|
|
298
|
-
return leaf.value;
|
|
299
|
-
if (leaf.kind === 'literal_symbol')
|
|
300
|
-
return leaf.value;
|
|
301
|
-
if (leaf.kind === 'literal_u32')
|
|
302
|
-
return String(leaf.value);
|
|
303
|
-
if (leaf.kind === 'literal_u64')
|
|
304
|
-
return leaf.value;
|
|
305
|
-
if (leaf.kind === 'literal_bytes')
|
|
306
|
-
return leaf.value;
|
|
307
|
-
if (leaf.kind === 'literal_vec') {
|
|
308
|
-
return `[${leaf.elements.map(renderHaystackElement).join(', ')}]`;
|
|
309
|
-
}
|
|
310
|
-
return `<${leaf.kind}>`;
|
|
311
|
-
}
|
|
312
|
-
function comparisonOpText(op) {
|
|
313
|
-
switch (op) {
|
|
314
|
-
case 'lt':
|
|
315
|
-
return '<';
|
|
316
|
-
case 'lte':
|
|
317
|
-
return '<=';
|
|
318
|
-
case 'gt':
|
|
319
|
-
return '>';
|
|
320
|
-
case 'gte':
|
|
321
|
-
return '>=';
|
|
322
|
-
case 'eq':
|
|
323
|
-
return '==';
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
182
|
/** Render the plain-English one-liner. Format: `<ruleName>: <constraints>`,
|
|
327
183
|
* joined by `; ` so the user reads one sentence per constraint. */
|
|
328
184
|
function renderPlainEnglish(ruleName, constraints) {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// demands the summary carries the corresponding string. A naive summary
|
|
13
13
|
// that drops a leaf (or fabricates a string not in the predicate) trips the
|
|
14
14
|
// guard.
|
|
15
|
+
import { comparisonOpText, renderHaystackElement, renderVecElement } from "./render-leaf.js";
|
|
15
16
|
/** Assert every leaf in the predicate appears as a constraint string in
|
|
16
17
|
* `summary.constraints`. Returns the missing templates (without the leaf
|
|
17
18
|
* values filled in) when any leaf was dropped. */
|
|
@@ -35,18 +36,11 @@ export function summaryCrossCheck(predicate, summary) {
|
|
|
35
36
|
function collect(node, out) {
|
|
36
37
|
switch (node.op) {
|
|
37
38
|
case 'and':
|
|
38
|
-
case 'or':
|
|
39
39
|
for (const child of node.children)
|
|
40
40
|
collect(child, out);
|
|
41
41
|
return;
|
|
42
|
-
case 'not':
|
|
43
|
-
collect(node.child, out);
|
|
44
|
-
return;
|
|
45
42
|
case 'eq':
|
|
46
|
-
case 'lt':
|
|
47
43
|
case 'lte':
|
|
48
|
-
case 'gt':
|
|
49
|
-
case 'gte':
|
|
50
44
|
pushComparison(node.left, node.right, node.op, out);
|
|
51
45
|
return;
|
|
52
46
|
case 'in':
|
|
@@ -84,14 +78,6 @@ function pushComparison(left, right, op, out) {
|
|
|
84
78
|
out.push(`${head} ${sep} ${right.value}`);
|
|
85
79
|
return;
|
|
86
80
|
}
|
|
87
|
-
if (right.kind === 'literal_bytes') {
|
|
88
|
-
out.push(`${head} ${sep} ${right.value}`);
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
if (right.kind === 'literal_u64') {
|
|
92
|
-
out.push(`${head} ${sep} ${right.value}`);
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
81
|
if (right.kind === 'literal_i128') {
|
|
96
82
|
out.push(`${head} ${sep} ${right.value}`);
|
|
97
83
|
return;
|
|
@@ -105,13 +91,6 @@ function pushComparison(left, right, op, out) {
|
|
|
105
91
|
return;
|
|
106
92
|
}
|
|
107
93
|
}
|
|
108
|
-
// Mirrors the builder's wording byte-for-byte: how many calls the rule
|
|
109
|
-
// permits, since the bound counts the calls already made.
|
|
110
|
-
if (left.kind === 'invocation_count_in_window' && right.kind === 'literal_u32') {
|
|
111
|
-
const allowed = op === 'lt' ? right.value : right.value + 1;
|
|
112
|
-
out.push(`At most ${allowed} calls per ${left.windowSecs} seconds`);
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
94
|
// Mirrors the builder's per-call cap line. Both sides must render the same
|
|
116
95
|
// string: cross-check compares the summary against the predicate, so a
|
|
117
96
|
// shape only ONE of them renders would read as a dropped constraint.
|
|
@@ -119,26 +98,12 @@ function pushComparison(left, right, op, out) {
|
|
|
119
98
|
const head = `arg[${left.index}]`;
|
|
120
99
|
const sep = op === 'eq' ? '=' : comparisonOpText(op);
|
|
121
100
|
if (right.kind === 'literal_i128' ||
|
|
122
|
-
right.kind === 'literal_u64' ||
|
|
123
101
|
right.kind === 'literal_u32' ||
|
|
124
102
|
right.kind === 'literal_address' ||
|
|
125
|
-
right.kind === 'literal_symbol'
|
|
126
|
-
right.kind === 'literal_bytes') {
|
|
103
|
+
right.kind === 'literal_symbol') {
|
|
127
104
|
out.push(`${head} ${sep} ${right.value}`);
|
|
128
105
|
return;
|
|
129
106
|
}
|
|
130
|
-
if (right.kind === 'call_arg_scaled') {
|
|
131
|
-
out.push(`${head} >= arg[${right.index}] * ${right.num}/${right.den}`);
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
if (left.kind === 'amount' && right.kind === 'literal_i128') {
|
|
136
|
-
out.push(`Amount <= ${right.value}`);
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
if (left.kind === 'oracle_price' && right.kind === 'oracle_threshold') {
|
|
140
|
-
out.push(`Only when oracle_price(${left.asset}) ${comparisonOpText(op)} ${right.value} (${right.decimals} dp)`);
|
|
141
|
-
return;
|
|
142
107
|
}
|
|
143
108
|
}
|
|
144
109
|
function pushMembership(needle, haystack, out) {
|
|
@@ -147,71 +112,3 @@ function pushMembership(needle, haystack, out) {
|
|
|
147
112
|
const list = haystack.map(renderHaystackElement).join(', ');
|
|
148
113
|
out.push(`Recipient/arg must be one of [${list}]`);
|
|
149
114
|
}
|
|
150
|
-
function renderVecElement(leaf) {
|
|
151
|
-
switch (leaf.kind) {
|
|
152
|
-
case 'literal_address':
|
|
153
|
-
return leaf.value;
|
|
154
|
-
case 'literal_i128':
|
|
155
|
-
return leaf.value;
|
|
156
|
-
case 'literal_symbol':
|
|
157
|
-
return leaf.value;
|
|
158
|
-
case 'literal_u32':
|
|
159
|
-
return String(leaf.value);
|
|
160
|
-
case 'literal_u64':
|
|
161
|
-
return leaf.value;
|
|
162
|
-
case 'literal_bytes':
|
|
163
|
-
return leaf.value;
|
|
164
|
-
case 'literal_vec':
|
|
165
|
-
return `[${leaf.elements.map(renderVecElement).join(', ')}]`;
|
|
166
|
-
case 'call_contract':
|
|
167
|
-
case 'call_fn':
|
|
168
|
-
case 'call_arg':
|
|
169
|
-
case 'call_arg_len':
|
|
170
|
-
case 'call_arg_field':
|
|
171
|
-
case 'call_arg_scaled':
|
|
172
|
-
case 'amount':
|
|
173
|
-
case 'window_spent':
|
|
174
|
-
case 'now':
|
|
175
|
-
case 'valid_until':
|
|
176
|
-
case 'invocation_count_in_window':
|
|
177
|
-
case 'oracle_price':
|
|
178
|
-
return `<${leaf.kind}>`;
|
|
179
|
-
// Show the declared basis, not just the digits. A threshold on the wrong
|
|
180
|
-
// basis is the one policy error the contract cannot detect, so the review
|
|
181
|
-
// card is where a human has to be able to see it.
|
|
182
|
-
case 'oracle_threshold':
|
|
183
|
-
return `${leaf.value} (${leaf.decimals} dp)`;
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
function renderHaystackElement(leaf) {
|
|
187
|
-
if (leaf.kind === 'literal_address')
|
|
188
|
-
return leaf.value;
|
|
189
|
-
if (leaf.kind === 'literal_i128')
|
|
190
|
-
return leaf.value;
|
|
191
|
-
if (leaf.kind === 'literal_symbol')
|
|
192
|
-
return leaf.value;
|
|
193
|
-
if (leaf.kind === 'literal_u32')
|
|
194
|
-
return String(leaf.value);
|
|
195
|
-
if (leaf.kind === 'literal_u64')
|
|
196
|
-
return leaf.value;
|
|
197
|
-
if (leaf.kind === 'literal_bytes')
|
|
198
|
-
return leaf.value;
|
|
199
|
-
if (leaf.kind === 'literal_vec') {
|
|
200
|
-
return `[${leaf.elements.map(renderHaystackElement).join(', ')}]`;
|
|
201
|
-
}
|
|
202
|
-
return `<${leaf.kind}>`;
|
|
203
|
-
}
|
|
204
|
-
function comparisonOpText(op) {
|
|
205
|
-
switch (op) {
|
|
206
|
-
case 'lt':
|
|
207
|
-
return '<';
|
|
208
|
-
case 'lte':
|
|
209
|
-
return '<=';
|
|
210
|
-
case 'gt':
|
|
211
|
-
return '>';
|
|
212
|
-
case 'gte':
|
|
213
|
-
return '>=';
|
|
214
|
-
case 'eq':
|
|
215
|
-
return '==';
|
|
216
|
-
}
|
|
217
|
-
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// Leaf and operator rendering shared by the review-card builder and the
|
|
2
|
+
// summary cross-check. Both turn the SAME predicate into human-readable text -
|
|
3
|
+
// the cross-check exists to confirm the builder's summary matches the predicate
|
|
4
|
+
// - so rendering a leaf differently in the two would make the check compare a
|
|
5
|
+
// string against itself under a different spelling and pass on a real mismatch.
|
|
6
|
+
export function renderVecElement(leaf) {
|
|
7
|
+
switch (leaf.kind) {
|
|
8
|
+
case 'literal_address':
|
|
9
|
+
return leaf.value;
|
|
10
|
+
case 'literal_i128':
|
|
11
|
+
return leaf.value;
|
|
12
|
+
case 'literal_symbol':
|
|
13
|
+
return leaf.value;
|
|
14
|
+
case 'literal_u32':
|
|
15
|
+
return String(leaf.value);
|
|
16
|
+
case 'literal_vec':
|
|
17
|
+
return `[${leaf.elements.map(renderVecElement).join(', ')}]`;
|
|
18
|
+
case 'call_contract':
|
|
19
|
+
case 'call_fn':
|
|
20
|
+
case 'call_arg':
|
|
21
|
+
case 'call_arg_len':
|
|
22
|
+
case 'call_arg_field':
|
|
23
|
+
return `<${leaf.kind}>`;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export function renderHaystackElement(leaf) {
|
|
27
|
+
if (leaf.kind === 'literal_address')
|
|
28
|
+
return leaf.value;
|
|
29
|
+
if (leaf.kind === 'literal_i128')
|
|
30
|
+
return leaf.value;
|
|
31
|
+
if (leaf.kind === 'literal_symbol')
|
|
32
|
+
return leaf.value;
|
|
33
|
+
if (leaf.kind === 'literal_u32')
|
|
34
|
+
return String(leaf.value);
|
|
35
|
+
if (leaf.kind === 'literal_vec') {
|
|
36
|
+
return `[${leaf.elements.map(renderHaystackElement).join(', ')}]`;
|
|
37
|
+
}
|
|
38
|
+
return `<${leaf.kind}>`;
|
|
39
|
+
}
|
|
40
|
+
export function comparisonOpText(op) {
|
|
41
|
+
switch (op) {
|
|
42
|
+
case 'lte':
|
|
43
|
+
return '<=';
|
|
44
|
+
case 'eq':
|
|
45
|
+
return '==';
|
|
46
|
+
}
|
|
47
|
+
}
|
package/dist/run/index.d.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
import { type ErrorCode, type PredicateNode, type ProposedPolicy, type RecordedTransaction, type ToolError, type ToolResponse } from '../index.ts';
|
|
2
2
|
import { type BuildInstallPolicyResult, type BuildRevokePolicyResult } from '../install/build-install-policy.ts';
|
|
3
3
|
import { getInterpreterInfo } from '../install/get-interpreter-info.ts';
|
|
4
|
-
import type { SimulationResult } from '../verify/envelope.ts';
|
|
5
4
|
import { type RecordTransactionInput, type SimulatePolicyInput, type SynthesizePolicyInput, type VerifyPolicyInput } from './schemas.ts';
|
|
6
5
|
export type { GetInterpreterInfoInput, InstallPolicyInput, RecordTransactionInput, RevokePolicyInput, SimulatePolicyInput, SynthesizePolicyInput, VerifyPolicyInput, } from './schemas.ts';
|
|
7
|
-
export { ComposeUserResponsesSchema, GetInterpreterInfoInputSchema, InstallPolicyInputSchema, InterpreterOptionsSchema, MAINNET_RPC_URL,
|
|
6
|
+
export { ComposeUserResponsesSchema, GetInterpreterInfoInputSchema, InstallPolicyInputSchema, InterpreterOptionsSchema, MAINNET_RPC_URL, NetworkSchema, PINNED_INTERPRETER_ADDRESS_BY_NETWORK, PINNED_INTERPRETER_GRAMMAR_VERSION, PINNED_INTERPRETER_MAINNET_ADDRESS, PINNED_INTERPRETER_TESTNET_ADDRESS, PINNED_INTERPRETER_WASM_SHA256, PredicateLeafSchema, PredicateNodeSchema, RecordedTransactionSchema, RecordTransactionInputSchema, RevokePolicyInputSchema, RPC_URL_BY_NETWORK, SynthesizePolicyInputSchema, TESTNET_RPC_URL, ToolErrorSchema, } from './schemas.ts';
|
|
8
7
|
export type RunRecordTransactionInput = RecordTransactionInput;
|
|
9
8
|
export type RunSynthesizePolicyInput = SynthesizePolicyInput;
|
|
10
9
|
export type RunSimulatePolicyInput = SimulatePolicyInput;
|
|
@@ -23,7 +22,6 @@ type RunToolName = 'record_transaction' | 'synthesize_policy' | 'simulate_policy
|
|
|
23
22
|
* the agent always sees a machine-readable error code. */
|
|
24
23
|
export declare function runRecordTransaction(raw: unknown): Promise<ToolResponse<RecordedTransaction>>;
|
|
25
24
|
/** `synthesize_policy` body - discriminated union on `source`:
|
|
26
|
-
* - `mandate` -> synthesizeFromMandate
|
|
27
25
|
* - `recording` -> synthesizeFromRecording
|
|
28
26
|
* Exposing BOTH front-ends through ONE tool keeps the MCP surface tiny while
|
|
29
27
|
* letting the agent pick the deterministic or the inferred path. The CLI
|
|
@@ -37,35 +35,8 @@ export declare function runRecordTransaction(raw: unknown): Promise<ToolResponse
|
|
|
37
35
|
export declare function runSynthesizePolicy(raw: unknown): Promise<ToolResponse<ProposedPolicy> & {
|
|
38
36
|
explain?: {
|
|
39
37
|
predicateTree: PredicateNode | null;
|
|
40
|
-
simulation: SimulationResult;
|
|
41
38
|
};
|
|
42
39
|
}>;
|
|
43
|
-
/** `simulate_policy` body - thin wrapper over `simulatePolicy`. The engine
|
|
44
|
-
* already returns fail-closed `{ok:false, error}` for runtime failures
|
|
45
|
-
* (SIMULATION_ERROR), so the try/catch envelope is for raw SDK throws
|
|
46
|
-
* only - same pattern as the other two wrappers. The predicate is
|
|
47
|
-
* passed inline (stateless by design; no `proposed_policy_id` lookup). */
|
|
48
|
-
export declare function runSimulatePolicy(raw: unknown): Promise<ToolResponse<SimulationResult>>;
|
|
49
|
-
/** `verify_policy` body - thin wrapper over `verifyPolicy`. The engine
|
|
50
|
-
* already returns `{ok:false, error}` with code VERIFICATION_FAILED when
|
|
51
|
-
* the minimality check fails; the try/catch envelope is for raw SDK
|
|
52
|
-
* throws only. Mirrors `runSimulatePolicy` exactly. */
|
|
53
|
-
export declare function runVerifyPolicy(raw: unknown): Promise<ToolResponse<true>>;
|
|
54
|
-
/** `install_policy` body - thin wrapper over `buildInstallPolicyXdr`.
|
|
55
|
-
* Returns the unsigned Soroban transaction envelope (base64 XDR) the
|
|
56
|
-
* wallet signs. The wallet signature IS the user-confirmation step - no
|
|
57
|
-
* `action_id` two-call pair (the server is stateless, see server.ts:10-12).
|
|
58
|
-
* One call installs the policy outright: `add_context_rule` carries the
|
|
59
|
-
* predicate to the interpreter in its `policies` install_param, so no
|
|
60
|
-
* separate `interpreter.install` call is needed or possible.
|
|
61
|
-
*
|
|
62
|
-
* Default-deny: an interpreter policy address other than the pinned
|
|
63
|
-
* interpreter for the selected network is REFUSED (the smart account
|
|
64
|
-
* would delegate to an interpreter the caller controls); the same
|
|
65
|
-
* applies to a non-pinned RPC URL (the auth nonce the wallet signs
|
|
66
|
-
* comes from the RPC). Both gates accept an explicit opt-in flag.
|
|
67
|
-
* Pin selection follows `input.network` (defaults to `testnet` so the
|
|
68
|
-
* pre-mainnet callers keep working unchanged). */
|
|
69
40
|
export declare function runInstallPolicy(raw: unknown): Promise<ToolResponse<BuildInstallPolicyResult>>;
|
|
70
41
|
/** `revoke_policy` body - thin wrapper over `buildRevokePolicyXdr`.
|
|
71
42
|
* Emits an unsigned XDR for `account.remove_context_rule(ruleId)`; the
|
|
@@ -77,30 +48,41 @@ export declare function runInstallPolicy(raw: unknown): Promise<ToolResponse<Bui
|
|
|
77
48
|
* policy payload, so the interpreter pin is not re-checked here.
|
|
78
49
|
* Pin selection follows `input.network` (defaults to `testnet`). */
|
|
79
50
|
export declare function runRevokePolicy(raw: unknown): Promise<ToolResponse<BuildRevokePolicyResult>>;
|
|
80
|
-
/** `
|
|
81
|
-
* Returns the pinned deployment fingerprint + an optional live
|
|
82
|
-
* `grammar_version()` comparison. The audit field is deliberately
|
|
83
|
-
* OMITTED (phase-04's "audit #44" has no source of truth in the repo -
|
|
84
|
-
* fabricating it would be a lie on a security surface; the live
|
|
85
|
-
* mismatch check is worth MORE).
|
|
51
|
+
/** `simulate_policy` body - evaluate a predicate against one recorded call.
|
|
86
52
|
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
|
|
92
|
-
|
|
53
|
+
* The evaluator is a second implementation of the on-chain semantics, and the
|
|
54
|
+
* conformance harness asserts it agrees with the Rust interpreter case for
|
|
55
|
+
* case. A verdict here is therefore a claim about what the contract would do,
|
|
56
|
+
* not a guess. */
|
|
57
|
+
export declare function runSimulatePolicy(raw: unknown): ToolResponse<{
|
|
58
|
+
permitted: boolean;
|
|
59
|
+
reason: string | null;
|
|
60
|
+
call: {
|
|
61
|
+
contract: string;
|
|
62
|
+
fn: string;
|
|
63
|
+
argCount: number;
|
|
64
|
+
};
|
|
65
|
+
}>;
|
|
66
|
+
/** `verify_policy` body - the permit case plus a generated deny case per
|
|
67
|
+
* dimension.
|
|
93
68
|
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
69
|
+
* Two failure modes are being checked, and they are not the same thing. A
|
|
70
|
+
* permit case that denies means the policy is too STRICT: it would refuse the
|
|
71
|
+
* very transaction it was synthesised from. A deny case that permits means it
|
|
72
|
+
* is too LOOSE: some mutation of that transaction still gets through. `ok` is
|
|
73
|
+
* true only when neither holds. */
|
|
74
|
+
export declare function runVerifyPolicy(raw: unknown): ToolResponse<{
|
|
75
|
+
ok: boolean;
|
|
76
|
+
permit: {
|
|
77
|
+
permitted: boolean;
|
|
78
|
+
reason: string | null;
|
|
79
|
+
};
|
|
80
|
+
denies: Array<{
|
|
81
|
+
dimension: string;
|
|
82
|
+
denied: boolean;
|
|
83
|
+
reason: string | null;
|
|
84
|
+
}>;
|
|
85
|
+
dimensionsCovered: number;
|
|
86
|
+
}>;
|
|
99
87
|
export declare function runGetInterpreterInfo(raw: unknown): Promise<ToolResponse<ReturnType<typeof getInterpreterInfo>>>;
|
|
100
|
-
/** Build a canonical ToolError for a thrown exception caught by the tool
|
|
101
|
-
* envelope. The MCP SDK stringifies thrown objects as "[object Object]" by
|
|
102
|
-
* default, so we extract a string-friendly message and tag the original
|
|
103
|
-
* error in `details` for the agent to inspect. Exported as a test-only seam
|
|
104
|
-
* so the suite in run/index.test.ts can drive the envelope path without
|
|
105
|
-
* standing up a full recordTransaction pipeline. */
|
|
106
88
|
export declare function caughtError(toolName: RunToolName, code: ErrorCode, e: unknown): ToolError;
|