@crediolabs/policy-synth 0.1.9 → 0.1.11
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 +62 -11
- package/dist/errors.d.ts +1 -1
- package/dist/install/build-add-context-rule.d.ts +48 -0
- package/dist/install/build-add-context-rule.js +304 -0
- package/dist/ir/types.d.ts +4 -0
- package/dist/predicate/decode.d.ts +11 -0
- package/dist/predicate/decode.js +234 -0
- package/dist/predicate/encode.js +79 -14
- package/dist/predicate/from-json.d.ts +4 -0
- package/dist/predicate/from-json.js +113 -0
- package/dist/predicate/index.d.ts +2 -0
- package/dist/predicate/index.js +5 -1
- package/dist/registry/identify.js +21 -2
- package/dist/registry/protocols.d.ts +50 -1
- package/dist/registry/protocols.js +88 -0
- package/dist/review-card/builder.d.ts +13 -0
- package/dist/review-card/builder.js +61 -5
- package/dist/review-card/cross-check.js +50 -3
- package/dist/review-card/index.d.ts +1 -1
- package/dist/review-card/index.js +1 -1
- package/dist/run/index.d.ts +8 -2
- package/dist/run/index.js +2 -1
- package/dist/run/schemas.d.ts +16 -4
- package/dist/run/schemas.js +14 -0
- package/dist/synth/compose-from-recording.d.ts +4 -0
- package/dist/synth/compose-from-recording.js +34 -8
- package/dist/synth/deny-cases.d.ts +4 -1
- package/dist/synth/deny-cases.js +3 -1
- package/dist/synth/evaluate.js +130 -3
- package/dist/synth/permit-context.d.ts +15 -0
- package/dist/synth/permit-context.js +116 -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 +80 -3
- package/dist/types.d.ts +9 -0
- package/dist-cjs/adapters/interpreter/adapter.js +62 -11
- package/dist-cjs/errors.d.ts +1 -1
- package/dist-cjs/install/build-add-context-rule.d.ts +48 -0
- package/dist-cjs/install/build-add-context-rule.js +308 -0
- package/dist-cjs/ir/types.d.ts +4 -0
- package/dist-cjs/predicate/decode.d.ts +11 -0
- package/dist-cjs/predicate/decode.js +239 -0
- package/dist-cjs/predicate/encode.js +79 -14
- package/dist-cjs/predicate/from-json.d.ts +4 -0
- package/dist-cjs/predicate/from-json.js +116 -0
- package/dist-cjs/predicate/index.d.ts +2 -0
- package/dist-cjs/predicate/index.js +10 -2
- package/dist-cjs/registry/identify.js +20 -1
- package/dist-cjs/registry/protocols.d.ts +50 -1
- package/dist-cjs/registry/protocols.js +89 -1
- package/dist-cjs/review-card/builder.d.ts +13 -0
- package/dist-cjs/review-card/builder.js +62 -5
- package/dist-cjs/review-card/cross-check.js +50 -3
- package/dist-cjs/review-card/index.d.ts +1 -1
- package/dist-cjs/review-card/index.js +2 -1
- package/dist-cjs/run/index.d.ts +8 -2
- package/dist-cjs/run/index.js +2 -1
- package/dist-cjs/run/schemas.d.ts +16 -4
- package/dist-cjs/run/schemas.js +14 -0
- package/dist-cjs/synth/compose-from-recording.d.ts +4 -0
- package/dist-cjs/synth/compose-from-recording.js +34 -8
- package/dist-cjs/synth/deny-cases.d.ts +4 -1
- package/dist-cjs/synth/deny-cases.js +3 -0
- package/dist-cjs/synth/evaluate.js +130 -3
- package/dist-cjs/synth/permit-context.d.ts +15 -0
- package/dist-cjs/synth/permit-context.js +119 -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 +80 -3
- package/dist-cjs/types.d.ts +9 -0
- package/package.json +5 -2
- package/src/adapters/interpreter/adapter.ts +72 -11
- package/src/contracts/policy-template/OZ_POLICY_TRAIT.md +28 -3
- package/src/errors.ts +13 -0
- package/src/install/build-add-context-rule.ts +429 -0
- package/src/ir/types.ts +4 -0
- package/src/predicate/decode.ts +242 -0
- package/src/predicate/encode.ts +110 -13
- package/src/predicate/from-json.ts +124 -0
- package/src/predicate/index.ts +5 -1
- package/src/registry/identify.ts +22 -2
- package/src/registry/protocols.ts +90 -1
- package/src/review-card/builder.ts +57 -5
- package/src/review-card/cross-check.ts +50 -3
- package/src/review-card/index.ts +1 -0
- package/src/run/index.ts +16 -2
- package/src/run/schemas.ts +14 -0
- package/src/synth/compose-from-recording.ts +46 -7
- package/src/synth/deny-cases.ts +3 -1
- package/src/synth/evaluate.ts +131 -3
- package/src/synth/permit-context.ts +137 -0
- package/src/synth/synthesize-from-mandate.ts +40 -4
- package/src/synth/synthesize-from-recording.ts +116 -6
- package/src/types.ts +12 -0
|
@@ -70,16 +70,57 @@ function pushComparison(left, right, op, out) {
|
|
|
70
70
|
out.push(`Path must be exactly [${right.elements.map(renderVecElement).join(', ')}]`);
|
|
71
71
|
return;
|
|
72
72
|
}
|
|
73
|
+
if (left.kind === 'call_arg_len' && op === 'eq' && right.kind === 'literal_u32') {
|
|
74
|
+
out.push(`Length of arg[${left.index}] is ${right.value}`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
// call_arg_field: must mirror the builder's templates byte-for-byte so
|
|
78
|
+
// the cross-check can assert a faithful summary.
|
|
79
|
+
if (left.kind === 'call_arg_field') {
|
|
80
|
+
const head = `arg[${left.index}] element[${left.element}].${left.field}`;
|
|
81
|
+
const sep = op === 'eq' ? '=' : comparisonOpText(op);
|
|
82
|
+
if (right.kind === 'literal_address') {
|
|
83
|
+
out.push(`${head} ${sep} ${right.value}`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (right.kind === 'literal_symbol') {
|
|
87
|
+
out.push(`${head} ${sep} ${right.value}`);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (right.kind === 'literal_bytes') {
|
|
91
|
+
out.push(`${head} ${sep} ${right.value}`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (right.kind === 'literal_u64') {
|
|
95
|
+
out.push(`${head} ${sep} ${right.value}`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (right.kind === 'literal_i128') {
|
|
99
|
+
out.push(`${head} ${sep} ${right.value}`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (right.kind === 'literal_u32') {
|
|
103
|
+
out.push(`${head} ${sep} ${right.value}`);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (right.kind === 'literal_vec') {
|
|
107
|
+
out.push(`${head} ${sep} [${right.elements.map(renderVecElement).join(', ')}]`);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Mirrors the builder's wording byte-for-byte: how many calls the rule
|
|
112
|
+
// permits, since the bound counts the calls already made.
|
|
73
113
|
if (left.kind === 'invocation_count_in_window' && right.kind === 'literal_u32') {
|
|
74
|
-
|
|
114
|
+
const allowed = op === 'lt' ? right.value : right.value + 1;
|
|
115
|
+
out.push(`At most ${allowed} calls per ${left.windowSecs} seconds`);
|
|
75
116
|
return;
|
|
76
117
|
}
|
|
77
118
|
if (left.kind === 'amount' && right.kind === 'literal_i128') {
|
|
78
119
|
out.push(`Amount <= ${right.value}`);
|
|
79
120
|
return;
|
|
80
121
|
}
|
|
81
|
-
if (left.kind === 'oracle_price' && right.kind === '
|
|
82
|
-
out.push(`Only when oracle_price(${left.asset}) ${comparisonOpText(op)} ${right.value}`);
|
|
122
|
+
if (left.kind === 'oracle_price' && right.kind === 'oracle_threshold') {
|
|
123
|
+
out.push(`Only when oracle_price(${left.asset}) ${comparisonOpText(op)} ${right.value} (${right.decimals} dp)`);
|
|
83
124
|
return;
|
|
84
125
|
}
|
|
85
126
|
}
|
|
@@ -110,6 +151,7 @@ function renderVecElement(leaf) {
|
|
|
110
151
|
case 'call_arg':
|
|
111
152
|
case 'call_arg_len':
|
|
112
153
|
case 'call_arg_field':
|
|
154
|
+
case 'call_arg_scaled':
|
|
113
155
|
case 'amount':
|
|
114
156
|
case 'window_spent':
|
|
115
157
|
case 'now':
|
|
@@ -117,6 +159,11 @@ function renderVecElement(leaf) {
|
|
|
117
159
|
case 'invocation_count_in_window':
|
|
118
160
|
case 'oracle_price':
|
|
119
161
|
return `<${leaf.kind}>`;
|
|
162
|
+
// Show the declared basis, not just the digits. A threshold on the wrong
|
|
163
|
+
// basis is the one policy error the contract cannot detect, so the review
|
|
164
|
+
// card is where a human has to be able to see it.
|
|
165
|
+
case 'oracle_threshold':
|
|
166
|
+
return `${leaf.value} (${leaf.decimals} dp)`;
|
|
120
167
|
}
|
|
121
168
|
}
|
|
122
169
|
function renderHaystackElement(leaf) {
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { buildReviewCardSummary, type ReviewCardSummary, } from './builder.ts';
|
|
1
|
+
export { buildReviewCardSummary, describePredicate, type ReviewCardSummary, } from './builder.ts';
|
|
2
2
|
export { type ConflictAnnotation, classifyConflict, type RuleRef, } from './conflict.ts';
|
|
3
3
|
export { summaryCrossCheck } from './cross-check.ts';
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// src/review-card/index.ts - re-export the deterministic review-card surface.
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
exports.summaryCrossCheck = exports.classifyConflict = exports.buildReviewCardSummary = void 0;
|
|
4
|
+
exports.summaryCrossCheck = exports.classifyConflict = exports.describePredicate = exports.buildReviewCardSummary = void 0;
|
|
5
5
|
var builder_ts_1 = require("./builder.js");
|
|
6
6
|
Object.defineProperty(exports, "buildReviewCardSummary", { enumerable: true, get: function () { return builder_ts_1.buildReviewCardSummary; } });
|
|
7
|
+
Object.defineProperty(exports, "describePredicate", { enumerable: true, get: function () { return builder_ts_1.describePredicate; } });
|
|
7
8
|
var conflict_ts_1 = require("./conflict.js");
|
|
8
9
|
Object.defineProperty(exports, "classifyConflict", { enumerable: true, get: function () { return conflict_ts_1.classifyConflict; } });
|
|
9
10
|
var cross_check_ts_1 = require("./cross-check.js");
|
package/dist-cjs/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-cjs/run/index.js
CHANGED
|
@@ -104,7 +104,7 @@ async function runSynthesizePolicy(raw) {
|
|
|
104
104
|
// Zod's optional fields widen to `T | undefined`, which the core's
|
|
105
105
|
// exact-optional MandateSpec rejects; the schema already validated the
|
|
106
106
|
// shape, so assert it (same pattern as the recordedTx cast below).
|
|
107
|
-
return await (0, index_ts_1.synthesizeFromMandate)(input.mandate, ozConfig);
|
|
107
|
+
return await (0, index_ts_1.synthesizeFromMandate)(input.mandate, ozConfig, input.explain === true ? { explain: true } : {});
|
|
108
108
|
}
|
|
109
109
|
// recording source
|
|
110
110
|
const recorded = input.recordedTx;
|
|
@@ -115,6 +115,7 @@ async function runSynthesizePolicy(raw) {
|
|
|
115
115
|
? { confidenceOverride: input.confidenceOverride }
|
|
116
116
|
: {}),
|
|
117
117
|
...(input.interpreter !== undefined ? { interpreter: input.interpreter } : {}),
|
|
118
|
+
...(input.explain === true ? { explain: true } : {}),
|
|
118
119
|
}, ozConfig);
|
|
119
120
|
}
|
|
120
121
|
catch (e) {
|
|
@@ -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,17 +1088,18 @@ 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";
|
|
1091
1095
|
recordedTx: {
|
|
1096
|
+
signers: string[];
|
|
1092
1097
|
events: {
|
|
1093
1098
|
contract: string;
|
|
1094
1099
|
topics: string[];
|
|
1095
1100
|
data?: unknown;
|
|
1096
1101
|
}[];
|
|
1097
1102
|
network: "mainnet" | "testnet";
|
|
1098
|
-
signers: string[];
|
|
1099
1103
|
invocations: unknown[];
|
|
1100
1104
|
tokenMovements: {
|
|
1101
1105
|
amount: string;
|
|
@@ -1149,17 +1153,18 @@ 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";
|
|
1155
1160
|
recordedTx: {
|
|
1161
|
+
signers: string[];
|
|
1156
1162
|
events: {
|
|
1157
1163
|
contract: string;
|
|
1158
1164
|
topics: string[];
|
|
1159
1165
|
data?: unknown;
|
|
1160
1166
|
}[];
|
|
1161
1167
|
network: "mainnet" | "testnet";
|
|
1162
|
-
signers: string[];
|
|
1163
1168
|
invocations: unknown[];
|
|
1164
1169
|
tokenMovements: {
|
|
1165
1170
|
amount: string;
|
|
@@ -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,17 +1745,18 @@ 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";
|
|
1742
1752
|
recordedTx: {
|
|
1753
|
+
signers: string[];
|
|
1743
1754
|
events: {
|
|
1744
1755
|
contract: string;
|
|
1745
1756
|
topics: string[];
|
|
1746
1757
|
data?: unknown;
|
|
1747
1758
|
}[];
|
|
1748
1759
|
network: "mainnet" | "testnet";
|
|
1749
|
-
signers: string[];
|
|
1750
1760
|
invocations: unknown[];
|
|
1751
1761
|
tokenMovements: {
|
|
1752
1762
|
amount: string;
|
|
@@ -1800,17 +1810,18 @@ 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";
|
|
1806
1817
|
recordedTx: {
|
|
1818
|
+
signers: string[];
|
|
1807
1819
|
events: {
|
|
1808
1820
|
contract: string;
|
|
1809
1821
|
topics: string[];
|
|
1810
1822
|
data?: unknown;
|
|
1811
1823
|
}[];
|
|
1812
1824
|
network: "mainnet" | "testnet";
|
|
1813
|
-
signers: string[];
|
|
1814
1825
|
invocations: unknown[];
|
|
1815
1826
|
tokenMovements: {
|
|
1816
1827
|
amount: string;
|
|
@@ -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-cjs/run/schemas.js
CHANGED
|
@@ -178,6 +178,11 @@ exports.SynthesizePolicyMandateInputSchema = zod_1.z.object({
|
|
|
178
178
|
source: zod_1.z.literal('mandate'),
|
|
179
179
|
mandate: exports.MandateSpecSchema,
|
|
180
180
|
ozConfig: exports.OzAdapterConfigSchema.optional(),
|
|
181
|
+
// --explain opt-in. When true, the orchestrator attaches the
|
|
182
|
+
// in-memory predicate tree (null for the mandate path) + a minimal
|
|
183
|
+
// honest SimulationResult to the success envelope. Absent or false
|
|
184
|
+
// -> the success envelope is unchanged (byte-identical to today).
|
|
185
|
+
explain: zod_1.z.boolean().optional(),
|
|
181
186
|
});
|
|
182
187
|
/** Interpreter opt-in for the recording path. Present -> constraints OZ cannot
|
|
183
188
|
* express (per-method scoping, invocation-count windows, oracle bounds, exact
|
|
@@ -203,6 +208,15 @@ exports.SynthesizePolicyRecordingInputSchema = zod_1.z.object({
|
|
|
203
208
|
confidenceOverride: zod_1.z.object({ threshold: zod_1.z.number().min(0).max(1) }).optional(),
|
|
204
209
|
interpreter: exports.InterpreterOptionsSchema.optional(),
|
|
205
210
|
ozConfig: exports.OzAdapterConfigSchema.optional(),
|
|
211
|
+
// --explain opt-in. When true, the orchestrator attaches the
|
|
212
|
+
// in-memory PredicateNode + the corresponding SimulationResult
|
|
213
|
+
// (real one from the self-verify pipeline when the interpreter is
|
|
214
|
+
// engaged, minimal honest value otherwise) to the success envelope.
|
|
215
|
+
// Absent or false -> the success envelope is unchanged (byte-identical
|
|
216
|
+
// to today). The flag is ADDITIVE: the existing ProposedPolicy fields
|
|
217
|
+
// (encodedPredicate, predicateHash, etc.) are never altered by enabling
|
|
218
|
+
// explain.
|
|
219
|
+
explain: zod_1.z.boolean().optional(),
|
|
206
220
|
});
|
|
207
221
|
exports.SynthesizePolicyInputSchema = zod_1.z.discriminatedUnion('source', [
|
|
208
222
|
exports.SynthesizePolicyMandateInputSchema,
|
|
@@ -8,6 +8,10 @@ export interface OraclePriceBound {
|
|
|
8
8
|
asset: string;
|
|
9
9
|
operator: IRCompOp;
|
|
10
10
|
value: string;
|
|
11
|
+
/** Decimal basis `value` is written on. REQUIRED: oracle prices normalise to
|
|
12
|
+
* 9 dp, and a threshold silently assumed to share that basis is what let a
|
|
13
|
+
* raw 14-dp bound permit everything. The author states it; we convert. */
|
|
14
|
+
decimals: number;
|
|
11
15
|
}
|
|
12
16
|
/** Caller-supplied answers to the ambiguity prompts. Every numeric bound the
|
|
13
17
|
* synth might apply must come from here - the recording supplies observed
|
|
@@ -108,11 +108,17 @@ function composeFromRecording(facts, scopeContract, topLevel, opts) {
|
|
|
108
108
|
value: limit,
|
|
109
109
|
},
|
|
110
110
|
};
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
111
|
+
// A rolling spend cap always goes to OZ, whose `spending_limit` is the
|
|
112
|
+
// audited implementation. The interpreter is NOT a fallback for the
|
|
113
|
+
// token != scopeContract case: on chain it sees one authorized call,
|
|
114
|
+
// not the transaction's token movements, so it has no per-call amount
|
|
115
|
+
// to accumulate and the counter would never move. OZ reports the case
|
|
116
|
+
// it cannot cover (it pins the limit to the context contract), which
|
|
117
|
+
// is the honest outcome - the old fallback produced an interpreter
|
|
118
|
+
// predicate that silently never bound.
|
|
119
|
+
ozConstraints.push(spendCond);
|
|
120
|
+
if (interpreterEnabled && token !== scopeContract) {
|
|
121
|
+
warnings.push(`rolling spend cap on ${token} cannot be enforced on chain: OZ spending_limit pins the limit to the context contract, and the interpreter cannot observe token movements. Bound the per-call value with an argument cap plus an invocation-count limit instead.`);
|
|
116
122
|
}
|
|
117
123
|
}
|
|
118
124
|
}
|
|
@@ -137,7 +143,10 @@ function composeFromRecording(facts, scopeContract, topLevel, opts) {
|
|
|
137
143
|
op: 'compare',
|
|
138
144
|
compare: {
|
|
139
145
|
selector: { kind: 'invocation_count', windowSeconds },
|
|
140
|
-
|
|
146
|
+
// `lt`, not `lte`: the leaf reports the calls ALREADY made in the
|
|
147
|
+
// window, so `< N` is what permits N of them. With `lte` a limit
|
|
148
|
+
// of N let an N+1th call through.
|
|
149
|
+
operator: 'lt',
|
|
141
150
|
value: String(invocationLimit),
|
|
142
151
|
},
|
|
143
152
|
};
|
|
@@ -168,6 +177,7 @@ function composeFromRecording(facts, scopeContract, topLevel, opts) {
|
|
|
168
177
|
selector: { kind: 'oracle_price', asset: b.asset },
|
|
169
178
|
operator: b.operator,
|
|
170
179
|
value: b.value,
|
|
180
|
+
valueDecimals: b.decimals,
|
|
171
181
|
},
|
|
172
182
|
};
|
|
173
183
|
if (interpreterEnabled) {
|
|
@@ -303,8 +313,15 @@ function appendProtocolSpecificConstraints(ozConstraints, interpreterConstraints
|
|
|
303
313
|
});
|
|
304
314
|
for (let i = 0; i < elements.length; i++) {
|
|
305
315
|
const element = elements[i];
|
|
306
|
-
|
|
316
|
+
// An element we cannot bind leaves that request's action, asset and
|
|
317
|
+
// amount free while the length pin makes the policy LOOK total. The
|
|
318
|
+
// length pin still stays (it is a real restriction, and dropping it
|
|
319
|
+
// would only widen the policy) - what must not happen is dropping the
|
|
320
|
+
// element quietly.
|
|
321
|
+
if (element?.type !== 'map') {
|
|
322
|
+
warnings.push(`Blend submit requests element ${i} is not a Request map (recorded as ${element?.type ?? 'nothing'}): its request_type, address and amount are NOT pinned, so any action on any asset for any amount is permitted in that position`);
|
|
307
323
|
continue;
|
|
324
|
+
}
|
|
308
325
|
const fields = element.value;
|
|
309
326
|
const fieldValue = (name, scalarType) => {
|
|
310
327
|
const entry = fields.find((e) => e.key === name);
|
|
@@ -321,8 +338,17 @@ function appendProtocolSpecificConstraints(ozConstraints, interpreterConstraints
|
|
|
321
338
|
const address = fieldValue('address', 'address');
|
|
322
339
|
const amount = fieldValue('amount', 'i128');
|
|
323
340
|
const requestType = fieldValue('request_type', 'u32');
|
|
324
|
-
if (address === null || amount === null || requestType === null)
|
|
341
|
+
if (address === null || amount === null || requestType === null) {
|
|
342
|
+
const missing = [
|
|
343
|
+
requestType === null ? 'request_type (u32)' : null,
|
|
344
|
+
address === null ? 'address' : null,
|
|
345
|
+
amount === null ? 'amount (i128)' : null,
|
|
346
|
+
].filter((f) => f !== null);
|
|
347
|
+
// Partial pins are deliberately NOT emitted (see the note above): a
|
|
348
|
+
// half-pinned request reads as a bound one on the review card.
|
|
349
|
+
warnings.push(`Blend submit requests element ${i} could not be bound: ${missing.join(', ')} missing or of an unexpected type. That request's request_type, address and amount are NOT pinned, so any action on any asset for any amount is permitted in that position`);
|
|
325
350
|
continue;
|
|
351
|
+
}
|
|
326
352
|
interpreterConstraints.push({
|
|
327
353
|
op: 'compare',
|
|
328
354
|
compare: {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { PredicateNode } from '../types.ts';
|
|
1
|
+
import type { PredicateNode, ScVal } from '../types.ts';
|
|
2
2
|
import type { EvalContext } from './evaluate.ts';
|
|
3
3
|
export interface DenyCase {
|
|
4
4
|
dimension: string;
|
|
@@ -20,3 +20,6 @@ export { ORIGINAL_DIMENSIONS, OVERPERMISSIVE_DIMENSIONS };
|
|
|
20
20
|
* are emitted. The synth pipeline passes ORIGINAL_DIMENSIONS so
|
|
21
21
|
* existing fixtures do not regress. */
|
|
22
22
|
export declare function generateCases(predicate: PredicateNode, permitCtx: EvalContext, dimensions?: string[]): GeneratedCases;
|
|
23
|
+
/** Deep-copy an ScVal so a mutation cannot alias the recorded call.
|
|
24
|
+
* Exported so the permit-context builder shares this one implementation. */
|
|
25
|
+
export declare function cloneScVal(value: ScVal, depth?: number): ScVal;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.OVERPERMISSIVE_DIMENSIONS = exports.ORIGINAL_DIMENSIONS = void 0;
|
|
4
4
|
exports.generateCases = generateCases;
|
|
5
|
+
exports.cloneScVal = cloneScVal;
|
|
5
6
|
const types_ts_1 = require("../types.js");
|
|
6
7
|
const predicate_literals_ts_1 = require("./predicate-literals.js");
|
|
7
8
|
const ORACLE_CASES = [
|
|
@@ -481,6 +482,8 @@ function cloneContext(ctx) {
|
|
|
481
482
|
cloned.signerWeights = { ...ctx.signerWeights };
|
|
482
483
|
return cloned;
|
|
483
484
|
}
|
|
485
|
+
/** Deep-copy an ScVal so a mutation cannot alias the recorded call.
|
|
486
|
+
* Exported so the permit-context builder shares this one implementation. */
|
|
484
487
|
function cloneScVal(value, depth = 0) {
|
|
485
488
|
// Recursion is bounded by MAX_SCVAL_CLONE_DEPTH so a hand-crafted nested-vec
|
|
486
489
|
// payload cannot RangeError the JS stack during deny-case mutation. Over-depth
|
|
@@ -32,6 +32,11 @@
|
|
|
32
32
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
33
|
exports.evaluate = evaluate;
|
|
34
34
|
const predicate_literals_ts_1 = require("./predicate-literals.js");
|
|
35
|
+
/** Oracle prices normalise to this many decimals; mirrors NORMALISED_DECIMALS
|
|
36
|
+
* in oracle.rs. A threshold on any other basis must say so. */
|
|
37
|
+
const NORMALISED_DECIMALS = 9;
|
|
38
|
+
/** Mirrors MAX_ORACLE_THRESHOLD_DECIMALS in dsl.rs. */
|
|
39
|
+
const MAX_ORACLE_THRESHOLD_DECIMALS = 18;
|
|
35
40
|
/** Internal fatal thrown by the oracle path; caught at the top of `evaluate`
|
|
36
41
|
* and converted to the matching `ORACLE_*` deny reason. NOT a
|
|
37
42
|
* boolean-false that `not` / `or` could mask. */
|
|
@@ -168,6 +173,13 @@ function evalCompare(op, left, right, ctx) {
|
|
|
168
173
|
}
|
|
169
174
|
// --- step 4b: call_arg comparison (eq / exact-vec, or an ordered numeric bound) ---
|
|
170
175
|
if (left.kind === 'call_arg') {
|
|
176
|
+
// The swap's canonical form is `call_arg[out] >= call_arg_scaled(in, num, den)`.
|
|
177
|
+
// Dispatched here so the floor's dedicated reason codes (ARITHMETIC_OVERFLOW
|
|
178
|
+
// -> SLIPPAGE_FLOOR) reach the user; routing the scaled RHS through the
|
|
179
|
+
// generic `evalArgOrderedCompare` would mask overflow as ARG_MISMATCH.
|
|
180
|
+
if (right.kind === 'call_arg_scaled') {
|
|
181
|
+
return evalScaledArgCompare(op, left, right, ctx);
|
|
182
|
+
}
|
|
171
183
|
const actual = ctx.args[left.index];
|
|
172
184
|
// An ordered comparison (lt/lte/gt/gte) reads the arg as an integer and
|
|
173
185
|
// compares it to a numeric literal via BigInt (e.g. a SoroSwap input-amount
|
|
@@ -178,6 +190,11 @@ function evalCompare(op, left, right, ctx) {
|
|
|
178
190
|
return evalArgOrderedCompare(op, actual, right);
|
|
179
191
|
return evalArgEq(op, actual, right, ctx);
|
|
180
192
|
}
|
|
193
|
+
// --- step 4b': scaled leaf on the LEFT (`call_arg_scaled(in, num, den) <= call_arg[out]`).
|
|
194
|
+
// Symmetric form so a policy that phrases the floor either way works.
|
|
195
|
+
if (left.kind === 'call_arg_scaled') {
|
|
196
|
+
return evalScaledArgCompare(op, right, left, ctx);
|
|
197
|
+
}
|
|
181
198
|
// --- step 4c: call_arg_len: the length of a vec-typed argument as a u32.
|
|
182
199
|
// Fails closed on a non-vec arg, an absent arg, or a non-u32 literal.
|
|
183
200
|
if (left.kind === 'call_arg_len') {
|
|
@@ -296,6 +313,102 @@ function evalArgEq(op, actual, right, ctx) {
|
|
|
296
313
|
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
297
314
|
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
298
315
|
}
|
|
316
|
+
/** Step 4b': slippage-floor comparison. Mirrors the Rust `eval_scaled_arg_compare`
|
|
317
|
+
* path: the scaled leaf is `args[index] * num / den` (truncating toward
|
|
318
|
+
* zero). On `checked_mul` / `checked_div` failure (overflow or
|
|
319
|
+
* divide-by-zero) the comparison denies with `ARITHMETIC_OVERFLOW`. A
|
|
320
|
+
* failed comparison denies with `SLIPPAGE_FLOOR` (the dedicated reason)
|
|
321
|
+
* rather than the generic `ARG_MISMATCH`.
|
|
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`. */
|
|
328
|
+
function evalScaledArgCompare(op, left, right, ctx) {
|
|
329
|
+
// Identify which side is scaled, and pull the other side as a number.
|
|
330
|
+
let scaled;
|
|
331
|
+
let other;
|
|
332
|
+
let scaledOnRight;
|
|
333
|
+
if (left.kind === 'call_arg_scaled') {
|
|
334
|
+
scaled = left;
|
|
335
|
+
other = right;
|
|
336
|
+
scaledOnRight = false;
|
|
337
|
+
}
|
|
338
|
+
else if (right.kind === 'call_arg_scaled') {
|
|
339
|
+
scaled = right;
|
|
340
|
+
other = left;
|
|
341
|
+
scaledOnRight = true;
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
// Neither side is scaled - this is a programming error in the
|
|
345
|
+
// dispatcher; the contract returns ARG_MISMATCH to fail closed.
|
|
346
|
+
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
347
|
+
}
|
|
348
|
+
// Source `args[index]` -> BigInt. An out-of-bounds index or a
|
|
349
|
+
// non-numeric arg fails closed as ARG_MISMATCH (not SLIPPAGE_FLOOR) -
|
|
350
|
+
// a violated floor is the wrong code when the operand itself could
|
|
351
|
+
// not be read.
|
|
352
|
+
if (scaled.index >= ctx.args.length) {
|
|
353
|
+
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
354
|
+
}
|
|
355
|
+
const input = argNumericBigInt(ctx.args[scaled.index]);
|
|
356
|
+
if (input === null)
|
|
357
|
+
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
358
|
+
let num;
|
|
359
|
+
let den;
|
|
360
|
+
try {
|
|
361
|
+
num = BigInt(scaled.num);
|
|
362
|
+
den = BigInt(scaled.den);
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
366
|
+
}
|
|
367
|
+
// Install refuses `den == 0` and `num <= 0` / `den <= 0`. The runtime
|
|
368
|
+
// check is a defensive belt-and-braces - a future validator
|
|
369
|
+
// regression cannot panic the frame on a divide-by-zero.
|
|
370
|
+
if (den === 0n)
|
|
371
|
+
return { permit: false, reason: 'ARITHMETIC_OVERFLOW' };
|
|
372
|
+
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
|
+
let scaledValue;
|
|
379
|
+
try {
|
|
380
|
+
// BigInt division truncates toward zero (matches Rust `i128::checked_div`).
|
|
381
|
+
scaledValue = product / den;
|
|
382
|
+
}
|
|
383
|
+
catch {
|
|
384
|
+
return { permit: false, reason: 'ARITHMETIC_OVERFLOW' };
|
|
385
|
+
}
|
|
386
|
+
// i128 range check: if the scaled value is outside i128, the contract
|
|
387
|
+
// would have wrapped on i128 arithmetic; surface the same deny.
|
|
388
|
+
const I128_MAX = (1n << 127n) - 1n;
|
|
389
|
+
const I128_MIN = -(1n << 127n);
|
|
390
|
+
if (scaledValue > I128_MAX || scaledValue < I128_MIN) {
|
|
391
|
+
return { permit: false, reason: 'ARITHMETIC_OVERFLOW' };
|
|
392
|
+
}
|
|
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
|
+
let otherVal;
|
|
399
|
+
if (other.kind === 'call_arg') {
|
|
400
|
+
otherVal = argNumericBigInt(ctx.args[other.index]);
|
|
401
|
+
}
|
|
402
|
+
else {
|
|
403
|
+
otherVal = (0, predicate_literals_ts_1.literalNumericBigInt)(other);
|
|
404
|
+
}
|
|
405
|
+
if (otherVal === null)
|
|
406
|
+
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
407
|
+
const pass = scaledOnRight
|
|
408
|
+
? bigintCmp(op, otherVal.toString(), scaledValue.toString())
|
|
409
|
+
: bigintCmp(op, scaledValue.toString(), otherVal.toString());
|
|
410
|
+
return pass ? { permit: true } : { permit: false, reason: 'SLIPPAGE_FLOOR' };
|
|
411
|
+
}
|
|
299
412
|
/** Ordered numeric comparison (lt/lte/gt/gte) on a `call_arg`. The interpreter
|
|
300
413
|
* reads the arg as an integer (i128 / u64 / u32 on the recorder's ScVal
|
|
301
414
|
* surface) and compares it to a numeric literal via BigInt. A non-numeric arg
|
|
@@ -392,6 +505,7 @@ function resolveLeaf(leaf, ctx) {
|
|
|
392
505
|
case 'invocation_count_in_window':
|
|
393
506
|
case 'now':
|
|
394
507
|
case 'valid_until':
|
|
508
|
+
case 'call_arg_scaled':
|
|
395
509
|
return undefined; // selector leaves with no ScVal projection
|
|
396
510
|
case 'literal_address':
|
|
397
511
|
return { type: 'address', value: leaf.value };
|
|
@@ -450,10 +564,23 @@ function evalOracleCompare(op, asset, right, ctx) {
|
|
|
450
564
|
throw new OracleError('ORACLE_STALE');
|
|
451
565
|
throw new OracleError(mapped);
|
|
452
566
|
}
|
|
453
|
-
|
|
454
|
-
|
|
567
|
+
// Mirrors eval_oracle_compare in dsl.rs. The threshold MUST declare its
|
|
568
|
+
// decimal basis: prices are on the normalised 9-dp basis, and assuming a
|
|
569
|
+
// bare literal shares it is what let a raw 14-dp threshold permit
|
|
570
|
+
// everything. A bare literal is refused rather than assumed.
|
|
571
|
+
if (right.kind !== 'oracle_threshold')
|
|
455
572
|
throw new OracleError('ORACLE_DECIMALS_MISMATCH');
|
|
456
|
-
|
|
573
|
+
if (right.decimals > MAX_ORACLE_THRESHOLD_DECIMALS) {
|
|
574
|
+
throw new OracleError('ORACLE_THRESHOLD_DECIMALS_OUT_OF_RANGE');
|
|
575
|
+
}
|
|
576
|
+
// Scale BOTH sides up to the wider basis rather than dividing the threshold
|
|
577
|
+
// down: dividing truncates, and a truncated bound moves the permit boundary.
|
|
578
|
+
const decimals = BigInt(right.decimals);
|
|
579
|
+
const normalised = BigInt(NORMALISED_DECIMALS);
|
|
580
|
+
const common = decimals > normalised ? decimals : normalised;
|
|
581
|
+
const priceScaled = BigInt(entry.price) * 10n ** (common - normalised);
|
|
582
|
+
const literalScaled = BigInt(right.value) * 10n ** (common - decimals);
|
|
583
|
+
return bigintCmp(op, String(priceScaled), String(literalScaled))
|
|
457
584
|
? { permit: true }
|
|
458
585
|
: { permit: false, reason: 'FN_MISMATCH' };
|
|
459
586
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { PredicateNode, RecordedTransaction } from '../types.ts';
|
|
2
|
+
import type { EvalContext } from './evaluate.ts';
|
|
3
|
+
export interface PermitContextResponses {
|
|
4
|
+
windowSeconds: number;
|
|
5
|
+
invocationLimit?: number;
|
|
6
|
+
limitAmount?: string;
|
|
7
|
+
validUntilLedger: number;
|
|
8
|
+
oraclePriceBound?: Array<{
|
|
9
|
+
asset: string;
|
|
10
|
+
operator: string;
|
|
11
|
+
value: string;
|
|
12
|
+
decimals: number;
|
|
13
|
+
}>;
|
|
14
|
+
}
|
|
15
|
+
export declare function buildPermitContext(tx: RecordedTransaction, responses: PermitContextResponses, predicate: PredicateNode): EvalContext;
|