@crediolabs/policy-synth 0.1.8 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/interpreter/adapter.js +21 -3
- package/dist/adapters/oz/adapter.js +8 -0
- package/dist/ir/types.d.ts +9 -0
- package/dist/predicate/encode.js +9 -0
- package/dist/review-card/builder.js +32 -0
- package/dist/review-card/cross-check.js +40 -0
- package/dist/run/index.d.ts +8 -2
- package/dist/run/index.js +2 -1
- package/dist/run/schemas.d.ts +12 -0
- package/dist/run/schemas.js +14 -0
- package/dist/synth/compose-from-recording.js +89 -0
- package/dist/synth/deny-cases.js +89 -1
- package/dist/synth/evaluate.js +49 -0
- package/dist/synth/synthesize-from-mandate.d.ts +17 -2
- package/dist/synth/synthesize-from-mandate.js +27 -2
- package/dist/synth/synthesize-from-recording.d.ts +25 -1
- package/dist/synth/synthesize-from-recording.js +66 -1
- package/dist/types.d.ts +8 -0
- package/dist-cjs/adapters/interpreter/adapter.js +21 -3
- package/dist-cjs/adapters/oz/adapter.js +8 -0
- package/dist-cjs/ir/types.d.ts +9 -0
- package/dist-cjs/predicate/encode.js +9 -0
- package/dist-cjs/review-card/builder.js +32 -0
- package/dist-cjs/review-card/cross-check.js +40 -0
- package/dist-cjs/run/index.d.ts +8 -2
- package/dist-cjs/run/index.js +2 -1
- package/dist-cjs/run/schemas.d.ts +12 -0
- package/dist-cjs/run/schemas.js +14 -0
- package/dist-cjs/synth/compose-from-recording.js +89 -0
- package/dist-cjs/synth/deny-cases.js +89 -1
- package/dist-cjs/synth/evaluate.js +49 -0
- package/dist-cjs/synth/synthesize-from-mandate.d.ts +17 -2
- package/dist-cjs/synth/synthesize-from-mandate.js +27 -2
- package/dist-cjs/synth/synthesize-from-recording.d.ts +25 -1
- package/dist-cjs/synth/synthesize-from-recording.js +66 -1
- package/dist-cjs/types.d.ts +8 -0
- package/package.json +1 -1
- package/src/adapters/interpreter/adapter.ts +20 -4
- package/src/adapters/oz/adapter.ts +8 -0
- package/src/ir/types.ts +8 -0
- package/src/predicate/encode.ts +9 -0
- package/src/review-card/builder.ts +28 -0
- package/src/review-card/cross-check.ts +40 -0
- package/src/run/index.ts +16 -2
- package/src/run/schemas.ts +14 -0
- package/src/synth/compose-from-recording.ts +87 -0
- package/src/synth/deny-cases.ts +86 -1
- package/src/synth/evaluate.ts +41 -0
- package/src/synth/synthesize-from-mandate.ts +40 -4
- package/src/synth/synthesize-from-recording.ts +103 -4
- package/src/types.ts +10 -0
|
@@ -307,6 +307,10 @@ function lowerSelector(s) {
|
|
|
307
307
|
switch (s.kind) {
|
|
308
308
|
case 'arg':
|
|
309
309
|
return { kind: 'call_arg', index: s.argIndex };
|
|
310
|
+
case 'arg_len':
|
|
311
|
+
return { kind: 'call_arg_len', index: s.argIndex };
|
|
312
|
+
case 'arg_field':
|
|
313
|
+
return { kind: 'call_arg_field', index: s.argIndex, element: s.element, field: s.field };
|
|
310
314
|
case 'amount':
|
|
311
315
|
return { kind: 'amount', token: s.token };
|
|
312
316
|
case 'window_spent':
|
|
@@ -329,13 +333,15 @@ function lowerSelector(s) {
|
|
|
329
333
|
* selector kind to the matching `literal_*` kind. The IR compare value is a
|
|
330
334
|
* raw string (i128-safe); the selector kind fixes the canonical wire type:
|
|
331
335
|
* - arg -> IR scalarType (set by the recorder/parser)
|
|
336
|
+
* - arg_len -> u32 (vec length is a small non-negative integer)
|
|
337
|
+
* - arg_field -> IR scalarType (the field's recorded type)
|
|
332
338
|
* - amount -> i128 (canonical Stellar token amount encoding)
|
|
333
339
|
* - window_spent -> i128 (canonical amount encoding)
|
|
334
340
|
* - oracle_price -> i128 (canonical price encoding)
|
|
335
341
|
* - invocation_count -> u32 (counts are small non-negative integers)
|
|
336
342
|
* - now / valid_until -> u64 (unix timestamps in seconds) */
|
|
337
343
|
function literalFromIRCompare(c) {
|
|
338
|
-
const scalarType =
|
|
344
|
+
const scalarType = selectorScalarType(c.selector);
|
|
339
345
|
return literalFromScalar(c.value, scalarType);
|
|
340
346
|
}
|
|
341
347
|
function literalScalarForSelector(kind) {
|
|
@@ -345,14 +351,16 @@ function literalScalarForSelector(kind) {
|
|
|
345
351
|
case 'oracle_price':
|
|
346
352
|
return 'i128';
|
|
347
353
|
case 'invocation_count':
|
|
354
|
+
case 'arg_len':
|
|
348
355
|
return 'u32';
|
|
349
356
|
case 'now':
|
|
350
357
|
case 'valid_until':
|
|
351
358
|
return 'u64';
|
|
352
359
|
case 'arg':
|
|
360
|
+
case 'arg_field':
|
|
353
361
|
case 'calldata':
|
|
354
362
|
case 'value':
|
|
355
|
-
// arg -> caller handles scalarType; calldata/value -> unreachable (Path-B).
|
|
363
|
+
// arg / arg_field -> caller handles scalarType; calldata/value -> unreachable (Path-B).
|
|
356
364
|
return 'i128';
|
|
357
365
|
}
|
|
358
366
|
}
|
|
@@ -380,10 +388,12 @@ function literalFromScalar(value, scalarType) {
|
|
|
380
388
|
/** Scalar type of an IRSelector for the purpose of building literal leaves
|
|
381
389
|
* (the right-hand side of `eq` / elements of an `in` haystack / elements of
|
|
382
390
|
* a `literal_vec`). Mirrors `literalScalarForSelector` for OZ extensions and
|
|
383
|
-
* uses the selector's own `scalarType` for `arg` selectors. */
|
|
391
|
+
* uses the selector's own `scalarType` for `arg` and `arg_field` selectors. */
|
|
384
392
|
function selectorScalarType(selector) {
|
|
385
393
|
if (selector.kind === 'arg')
|
|
386
394
|
return selector.scalarType;
|
|
395
|
+
if (selector.kind === 'arg_field')
|
|
396
|
+
return selector.scalarType;
|
|
387
397
|
return literalScalarForSelector(selector.kind);
|
|
388
398
|
}
|
|
389
399
|
/** Walk a condition tree and throw if any oracle_price leaf is found anywhere
|
|
@@ -457,6 +467,10 @@ function describeCondition(cond) {
|
|
|
457
467
|
return `per-call amount comparison on ${s.token}`;
|
|
458
468
|
case 'arg':
|
|
459
469
|
return `argument comparison on arg ${s.argIndex}`;
|
|
470
|
+
case 'arg_len':
|
|
471
|
+
return `vec length comparison on arg ${s.argIndex}`;
|
|
472
|
+
case 'arg_field':
|
|
473
|
+
return `map field comparison on arg ${s.argIndex}.${s.field}`;
|
|
460
474
|
case 'calldata':
|
|
461
475
|
return 'EVM calldata comparison';
|
|
462
476
|
case 'value':
|
|
@@ -472,6 +486,10 @@ function describeSelector(s) {
|
|
|
472
486
|
switch (s.kind) {
|
|
473
487
|
case 'arg':
|
|
474
488
|
return `arg ${s.argIndex}`;
|
|
489
|
+
case 'arg_len':
|
|
490
|
+
return `arg_len(${s.argIndex})`;
|
|
491
|
+
case 'arg_field':
|
|
492
|
+
return `arg_field(${s.argIndex}, ${s.element}, ${s.field})`;
|
|
475
493
|
case 'amount':
|
|
476
494
|
return `amount(${s.token})`;
|
|
477
495
|
case 'window_spent':
|
|
@@ -229,6 +229,10 @@ function describeCondition(cond) {
|
|
|
229
229
|
return `per-call amount comparison on ${s.token} (predicate DSL)`;
|
|
230
230
|
case 'arg':
|
|
231
231
|
return `argument comparison on arg ${s.argIndex} (predicate DSL)`;
|
|
232
|
+
case 'arg_len':
|
|
233
|
+
return `vec length comparison on arg ${s.argIndex} (predicate DSL)`;
|
|
234
|
+
case 'arg_field':
|
|
235
|
+
return `map field comparison on arg ${s.argIndex}.${s.field} (predicate DSL)`;
|
|
232
236
|
case 'calldata':
|
|
233
237
|
return 'EVM calldata comparison (predicate DSL)';
|
|
234
238
|
case 'value':
|
|
@@ -244,6 +248,10 @@ function describeSelector(s) {
|
|
|
244
248
|
switch (s.kind) {
|
|
245
249
|
case 'arg':
|
|
246
250
|
return `arg ${s.argIndex}`;
|
|
251
|
+
case 'arg_len':
|
|
252
|
+
return `arg_len(${s.argIndex})`;
|
|
253
|
+
case 'arg_field':
|
|
254
|
+
return `arg_field(${s.argIndex}, ${s.element}, ${s.field})`;
|
|
247
255
|
case 'amount':
|
|
248
256
|
return `amount(${s.token})`;
|
|
249
257
|
case 'window_spent':
|
package/dist/ir/types.d.ts
CHANGED
|
@@ -14,6 +14,15 @@ export type IRSelector = {
|
|
|
14
14
|
fieldIndex?: number;
|
|
15
15
|
vecMode?: IRVecMode;
|
|
16
16
|
scalarType: IRScalarType;
|
|
17
|
+
} | {
|
|
18
|
+
kind: 'arg_len';
|
|
19
|
+
argIndex: number;
|
|
20
|
+
} | {
|
|
21
|
+
kind: 'arg_field';
|
|
22
|
+
argIndex: number;
|
|
23
|
+
element: number;
|
|
24
|
+
field: string;
|
|
25
|
+
scalarType: IRScalarType;
|
|
17
26
|
} | {
|
|
18
27
|
kind: 'calldata';
|
|
19
28
|
offset: number;
|
package/dist/predicate/encode.js
CHANGED
|
@@ -167,6 +167,15 @@ function encodeLeaf(leaf) {
|
|
|
167
167
|
return xdr.ScVal.scvVec([symbol('call_fn')]);
|
|
168
168
|
case 'call_arg':
|
|
169
169
|
return xdr.ScVal.scvVec([symbol('call_arg'), xdr.ScVal.scvU32(leaf.index)]);
|
|
170
|
+
case 'call_arg_len':
|
|
171
|
+
return xdr.ScVal.scvVec([symbol('call_arg_len'), xdr.ScVal.scvU32(leaf.index)]);
|
|
172
|
+
case 'call_arg_field':
|
|
173
|
+
return xdr.ScVal.scvVec([
|
|
174
|
+
symbol('call_arg_field'),
|
|
175
|
+
xdr.ScVal.scvU32(leaf.index),
|
|
176
|
+
xdr.ScVal.scvU32(leaf.element),
|
|
177
|
+
xdr.ScVal.scvSymbol(leaf.field),
|
|
178
|
+
]);
|
|
170
179
|
case 'amount':
|
|
171
180
|
return xdr.ScVal.scvVec([symbol('amount'), scvAddressFromStrkey(leaf.token)]);
|
|
172
181
|
case 'window_spent':
|
|
@@ -138,6 +138,36 @@ function renderComparison(node) {
|
|
|
138
138
|
if (left.kind === 'call_arg' && node.op === 'eq' && right.kind === 'literal_vec') {
|
|
139
139
|
return `Path must be exactly [${right.elements.map(renderVecElement).join(', ')}]`;
|
|
140
140
|
}
|
|
141
|
+
// eq(call_arg_len[i], literal_u32) -> Length of arg[i] is K
|
|
142
|
+
// Binds the OUTER vec length so a caller cannot append an element to
|
|
143
|
+
// defeat a per-element pin. Rendered as a single readable line so the
|
|
144
|
+
// review card surfaces the implicit "no new elements" guarantee.
|
|
145
|
+
if (left.kind === 'call_arg_len' && node.op === 'eq' && right.kind === 'literal_u32') {
|
|
146
|
+
return `Length of arg[${left.index}] is ${right.value}`;
|
|
147
|
+
}
|
|
148
|
+
// eq/lte(call_arg_field[i, el, field], <literal>) -> arg[i] element[el].field = <value>
|
|
149
|
+
// The structured-argument bind pins a scalar field inside a vec element.
|
|
150
|
+
// For lt/gt/lte/gte the line reads "OP <value>" so the comparison kind
|
|
151
|
+
// is visible; eq reads "= <value>".
|
|
152
|
+
if (left.kind === 'call_arg_field') {
|
|
153
|
+
const head = `arg[${left.index}] element[${left.element}].${left.field}`;
|
|
154
|
+
const sep = node.op === 'eq' ? '=' : comparisonOpText(node.op);
|
|
155
|
+
if (right.kind === 'literal_address')
|
|
156
|
+
return `${head} ${sep} ${right.value}`;
|
|
157
|
+
if (right.kind === 'literal_symbol')
|
|
158
|
+
return `${head} ${sep} ${right.value}`;
|
|
159
|
+
if (right.kind === 'literal_bytes')
|
|
160
|
+
return `${head} ${sep} ${right.value}`;
|
|
161
|
+
if (right.kind === 'literal_u64')
|
|
162
|
+
return `${head} ${sep} ${right.value}`;
|
|
163
|
+
if (right.kind === 'literal_i128')
|
|
164
|
+
return `${head} ${sep} ${right.value}`;
|
|
165
|
+
if (right.kind === 'literal_u32')
|
|
166
|
+
return `${head} ${sep} ${right.value}`;
|
|
167
|
+
if (right.kind === 'literal_vec') {
|
|
168
|
+
return `${head} ${sep} [${right.elements.map(renderVecElement).join(', ')}]`;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
141
171
|
// invocation_count_in_window <= N -> Invocations <= N per <window> seconds
|
|
142
172
|
if (left.kind === 'invocation_count_in_window' && right.kind === 'literal_u32') {
|
|
143
173
|
return `Invocations <= ${right.value} per ${left.windowSecs} seconds`;
|
|
@@ -184,6 +214,8 @@ function renderVecElement(leaf) {
|
|
|
184
214
|
case 'call_contract':
|
|
185
215
|
case 'call_fn':
|
|
186
216
|
case 'call_arg':
|
|
217
|
+
case 'call_arg_len':
|
|
218
|
+
case 'call_arg_field':
|
|
187
219
|
case 'amount':
|
|
188
220
|
case 'window_spent':
|
|
189
221
|
case 'now':
|
|
@@ -67,6 +67,44 @@ function pushComparison(left, right, op, out) {
|
|
|
67
67
|
out.push(`Path must be exactly [${right.elements.map(renderVecElement).join(', ')}]`);
|
|
68
68
|
return;
|
|
69
69
|
}
|
|
70
|
+
if (left.kind === 'call_arg_len' && op === 'eq' && right.kind === 'literal_u32') {
|
|
71
|
+
out.push(`Length of arg[${left.index}] is ${right.value}`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
// call_arg_field: must mirror the builder's templates byte-for-byte so
|
|
75
|
+
// the cross-check can assert a faithful summary.
|
|
76
|
+
if (left.kind === 'call_arg_field') {
|
|
77
|
+
const head = `arg[${left.index}] element[${left.element}].${left.field}`;
|
|
78
|
+
const sep = op === 'eq' ? '=' : comparisonOpText(op);
|
|
79
|
+
if (right.kind === 'literal_address') {
|
|
80
|
+
out.push(`${head} ${sep} ${right.value}`);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (right.kind === 'literal_symbol') {
|
|
84
|
+
out.push(`${head} ${sep} ${right.value}`);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
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
|
+
if (right.kind === 'literal_i128') {
|
|
96
|
+
out.push(`${head} ${sep} ${right.value}`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (right.kind === 'literal_u32') {
|
|
100
|
+
out.push(`${head} ${sep} ${right.value}`);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (right.kind === 'literal_vec') {
|
|
104
|
+
out.push(`${head} ${sep} [${right.elements.map(renderVecElement).join(', ')}]`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
70
108
|
if (left.kind === 'invocation_count_in_window' && right.kind === 'literal_u32') {
|
|
71
109
|
out.push(`Invocations <= ${right.value} per ${left.windowSecs} seconds`);
|
|
72
110
|
return;
|
|
@@ -105,6 +143,8 @@ function renderVecElement(leaf) {
|
|
|
105
143
|
case 'call_contract':
|
|
106
144
|
case 'call_fn':
|
|
107
145
|
case 'call_arg':
|
|
146
|
+
case 'call_arg_len':
|
|
147
|
+
case 'call_arg_field':
|
|
108
148
|
case 'amount':
|
|
109
149
|
case 'window_spent':
|
|
110
150
|
case 'now':
|
package/dist/run/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { type ErrorCode, type ProposedPolicy, type RecordedTransaction, type ToolError, type ToolResponse } from '../index.ts';
|
|
1
|
+
import { type ErrorCode, type PredicateNode, type ProposedPolicy, type RecordedTransaction, type ToolError, type ToolResponse } from '../index.ts';
|
|
2
|
+
import type { SimulationResult } from '../verify/envelope.ts';
|
|
2
3
|
import { type RecordTransactionInput, type SynthesizePolicyInput } from './schemas.ts';
|
|
3
4
|
export type { RecordTransactionInput, SynthesizePolicyInput } from './schemas.ts';
|
|
4
5
|
export { ComposeUserResponsesSchema, InterpreterOptionsSchema, MandateSpecSchema, NetworkSchema, OzAdapterConfigSchema, RecordedTransactionSchema, RecordTransactionInputSchema, SynthesizePolicyInputSchema, ToolErrorSchema, } from './schemas.ts';
|
|
@@ -28,7 +29,12 @@ export declare function runRecordTransaction(raw: unknown): Promise<ToolResponse
|
|
|
28
29
|
* `{ok:false, error}`, but a raw throw from the SDK (e.g. an unexpected
|
|
29
30
|
* XDR decode error in the adapter) would otherwise surface as
|
|
30
31
|
* "[object Object]" in the MCP transport. */
|
|
31
|
-
export declare function runSynthesizePolicy(raw: unknown): Promise<ToolResponse<ProposedPolicy
|
|
32
|
+
export declare function runSynthesizePolicy(raw: unknown): Promise<ToolResponse<ProposedPolicy> & {
|
|
33
|
+
explain?: {
|
|
34
|
+
predicateTree: PredicateNode | null;
|
|
35
|
+
simulation: SimulationResult;
|
|
36
|
+
};
|
|
37
|
+
}>;
|
|
32
38
|
/** Build a canonical ToolError for a thrown exception caught by the tool
|
|
33
39
|
* envelope. The MCP SDK stringifies thrown objects as "[object Object]" by
|
|
34
40
|
* default, so we extract a string-friendly message and tag the original
|
package/dist/run/index.js
CHANGED
|
@@ -89,7 +89,7 @@ export async function runSynthesizePolicy(raw) {
|
|
|
89
89
|
// Zod's optional fields widen to `T | undefined`, which the core's
|
|
90
90
|
// exact-optional MandateSpec rejects; the schema already validated the
|
|
91
91
|
// shape, so assert it (same pattern as the recordedTx cast below).
|
|
92
|
-
return await synthesizeFromMandate(input.mandate, ozConfig);
|
|
92
|
+
return await synthesizeFromMandate(input.mandate, ozConfig, input.explain === true ? { explain: true } : {});
|
|
93
93
|
}
|
|
94
94
|
// recording source
|
|
95
95
|
const recorded = input.recordedTx;
|
|
@@ -100,6 +100,7 @@ export async function runSynthesizePolicy(raw) {
|
|
|
100
100
|
? { confidenceOverride: input.confidenceOverride }
|
|
101
101
|
: {}),
|
|
102
102
|
...(input.interpreter !== undefined ? { interpreter: input.interpreter } : {}),
|
|
103
|
+
...(input.explain === true ? { explain: true } : {}),
|
|
103
104
|
}, ozConfig);
|
|
104
105
|
}
|
|
105
106
|
catch (e) {
|
package/dist/run/schemas.d.ts
CHANGED
|
@@ -648,6 +648,7 @@ export declare const SynthesizePolicyMandateInputSchema: z.ZodObject<{
|
|
|
648
648
|
weighted_threshold: string;
|
|
649
649
|
};
|
|
650
650
|
}>>;
|
|
651
|
+
explain: z.ZodOptional<z.ZodBoolean>;
|
|
651
652
|
}, "strip", z.ZodTypeAny, {
|
|
652
653
|
source: "mandate";
|
|
653
654
|
mandate: {
|
|
@@ -676,6 +677,7 @@ export declare const SynthesizePolicyMandateInputSchema: z.ZodObject<{
|
|
|
676
677
|
weighted_threshold: string;
|
|
677
678
|
};
|
|
678
679
|
} | undefined;
|
|
680
|
+
explain?: boolean | undefined;
|
|
679
681
|
}, {
|
|
680
682
|
source: "mandate";
|
|
681
683
|
mandate: {
|
|
@@ -704,6 +706,7 @@ export declare const SynthesizePolicyMandateInputSchema: z.ZodObject<{
|
|
|
704
706
|
weighted_threshold: string;
|
|
705
707
|
};
|
|
706
708
|
} | undefined;
|
|
709
|
+
explain?: boolean | undefined;
|
|
707
710
|
}>;
|
|
708
711
|
/** Interpreter opt-in for the recording path. Present -> constraints OZ cannot
|
|
709
712
|
* express (per-method scoping, invocation-count windows, oracle bounds, exact
|
|
@@ -1085,6 +1088,7 @@ export declare const SynthesizePolicyRecordingInputSchema: z.ZodObject<{
|
|
|
1085
1088
|
weighted_threshold: string;
|
|
1086
1089
|
};
|
|
1087
1090
|
}>>;
|
|
1091
|
+
explain: z.ZodOptional<z.ZodBoolean>;
|
|
1088
1092
|
}, "strip", z.ZodTypeAny, {
|
|
1089
1093
|
network: "mainnet" | "testnet";
|
|
1090
1094
|
source: "recording";
|
|
@@ -1149,6 +1153,7 @@ export declare const SynthesizePolicyRecordingInputSchema: z.ZodObject<{
|
|
|
1149
1153
|
weighted_threshold: string;
|
|
1150
1154
|
};
|
|
1151
1155
|
} | undefined;
|
|
1156
|
+
explain?: boolean | undefined;
|
|
1152
1157
|
}, {
|
|
1153
1158
|
network: "mainnet" | "testnet";
|
|
1154
1159
|
source: "recording";
|
|
@@ -1213,6 +1218,7 @@ export declare const SynthesizePolicyRecordingInputSchema: z.ZodObject<{
|
|
|
1213
1218
|
weighted_threshold: string;
|
|
1214
1219
|
};
|
|
1215
1220
|
} | undefined;
|
|
1221
|
+
explain?: boolean | undefined;
|
|
1216
1222
|
}>;
|
|
1217
1223
|
export declare const SynthesizePolicyInputSchema: z.ZodDiscriminatedUnion<"source", [z.ZodObject<{
|
|
1218
1224
|
source: z.ZodLiteral<"mandate">;
|
|
@@ -1334,6 +1340,7 @@ export declare const SynthesizePolicyInputSchema: z.ZodDiscriminatedUnion<"sourc
|
|
|
1334
1340
|
weighted_threshold: string;
|
|
1335
1341
|
};
|
|
1336
1342
|
}>>;
|
|
1343
|
+
explain: z.ZodOptional<z.ZodBoolean>;
|
|
1337
1344
|
}, "strip", z.ZodTypeAny, {
|
|
1338
1345
|
source: "mandate";
|
|
1339
1346
|
mandate: {
|
|
@@ -1362,6 +1369,7 @@ export declare const SynthesizePolicyInputSchema: z.ZodDiscriminatedUnion<"sourc
|
|
|
1362
1369
|
weighted_threshold: string;
|
|
1363
1370
|
};
|
|
1364
1371
|
} | undefined;
|
|
1372
|
+
explain?: boolean | undefined;
|
|
1365
1373
|
}, {
|
|
1366
1374
|
source: "mandate";
|
|
1367
1375
|
mandate: {
|
|
@@ -1390,6 +1398,7 @@ export declare const SynthesizePolicyInputSchema: z.ZodDiscriminatedUnion<"sourc
|
|
|
1390
1398
|
weighted_threshold: string;
|
|
1391
1399
|
};
|
|
1392
1400
|
} | undefined;
|
|
1401
|
+
explain?: boolean | undefined;
|
|
1393
1402
|
}>, z.ZodObject<{
|
|
1394
1403
|
source: z.ZodLiteral<"recording">;
|
|
1395
1404
|
recordedTx: z.ZodObject<{
|
|
@@ -1736,6 +1745,7 @@ export declare const SynthesizePolicyInputSchema: z.ZodDiscriminatedUnion<"sourc
|
|
|
1736
1745
|
weighted_threshold: string;
|
|
1737
1746
|
};
|
|
1738
1747
|
}>>;
|
|
1748
|
+
explain: z.ZodOptional<z.ZodBoolean>;
|
|
1739
1749
|
}, "strip", z.ZodTypeAny, {
|
|
1740
1750
|
network: "mainnet" | "testnet";
|
|
1741
1751
|
source: "recording";
|
|
@@ -1800,6 +1810,7 @@ export declare const SynthesizePolicyInputSchema: z.ZodDiscriminatedUnion<"sourc
|
|
|
1800
1810
|
weighted_threshold: string;
|
|
1801
1811
|
};
|
|
1802
1812
|
} | undefined;
|
|
1813
|
+
explain?: boolean | undefined;
|
|
1803
1814
|
}, {
|
|
1804
1815
|
network: "mainnet" | "testnet";
|
|
1805
1816
|
source: "recording";
|
|
@@ -1864,6 +1875,7 @@ export declare const SynthesizePolicyInputSchema: z.ZodDiscriminatedUnion<"sourc
|
|
|
1864
1875
|
weighted_threshold: string;
|
|
1865
1876
|
};
|
|
1866
1877
|
} | undefined;
|
|
1878
|
+
explain?: boolean | undefined;
|
|
1867
1879
|
}>]>;
|
|
1868
1880
|
export type SynthesizePolicyInput = z.infer<typeof SynthesizePolicyInputSchema>;
|
|
1869
1881
|
export declare const ToolErrorSchema: z.ZodObject<{
|
package/dist/run/schemas.js
CHANGED
|
@@ -175,6 +175,11 @@ export const SynthesizePolicyMandateInputSchema = z.object({
|
|
|
175
175
|
source: z.literal('mandate'),
|
|
176
176
|
mandate: MandateSpecSchema,
|
|
177
177
|
ozConfig: OzAdapterConfigSchema.optional(),
|
|
178
|
+
// --explain opt-in. When true, the orchestrator attaches the
|
|
179
|
+
// in-memory predicate tree (null for the mandate path) + a minimal
|
|
180
|
+
// honest SimulationResult to the success envelope. Absent or false
|
|
181
|
+
// -> the success envelope is unchanged (byte-identical to today).
|
|
182
|
+
explain: z.boolean().optional(),
|
|
178
183
|
});
|
|
179
184
|
/** Interpreter opt-in for the recording path. Present -> constraints OZ cannot
|
|
180
185
|
* express (per-method scoping, invocation-count windows, oracle bounds, exact
|
|
@@ -200,6 +205,15 @@ export const SynthesizePolicyRecordingInputSchema = z.object({
|
|
|
200
205
|
confidenceOverride: z.object({ threshold: z.number().min(0).max(1) }).optional(),
|
|
201
206
|
interpreter: InterpreterOptionsSchema.optional(),
|
|
202
207
|
ozConfig: OzAdapterConfigSchema.optional(),
|
|
208
|
+
// --explain opt-in. When true, the orchestrator attaches the
|
|
209
|
+
// in-memory PredicateNode + the corresponding SimulationResult
|
|
210
|
+
// (real one from the self-verify pipeline when the interpreter is
|
|
211
|
+
// engaged, minimal honest value otherwise) to the success envelope.
|
|
212
|
+
// Absent or false -> the success envelope is unchanged (byte-identical
|
|
213
|
+
// to today). The flag is ADDITIVE: the existing ProposedPolicy fields
|
|
214
|
+
// (encodedPredicate, predicateHash, etc.) are never altered by enabling
|
|
215
|
+
// explain.
|
|
216
|
+
explain: z.boolean().optional(),
|
|
203
217
|
});
|
|
204
218
|
export const SynthesizePolicyInputSchema = z.discriminatedUnion('source', [
|
|
205
219
|
SynthesizePolicyMandateInputSchema,
|
|
@@ -276,6 +276,95 @@ function appendProtocolSpecificConstraints(ozConstraints, interpreterConstraints
|
|
|
276
276
|
}
|
|
277
277
|
}
|
|
278
278
|
}
|
|
279
|
+
// Blend `submit` ONLY (not `claim`, whose vec arg is a vec<u32> of
|
|
280
|
+
// reserve_token_ids - no map fields to bind). The `requests` vec
|
|
281
|
+
// (call_arg[3]) is a vec<Request{ address, amount, request_type }>. Each
|
|
282
|
+
// Request is the per-reserve action selector - 0 Supply, 1 Withdraw,
|
|
283
|
+
// 2 SupplyCollateral, 3 WithdrawCollateral, 4 Borrow, 5 Repay, 6-9
|
|
284
|
+
// liquidation/auction fills. Pinning only one element is unsafe: a caller
|
|
285
|
+
// can append a second element with a different action (WithdrawCollateral
|
|
286
|
+
// -> Borrow on a different asset, any amount, then auction fills). Length
|
|
287
|
+
// + per-element pinning is total; a quantifier over elements is not. If we
|
|
288
|
+
// cannot emit BOTH, we surface AMBIGUITY rather than emit a partial bind.
|
|
289
|
+
if (protocol.protocol === 'blend' && protocol.fn === 'submit' && interpreterEnabled) {
|
|
290
|
+
const requestsArg = topLevel.args[3];
|
|
291
|
+
if (requestsArg && requestsArg.type === 'vec') {
|
|
292
|
+
const elements = requestsArg.value;
|
|
293
|
+
interpreterConstraints.push({
|
|
294
|
+
op: 'compare',
|
|
295
|
+
compare: {
|
|
296
|
+
selector: { kind: 'arg_len', argIndex: 3 },
|
|
297
|
+
operator: 'eq',
|
|
298
|
+
value: String(elements.length),
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
for (let i = 0; i < elements.length; i++) {
|
|
302
|
+
const element = elements[i];
|
|
303
|
+
if (!element || element.type !== 'map')
|
|
304
|
+
continue;
|
|
305
|
+
const fields = element.value;
|
|
306
|
+
const fieldValue = (name, scalarType) => {
|
|
307
|
+
const entry = fields.find((e) => e.key === name);
|
|
308
|
+
if (!entry)
|
|
309
|
+
return null;
|
|
310
|
+
if (entry.val.type === 'address' && scalarType === 'address')
|
|
311
|
+
return entry.val.value;
|
|
312
|
+
if (entry.val.type === 'i128' && scalarType === 'i128')
|
|
313
|
+
return entry.val.value;
|
|
314
|
+
if (entry.val.type === 'u32' && scalarType === 'u32')
|
|
315
|
+
return entry.val.value;
|
|
316
|
+
return null;
|
|
317
|
+
};
|
|
318
|
+
const address = fieldValue('address', 'address');
|
|
319
|
+
const amount = fieldValue('amount', 'i128');
|
|
320
|
+
const requestType = fieldValue('request_type', 'u32');
|
|
321
|
+
if (address === null || amount === null || requestType === null)
|
|
322
|
+
continue;
|
|
323
|
+
interpreterConstraints.push({
|
|
324
|
+
op: 'compare',
|
|
325
|
+
compare: {
|
|
326
|
+
selector: {
|
|
327
|
+
kind: 'arg_field',
|
|
328
|
+
argIndex: 3,
|
|
329
|
+
element: i,
|
|
330
|
+
field: 'request_type',
|
|
331
|
+
scalarType: 'u32',
|
|
332
|
+
},
|
|
333
|
+
operator: 'eq',
|
|
334
|
+
value: requestType,
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
interpreterConstraints.push({
|
|
338
|
+
op: 'compare',
|
|
339
|
+
compare: {
|
|
340
|
+
selector: {
|
|
341
|
+
kind: 'arg_field',
|
|
342
|
+
argIndex: 3,
|
|
343
|
+
element: i,
|
|
344
|
+
field: 'address',
|
|
345
|
+
scalarType: 'address',
|
|
346
|
+
},
|
|
347
|
+
operator: 'eq',
|
|
348
|
+
value: address,
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
interpreterConstraints.push({
|
|
352
|
+
op: 'compare',
|
|
353
|
+
compare: {
|
|
354
|
+
selector: {
|
|
355
|
+
kind: 'arg_field',
|
|
356
|
+
argIndex: 3,
|
|
357
|
+
element: i,
|
|
358
|
+
field: 'amount',
|
|
359
|
+
scalarType: 'i128',
|
|
360
|
+
},
|
|
361
|
+
operator: 'lte',
|
|
362
|
+
value: amount,
|
|
363
|
+
},
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
279
368
|
// SoroSwap: the recorded hop path -> eq_seq on the path arg (call_arg[2]).
|
|
280
369
|
// The interpreter adapter is the ONLY way to express an exact ordered
|
|
281
370
|
// sequence (OZ built-ins cannot). Element order is preserved verbatim.
|
package/dist/synth/deny-cases.js
CHANGED
|
@@ -16,6 +16,15 @@ const ADJACENT_ASSETS = [
|
|
|
16
16
|
// them as FINDINGS against the already-emitted policy.
|
|
17
17
|
const OVERPERMISSIVE_DIMENSIONS = ['argument_reorder'];
|
|
18
18
|
// The 15 dimensions the synth pipeline uses for self-verify and minimise.
|
|
19
|
+
// Phase 1 grammar extension: `vec_append` and `map_field_flip` are listed below
|
|
20
|
+
// alongside the existing dimensions so the per-element binds emitted for
|
|
21
|
+
// Blend `submit` (call_arg_len + 3 call_arg_field per element) survive
|
|
22
|
+
// minimise. Without these, no deny case exercises the new leaves and the
|
|
23
|
+
// minimise pass drops them as "redundant" - reopening the element-append
|
|
24
|
+
// hole they were added to close. Both dimensions are no-ops for any fixture
|
|
25
|
+
// whose predicate has no `call_arg_len` / `call_arg_field` leaves (the
|
|
26
|
+
// generator short-circuits on leaf-kind), so they cannot make production
|
|
27
|
+
// synthesis refuse for fixtures that do not exercise the new grammar.
|
|
19
28
|
const ORIGINAL_DIMENSIONS = [
|
|
20
29
|
'amount',
|
|
21
30
|
'asset',
|
|
@@ -32,6 +41,8 @@ const ORIGINAL_DIMENSIONS = [
|
|
|
32
41
|
'oracle_deviation_exceeded',
|
|
33
42
|
'oracle_paused',
|
|
34
43
|
'soroswap_allowed_path',
|
|
44
|
+
'vec_append',
|
|
45
|
+
'map_field_flip',
|
|
35
46
|
];
|
|
36
47
|
export { ORIGINAL_DIMENSIONS, OVERPERMISSIVE_DIMENSIONS };
|
|
37
48
|
/** Build deterministic model-evaluated alternatives without mutating the intended call.
|
|
@@ -191,7 +202,9 @@ export function generateCases(predicate, permitCtx, dimensions) {
|
|
|
191
202
|
hasConstrainedArg &&
|
|
192
203
|
permitCtx.args.length >= 2 &&
|
|
193
204
|
permitCtx.args[0]?.type === 'address' &&
|
|
194
|
-
permitCtx.args[1]?.type === 'address'
|
|
205
|
+
permitCtx.args[1]?.type === 'address' &&
|
|
206
|
+
permitCtx.args[0].value !==
|
|
207
|
+
permitCtx.args[1].value) {
|
|
195
208
|
const ctx = cloneContext(permitCtx);
|
|
196
209
|
// Guard above guarantees args[0] and args[1] exist and are address-typed.
|
|
197
210
|
const a0 = ctx.args[0];
|
|
@@ -200,6 +213,59 @@ export function generateCases(predicate, permitCtx, dimensions) {
|
|
|
200
213
|
ctx.args[1] = a0;
|
|
201
214
|
denies.push({ dimension: 'argument_reorder', ctx });
|
|
202
215
|
}
|
|
216
|
+
// --- map_field_flip: flip a bound map field to a different valid value of
|
|
217
|
+
// the same type. Mirrors argument_reorder: OPT-IN only, never ORIGINAL.
|
|
218
|
+
// Targets each `call_arg_field` leaf and produces a ctx whose vec element
|
|
219
|
+
// has a different value of the recorded field (different request_type,
|
|
220
|
+
// different amount, different address). The blend-submit case uses this
|
|
221
|
+
// to detect a missing request_type pin. ---
|
|
222
|
+
if (!dimensions || dimensions.includes('map_field_flip')) {
|
|
223
|
+
for (const comparison of facts.comparisons) {
|
|
224
|
+
const sel = comparison.left;
|
|
225
|
+
if (sel.kind !== 'call_arg_field')
|
|
226
|
+
continue;
|
|
227
|
+
const arg = permitCtx.args[sel.index];
|
|
228
|
+
if (!arg || arg.type !== 'vec')
|
|
229
|
+
continue;
|
|
230
|
+
const element = arg.value[sel.element];
|
|
231
|
+
if (!element || element.type !== 'map' || !Array.isArray(element.value))
|
|
232
|
+
continue;
|
|
233
|
+
const entry = element.value.find((e) => e.key === sel.field);
|
|
234
|
+
if (!entry)
|
|
235
|
+
continue;
|
|
236
|
+
const flipped = flipFieldValue(entry.val);
|
|
237
|
+
if (flipped === null)
|
|
238
|
+
continue;
|
|
239
|
+
const ctx = cloneContext(permitCtx);
|
|
240
|
+
const clonedVec = arg.value.map(cloneScVal);
|
|
241
|
+
const clonedElement = clonedVec[sel.element];
|
|
242
|
+
if (clonedElement?.type !== 'map' || !Array.isArray(clonedElement.value))
|
|
243
|
+
continue;
|
|
244
|
+
clonedElement.value = clonedElement.value.map((e) => e.key === sel.field ? { key: e.key, val: flipped } : e);
|
|
245
|
+
clonedVec[sel.element] = clonedElement;
|
|
246
|
+
ctx.args[sel.index] = { type: 'vec', value: clonedVec };
|
|
247
|
+
denies.push({ dimension: 'map_field_flip', ctx });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// --- vec_append: append a new element to a bound vec. Mirrors map_field_flip:
|
|
251
|
+
// OPT-IN only, never ORIGINAL. Targets every `call_arg_len` leaf; without the
|
|
252
|
+
// length pin a caller can append an extra element to defeat per-element binds. ---
|
|
253
|
+
if (!dimensions || dimensions.includes('vec_append')) {
|
|
254
|
+
for (const comparison of facts.comparisons) {
|
|
255
|
+
const sel = comparison.left;
|
|
256
|
+
if (sel.kind !== 'call_arg_len')
|
|
257
|
+
continue;
|
|
258
|
+
const arg = permitCtx.args[sel.index];
|
|
259
|
+
if (!arg || arg.type !== 'vec')
|
|
260
|
+
continue;
|
|
261
|
+
const ctx = cloneContext(permitCtx);
|
|
262
|
+
ctx.args[sel.index] = {
|
|
263
|
+
type: 'vec',
|
|
264
|
+
value: [...arg.value, { type: 'other', value: 'deny-case-vec-append' }],
|
|
265
|
+
};
|
|
266
|
+
denies.push({ dimension: 'vec_append', ctx });
|
|
267
|
+
}
|
|
268
|
+
}
|
|
203
269
|
// Version mismatch, malformed predicates, master authorization, and nonce replay are install-time checks and are intentionally omitted from model-evaluated cases.
|
|
204
270
|
return { permit: cloneContext(permitCtx), denies };
|
|
205
271
|
}
|
|
@@ -431,3 +497,25 @@ function cloneDepthError(value) {
|
|
|
431
497
|
err.depthContext = value.type;
|
|
432
498
|
throw err;
|
|
433
499
|
}
|
|
500
|
+
/** Build a value of the SAME ScVal type that differs from the recorded one,
|
|
501
|
+
* used by the `map_field_flip` mutation to defeat a per-element pin without
|
|
502
|
+
* changing the wire type (a type/arity change is rejected by host dispatch
|
|
503
|
+
* before Policy::enforce, so it is not the predicate's job). Returns null
|
|
504
|
+
* when the ScVal type has no obvious different value (e.g. opaque/other). */
|
|
505
|
+
function flipFieldValue(val) {
|
|
506
|
+
switch (val.type) {
|
|
507
|
+
case 'address': {
|
|
508
|
+
const a = 'GBFKRGJYZXLTDEI36ZCQEIM225NMOCR2VDBOIHJTXJ54FEFFVL2FKALE';
|
|
509
|
+
const b = 'GD6XSMQJ47EHHJOWXQOND5YDVZC37JWZJHYHBKE6QJFSLLJ5KQXM5QS5';
|
|
510
|
+
return { type: 'address', value: val.value === a ? b : a };
|
|
511
|
+
}
|
|
512
|
+
case 'i128':
|
|
513
|
+
case 'u64':
|
|
514
|
+
case 'u32':
|
|
515
|
+
return { type: val.type, value: String(BigInt(val.value) + 1n) };
|
|
516
|
+
case 'symbol':
|
|
517
|
+
return { type: 'symbol', value: `${val.value}x` };
|
|
518
|
+
default:
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
}
|