@crediolabs/policy-synth 0.1.2 → 0.1.4
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 +193 -5
- package/dist/adapters/interpreter/adapter.d.ts +38 -0
- package/dist/adapters/interpreter/adapter.js +522 -0
- package/dist/adapters/interpreter/index.d.ts +1 -0
- package/dist/adapters/interpreter/index.js +2 -0
- package/dist/adapters/oz/adapter.js +20 -18
- package/dist/codegen/compile-gate.d.ts +33 -0
- package/dist/codegen/compile-gate.js +119 -0
- package/dist/codegen/index.d.ts +2 -0
- package/dist/codegen/index.js +8 -0
- package/dist/codegen/template.d.ts +18 -0
- package/dist/codegen/template.js +131 -0
- package/dist/errors.d.ts +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/ir/types.d.ts +13 -2
- package/dist/mandate/to-ir.js +4 -4
- package/dist/mandate/types.d.ts +1 -1
- package/dist/predicate/encode.d.ts +10 -0
- package/dist/predicate/encode.js +249 -0
- package/dist/predicate/index.d.ts +1 -0
- package/dist/predicate/index.js +2 -0
- package/dist/review-card/builder.d.ts +14 -0
- package/dist/review-card/builder.js +261 -0
- package/dist/review-card/conflict.d.ts +40 -0
- package/dist/review-card/conflict.js +111 -0
- package/dist/review-card/cross-check.d.ts +11 -0
- package/dist/review-card/cross-check.js +148 -0
- package/dist/review-card/index.d.ts +3 -0
- package/dist/review-card/index.js +4 -0
- package/dist/seams/types.d.ts +1 -1
- package/dist/synth/compose-from-recording.d.ts +35 -7
- package/dist/synth/compose-from-recording.js +231 -40
- package/dist/synth/deny-cases.d.ts +12 -0
- package/dist/synth/deny-cases.js +361 -0
- package/dist/synth/evaluate.d.ts +39 -0
- package/dist/synth/evaluate.js +425 -0
- package/dist/synth/harness.d.ts +16 -0
- package/dist/synth/harness.js +26 -0
- package/dist/synth/index.d.ts +4 -0
- package/dist/synth/index.js +4 -0
- package/dist/synth/lower.d.ts +2 -2
- package/dist/synth/minimize.d.ts +4 -0
- package/dist/synth/minimize.js +38 -0
- package/dist/synth/predicate-literals.d.ts +5 -0
- package/dist/synth/predicate-literals.js +25 -0
- package/dist/synth/synthesize-from-mandate.js +5 -5
- package/dist/synth/synthesize-from-recording.d.ts +32 -3
- package/dist/synth/synthesize-from-recording.js +491 -17
- package/dist/types.d.ts +23 -1
- package/dist/verify/envelope.d.ts +15 -0
- package/dist/verify/envelope.js +22 -0
- package/dist/verify/index.d.ts +3 -0
- package/dist/verify/index.js +3 -0
- package/dist/verify/simulate.d.ts +31 -0
- package/dist/verify/simulate.js +242 -0
- package/dist/verify/verify.d.ts +21 -0
- package/dist/verify/verify.js +173 -0
- package/package.json +1 -1
- package/src/adapters/interpreter/adapter.ts +642 -0
- package/src/adapters/interpreter/index.ts +8 -0
- package/src/adapters/oz/adapter.ts +20 -18
- package/src/codegen/compile-gate.ts +162 -0
- package/src/codegen/index.ts +17 -0
- package/src/codegen/template.ts +148 -0
- package/src/errors.ts +2 -0
- package/src/index.ts +2 -0
- package/src/ir/types.ts +9 -2
- package/src/mandate/to-ir.ts +4 -4
- package/src/mandate/types.ts +1 -1
- package/src/predicate/encode.ts +307 -0
- package/src/predicate/index.ts +3 -0
- package/src/review-card/builder.ts +303 -0
- package/src/review-card/conflict.ts +143 -0
- package/src/review-card/cross-check.ts +158 -0
- package/src/review-card/index.ts +12 -0
- package/src/seams/types.ts +1 -1
- package/src/synth/compose-from-recording.ts +283 -49
- package/src/synth/deny-cases.ts +447 -0
- package/src/synth/evaluate.ts +493 -0
- package/src/synth/harness.ts +40 -0
- package/src/synth/index.ts +12 -0
- package/src/synth/lower.ts +2 -2
- package/src/synth/minimize.ts +44 -0
- package/src/synth/predicate-literals.ts +27 -0
- package/src/synth/synthesize-from-mandate.ts +5 -5
- package/src/synth/synthesize-from-recording.ts +575 -18
- package/src/types.ts +19 -2
- package/src/verify/envelope.ts +28 -0
- package/src/verify/index.ts +5 -0
- package/src/verify/simulate.ts +292 -0
- package/src/verify/verify.ts +224 -0
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
// src/adapters/interpreter/adapter.ts - the interpreter-policy CustodyAdapter.
|
|
2
|
+
//
|
|
3
|
+
// Compiles a PolicyIR to a single interpreter `PolicyDocument` + `PolicyRef`
|
|
4
|
+
// carrying the canonical predicate encoding defined in
|
|
5
|
+
// `packages/policy-interpreter/INTERPRETER_INSTALL_PARAMS.md`. The adapter is
|
|
6
|
+
// the second backend (the OZ built-in adapter is the first); together they
|
|
7
|
+
// cover the canonical language subset. The compose step (P3) routes IR
|
|
8
|
+
// constructs between them.
|
|
9
|
+
//
|
|
10
|
+
// What it lowers (every IR construct the predicate DSL expresses):
|
|
11
|
+
// - IRLogic and/or -> PredicateNode and/or
|
|
12
|
+
// - IRLogic not -> PredicateNode not
|
|
13
|
+
// - IRCompare -> selector(left) vs literal(right), op carried over
|
|
14
|
+
// - IR `in` -> needle=selector, haystack=literals (PURESET
|
|
15
|
+
// membership; the haystack is always sorted by the
|
|
16
|
+
// encoder)
|
|
17
|
+
// - IR `eq_seq` -> eq(selectorLeaf, literal_vec([...])) - exact ordered
|
|
18
|
+
// sequence equality; element order is preserved
|
|
19
|
+
// verbatim (the encoder does NOT sort the vec)
|
|
20
|
+
// - IRSelector -> matching PredicateLeaf (see lowerSelector)
|
|
21
|
+
// - scope.contract -> sibling `call_contract == <contract>` (always
|
|
22
|
+
// emitted when set)
|
|
23
|
+
// - scope.method -> sibling `call_fn == <method>`
|
|
24
|
+
//
|
|
25
|
+
// Three fail-closed enforcement gates (per spec):
|
|
26
|
+
// - oracle_price leaves MUST sit directly under the top-level `and`; nesting
|
|
27
|
+
// under `not` / `or` / inside `in` throws ORACLE_LEAF_INVALID_POSITION.
|
|
28
|
+
// - oracleParams overrides may only TIGHTEN vs wasm defaults
|
|
29
|
+
// (maxStalenessSeconds <= 600, maxDeviationBps <= 200); a widening value
|
|
30
|
+
// throws ORACLE_PARAMS_OUT_OF_RANGE at compile time.
|
|
31
|
+
// - an `in` allowlist (or a `compare eq` vs an address, or any value in an
|
|
32
|
+
// `eq_seq`) that targets the smart account's own address throws
|
|
33
|
+
// SCOPE_SELF_CALL.
|
|
34
|
+
//
|
|
35
|
+
// Anything the adapter genuinely cannot express (EVM calldata / value, unix
|
|
36
|
+
// expiry, etc.) is named in `uncovered` rather than silently dropped.
|
|
37
|
+
import { encodePredicate } from "../../predicate/encode.js";
|
|
38
|
+
/** [VERIFY] NOT a real deployed address. The interpreter is a per-network
|
|
39
|
+
* deploy artifact we do not have yet; install is a later phase. */
|
|
40
|
+
export const PLACEHOLDER_INTERPRETER_ADDRESS = 'VERIFY-interpreter-address';
|
|
41
|
+
/** Wasm-level oracle defaults (mirrors INTERPRETER_INSTALL_PARAMS.md OracleParams).
|
|
42
|
+
* Per-policy overrides may TIGHTEN only - they may never exceed these. */
|
|
43
|
+
export const ORACLE_DEFAULTS = {
|
|
44
|
+
maxStalenessSeconds: 600,
|
|
45
|
+
maxDeviationBps: 200,
|
|
46
|
+
};
|
|
47
|
+
const CAPABILITIES = {
|
|
48
|
+
supportsSpendWindow: true,
|
|
49
|
+
supportsThreshold: false, // thresholds are the OZ adapter's job
|
|
50
|
+
supportsTimeExpiry: true, // via valid_until ledger expiry
|
|
51
|
+
supportsOraclePrice: true,
|
|
52
|
+
supportsInvocationCount: true,
|
|
53
|
+
supportsGeneralPredicate: true,
|
|
54
|
+
};
|
|
55
|
+
/** Parse confidence for a deterministic (non-decoded) input: full, with an
|
|
56
|
+
* empty unknown/opaque breakdown. A mandate needs no decoding, so the gate is
|
|
57
|
+
* not applicable and confidence is 1. */
|
|
58
|
+
const FULL_PARSE_CONFIDENCE = {
|
|
59
|
+
overall: 1,
|
|
60
|
+
knownContracts: [],
|
|
61
|
+
unknownContracts: [],
|
|
62
|
+
opaqueScVals: [],
|
|
63
|
+
thresholdUsed: 1,
|
|
64
|
+
};
|
|
65
|
+
export function createInterpreterAdapter(config) {
|
|
66
|
+
return {
|
|
67
|
+
name: 'interpreter',
|
|
68
|
+
mode: 'enforce',
|
|
69
|
+
capabilities: () => ({ ...CAPABILITIES }),
|
|
70
|
+
compile: (ir) => compile(ir, config),
|
|
71
|
+
simulate: () => simulateStub(),
|
|
72
|
+
export: (ir) => canonicalStringify(ir),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** Lower a single IR rule to the canonical pre-encoding `PredicateNode`. The
|
|
76
|
+
* orchestrator uses this to wire the self-verify + minimise pipeline: after
|
|
77
|
+
* `compile(ir)` succeeds (`covered === true`, `proposed` set), the
|
|
78
|
+
* orchestrator re-derives the PredicateNode via this helper to drive
|
|
79
|
+
* `minimize` and `runHarness` on the SAME shape the encoder saw. Pure and
|
|
80
|
+
* deterministic: same `rule + config` -> byte-identical PredicateNode. The
|
|
81
|
+
* `uncovered` list the adapter surfaces during compile is NOT re-derived
|
|
82
|
+
* here; callers that need it should use the result of `compile(ir)`. */
|
|
83
|
+
export function lowerRuleToPredicate(rule, config) {
|
|
84
|
+
return lowerRule(rule, config).predicate;
|
|
85
|
+
}
|
|
86
|
+
function compile(ir, config) {
|
|
87
|
+
// Tighten-only oracle-params gate (config-time). Widening -> throw at compile
|
|
88
|
+
// time before any lowering happens, so a misuse fails closed loudly.
|
|
89
|
+
assertOracleParamsTighten(config.oracleParams);
|
|
90
|
+
const firstRule = ir.rules[0];
|
|
91
|
+
if (!firstRule) {
|
|
92
|
+
return { covered: false, uncovered: ['empty PolicyIR (no rules to compile)'] };
|
|
93
|
+
}
|
|
94
|
+
if (ir.rules.length > 1) {
|
|
95
|
+
return {
|
|
96
|
+
covered: false,
|
|
97
|
+
uncovered: [
|
|
98
|
+
`multi-rule PolicyIR: ${ir.rules.length - 1} rule(s) beyond the first are not compiled (the interpreter adapter handles one rule per document in this slice)`,
|
|
99
|
+
],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const lowered = lowerRule(firstRule, config);
|
|
103
|
+
if (lowered.uncovered.length > 0) {
|
|
104
|
+
return { covered: false, uncovered: lowered.uncovered };
|
|
105
|
+
}
|
|
106
|
+
const { encodedPredicate, predicateHash } = encodePredicate(lowered.predicate);
|
|
107
|
+
const policyDocument = {
|
|
108
|
+
grammarVersion: 1,
|
|
109
|
+
installNonce: config.installNonce,
|
|
110
|
+
encodedPredicate,
|
|
111
|
+
predicateHash,
|
|
112
|
+
...(config.oracleParams ? { oracleParams: config.oracleParams } : {}),
|
|
113
|
+
};
|
|
114
|
+
const policyRef = {
|
|
115
|
+
kind: 'interpreter',
|
|
116
|
+
interpreterAddress: PLACEHOLDER_INTERPRETER_ADDRESS,
|
|
117
|
+
predicateBlobBase64: encodedPredicate,
|
|
118
|
+
};
|
|
119
|
+
const proposed = {
|
|
120
|
+
contextRule: lowered.contextRule,
|
|
121
|
+
policyDocuments: [policyDocument],
|
|
122
|
+
policyRefs: [policyRef],
|
|
123
|
+
parseConfidence: { ...FULL_PARSE_CONFIDENCE },
|
|
124
|
+
warnings: [],
|
|
125
|
+
ambiguities: [],
|
|
126
|
+
};
|
|
127
|
+
return { covered: true, uncovered: [], proposed };
|
|
128
|
+
}
|
|
129
|
+
function lowerRule(rule, config) {
|
|
130
|
+
const uncovered = [];
|
|
131
|
+
// scope -> context rule + sibling predicates. contract/method each become
|
|
132
|
+
// their own `eq` leaf and are merged into the top-level and alongside the
|
|
133
|
+
// constraints. This keeps the top-level shape `and` (mandatory per the oracle
|
|
134
|
+
// position rule and the canonical hash stability for `and`).
|
|
135
|
+
const scopeContract = rule.scope.contract;
|
|
136
|
+
const scopeMethod = rule.scope.method;
|
|
137
|
+
if (rule.scope.chainId !== undefined) {
|
|
138
|
+
uncovered.push(`chainId \`${rule.scope.chainId}\` not bindable by the interpreter adapter (network is per-context, not per-rule)`);
|
|
139
|
+
}
|
|
140
|
+
if (rule.roles.length > 0) {
|
|
141
|
+
uncovered.push(`roles [${rule.roles.join(', ')}] dropped (role-to-signer mapping is a later phase; interpreter policies reference addresses, not role names)`);
|
|
142
|
+
}
|
|
143
|
+
if (rule.guard) {
|
|
144
|
+
uncovered.push(`guard: ${describeCondition(rule.guard)} (interpreter adapter currently treats guards as uncovered)`);
|
|
145
|
+
}
|
|
146
|
+
// expiry -> context rule validUntilLedger only. Unix-timestamp expiry is
|
|
147
|
+
// honoured by the interpreter (the `valid_until` selector), but we surface
|
|
148
|
+
// it here as Path-B because the IR contract already says "OZ context rules
|
|
149
|
+
// expire by ledger sequence" and we keep one expiry model for both adapters.
|
|
150
|
+
let validUntilLedger = null;
|
|
151
|
+
if (rule.expiry) {
|
|
152
|
+
if (rule.expiry.validUntilLedger !== undefined) {
|
|
153
|
+
validUntilLedger = rule.expiry.validUntilLedger;
|
|
154
|
+
}
|
|
155
|
+
else if (rule.expiry.validUntilUnixSeconds !== undefined) {
|
|
156
|
+
uncovered.push('time expiry given as a unix timestamp (interpreter adapter currently lowers expiry only via the OZ context-rule validUntilLedger; supply expiry.validUntilLedger)');
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
// approval is the OZ adapter's job (it lowers to simple_threshold /
|
|
160
|
+
// weighted_threshold primitives). Surface it here so a caller that drops it
|
|
161
|
+
// through to the interpreter adapter sees an explicit uncovered entry
|
|
162
|
+
// instead of a silently-dropped M-of-N gate.
|
|
163
|
+
if (rule.approval) {
|
|
164
|
+
uncovered.push(`approval threshold ${rule.approval.threshold} not emitted by the interpreter adapter (thresholds lower to OZ built-in primitives in the OZ adapter)`);
|
|
165
|
+
}
|
|
166
|
+
// Pre-scan constraints: anything the interpreter adapter cannot express is
|
|
167
|
+
// named in `uncovered` and skipped from the predicate lowering. We surface
|
|
168
|
+
// these BEFORE lowering so the predicate is built from only the
|
|
169
|
+
// expressible subset.
|
|
170
|
+
const expressibleConstraints = [];
|
|
171
|
+
for (const c of rule.constraints) {
|
|
172
|
+
const unsupp = unsupportedConstruct(c);
|
|
173
|
+
if (unsupp !== null) {
|
|
174
|
+
uncovered.push(unsupp);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
expressibleConstraints.push(c);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// Build the top-level `and` of scope + expressible constraints. The
|
|
181
|
+
// top-level MUST be `and` so oracle leaves sit directly under it - the
|
|
182
|
+
// position rule is enforced inside `lowerCondition`.
|
|
183
|
+
const topChildren = [];
|
|
184
|
+
if (scopeContract !== undefined) {
|
|
185
|
+
topChildren.push({
|
|
186
|
+
op: 'eq',
|
|
187
|
+
left: { kind: 'call_contract' },
|
|
188
|
+
right: { kind: 'literal_address', value: scopeContract },
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
if (scopeMethod !== undefined) {
|
|
192
|
+
topChildren.push({
|
|
193
|
+
op: 'eq',
|
|
194
|
+
left: { kind: 'call_fn' },
|
|
195
|
+
right: { kind: 'literal_symbol', value: scopeMethod },
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
for (const c of expressibleConstraints) {
|
|
199
|
+
topChildren.push(lowerCondition(c, config));
|
|
200
|
+
}
|
|
201
|
+
const predicate = topChildren.length === 1 && topChildren[0] !== undefined
|
|
202
|
+
? topChildren[0]
|
|
203
|
+
: { op: 'and', children: topChildren };
|
|
204
|
+
const contextRule = {
|
|
205
|
+
contextRuleType: { kind: 'default' },
|
|
206
|
+
name: 'interpreter',
|
|
207
|
+
validUntilLedger,
|
|
208
|
+
signers: [],
|
|
209
|
+
policies: [],
|
|
210
|
+
};
|
|
211
|
+
return { predicate, contextRule, uncovered };
|
|
212
|
+
}
|
|
213
|
+
/** Detect IR constructs the interpreter adapter cannot express; return a
|
|
214
|
+
* human-readable descriptor (or null if the construct IS expressible). Used
|
|
215
|
+
* by `lowerRule` to populate `uncovered` before lowering. */
|
|
216
|
+
function unsupportedConstruct(cond) {
|
|
217
|
+
switch (cond.op) {
|
|
218
|
+
case 'in':
|
|
219
|
+
return cond.selector.kind === 'calldata' || cond.selector.kind === 'value'
|
|
220
|
+
? `EVM \`${cond.selector.kind}\` selector on allowlist (predicate DSL)`
|
|
221
|
+
: null;
|
|
222
|
+
case 'eq_seq':
|
|
223
|
+
return cond.selector.kind === 'calldata' || cond.selector.kind === 'value'
|
|
224
|
+
? `EVM \`${cond.selector.kind}\` selector on ordered-sequence equality (predicate DSL)`
|
|
225
|
+
: null;
|
|
226
|
+
case 'compare': {
|
|
227
|
+
const s = cond.compare.selector;
|
|
228
|
+
if (s.kind === 'calldata')
|
|
229
|
+
return 'EVM calldata comparison (predicate DSL)';
|
|
230
|
+
if (s.kind === 'value')
|
|
231
|
+
return 'tx.value comparison (predicate DSL)';
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
case 'and':
|
|
235
|
+
case 'or':
|
|
236
|
+
case 'not':
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
/** Lower one IR condition to a PredicateNode. Enforces the oracle position
|
|
241
|
+
* rule (oracle_price leaves must sit directly under the top-level `and`) and
|
|
242
|
+
* the self-call rule (`in` allowlist / `compare eq` vs the smart account
|
|
243
|
+
* address -> SCOPE_SELF_CALL). */
|
|
244
|
+
function lowerCondition(cond, config) {
|
|
245
|
+
switch (cond.op) {
|
|
246
|
+
case 'and':
|
|
247
|
+
return { op: 'and', children: cond.children.map((c) => lowerCondition(c, config)) };
|
|
248
|
+
case 'or':
|
|
249
|
+
// `or` at this depth: oracle_price anywhere inside is invalid (would
|
|
250
|
+
// not be a direct child of the top-level `and`).
|
|
251
|
+
assertNoOracleDescendants(cond);
|
|
252
|
+
return { op: 'or', children: cond.children.map((c) => lowerCondition(c, config)) };
|
|
253
|
+
case 'not':
|
|
254
|
+
// `not` at this depth: oracle_price anywhere inside is invalid.
|
|
255
|
+
assertNoOracleDescendants(cond);
|
|
256
|
+
return { op: 'not', child: lowerCondition(cond.child, config) };
|
|
257
|
+
case 'in': {
|
|
258
|
+
assertNoOracleDescendants(cond);
|
|
259
|
+
for (const v of cond.values) {
|
|
260
|
+
assertNotSelfCallAddress(v, config);
|
|
261
|
+
}
|
|
262
|
+
// `in` is PURE set membership; the encoder sorts the haystack. An exact
|
|
263
|
+
// ordered sequence (e.g. swap hop path) is expressed as `eq_seq` and
|
|
264
|
+
// lowers to `eq(selector, literal_vec)` instead - never to `in` with an
|
|
265
|
+
// `ordered` flag.
|
|
266
|
+
return {
|
|
267
|
+
op: 'in',
|
|
268
|
+
needle: lowerSelector(cond.selector),
|
|
269
|
+
haystack: cond.values.map((v) => literalFromScalar(v, selectorScalarType(cond.selector))),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
case 'eq_seq': {
|
|
273
|
+
assertNoOracleDescendants(cond);
|
|
274
|
+
for (const v of cond.values) {
|
|
275
|
+
assertNotSelfCallAddress(v, config);
|
|
276
|
+
}
|
|
277
|
+
// Exact ordered sequence equality: the right-hand side is a literal_vec
|
|
278
|
+
// whose element order is preserved verbatim by the encoder. `eq` does
|
|
279
|
+
// deep equality at evaluate time - this is the ONLY way to express an
|
|
280
|
+
// exact ordered sequence in the predicate grammar (the path of a swap).
|
|
281
|
+
return {
|
|
282
|
+
op: 'eq',
|
|
283
|
+
left: lowerSelector(cond.selector),
|
|
284
|
+
right: {
|
|
285
|
+
kind: 'literal_vec',
|
|
286
|
+
elements: cond.values.map((v) => literalFromScalar(v, selectorScalarType(cond.selector))),
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
case 'compare': {
|
|
291
|
+
// Self-call on an address eq.
|
|
292
|
+
if (cond.compare.operator === 'eq' &&
|
|
293
|
+
cond.compare.selector.kind === 'arg' &&
|
|
294
|
+
cond.compare.selector.scalarType === 'address') {
|
|
295
|
+
assertNotSelfCallAddress(cond.compare.value, config);
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
op: cond.compare.operator,
|
|
299
|
+
left: lowerSelector(cond.compare.selector),
|
|
300
|
+
right: literalFromIRCompare(cond.compare),
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
/** Lower an IR selector to the matching PredicateLeaf. */
|
|
306
|
+
function lowerSelector(s) {
|
|
307
|
+
switch (s.kind) {
|
|
308
|
+
case 'arg':
|
|
309
|
+
return { kind: 'call_arg', index: s.argIndex };
|
|
310
|
+
case 'amount':
|
|
311
|
+
return { kind: 'amount', token: s.token };
|
|
312
|
+
case 'window_spent':
|
|
313
|
+
return { kind: 'window_spent', token: s.token, windowSeconds: s.windowSeconds };
|
|
314
|
+
case 'invocation_count':
|
|
315
|
+
return { kind: 'invocation_count_in_window', windowSecs: s.windowSeconds };
|
|
316
|
+
case 'now':
|
|
317
|
+
return { kind: 'now' };
|
|
318
|
+
case 'valid_until':
|
|
319
|
+
return { kind: 'valid_until' };
|
|
320
|
+
case 'oracle_price':
|
|
321
|
+
return { kind: 'oracle_price', asset: s.asset };
|
|
322
|
+
case 'calldata':
|
|
323
|
+
case 'value':
|
|
324
|
+
// Unreachable: the caller flagged these as Path-B before reaching here.
|
|
325
|
+
throw toolError('SYNTHESIS_ERROR', `selector kind \`${s.kind}\` is not lowerable to a predicate leaf`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/** Build a literal leaf from the right-hand side of an IRCompare, mapping the
|
|
329
|
+
* selector kind to the matching `literal_*` kind. The IR compare value is a
|
|
330
|
+
* raw string (i128-safe); the selector kind fixes the canonical wire type:
|
|
331
|
+
* - arg -> IR scalarType (set by the recorder/parser)
|
|
332
|
+
* - amount -> i128 (canonical Stellar token amount encoding)
|
|
333
|
+
* - window_spent -> i128 (canonical amount encoding)
|
|
334
|
+
* - oracle_price -> i128 (canonical price encoding)
|
|
335
|
+
* - invocation_count -> u32 (counts are small non-negative integers)
|
|
336
|
+
* - now / valid_until -> u64 (unix timestamps in seconds) */
|
|
337
|
+
function literalFromIRCompare(c) {
|
|
338
|
+
const scalarType = c.selector.kind === 'arg' ? c.selector.scalarType : literalScalarForSelector(c.selector.kind);
|
|
339
|
+
return literalFromScalar(c.value, scalarType);
|
|
340
|
+
}
|
|
341
|
+
function literalScalarForSelector(kind) {
|
|
342
|
+
switch (kind) {
|
|
343
|
+
case 'amount':
|
|
344
|
+
case 'window_spent':
|
|
345
|
+
case 'oracle_price':
|
|
346
|
+
return 'i128';
|
|
347
|
+
case 'invocation_count':
|
|
348
|
+
return 'u32';
|
|
349
|
+
case 'now':
|
|
350
|
+
case 'valid_until':
|
|
351
|
+
return 'u64';
|
|
352
|
+
case 'arg':
|
|
353
|
+
case 'calldata':
|
|
354
|
+
case 'value':
|
|
355
|
+
// arg -> caller handles scalarType; calldata/value -> unreachable (Path-B).
|
|
356
|
+
return 'i128';
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
/** Build a literal leaf from a raw string value + an IRScalarType hint. */
|
|
360
|
+
function literalFromScalar(value, scalarType) {
|
|
361
|
+
switch (scalarType) {
|
|
362
|
+
case 'address':
|
|
363
|
+
return { kind: 'literal_address', value };
|
|
364
|
+
case 'i128':
|
|
365
|
+
case 'u128':
|
|
366
|
+
return { kind: 'literal_i128', value };
|
|
367
|
+
case 'u32':
|
|
368
|
+
return { kind: 'literal_u32', value: Number.parseInt(value, 10) };
|
|
369
|
+
case 'u64':
|
|
370
|
+
case 'i64':
|
|
371
|
+
return { kind: 'literal_u64', value };
|
|
372
|
+
case 'symbol':
|
|
373
|
+
return { kind: 'literal_symbol', value };
|
|
374
|
+
case 'bytes':
|
|
375
|
+
return { kind: 'literal_bytes', value };
|
|
376
|
+
case 'bool':
|
|
377
|
+
throw toolError('MALFORMED_PREDICATE', `boolean literal not supported in v1 predicate grammar`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
/** Scalar type of an IRSelector for the purpose of building literal leaves
|
|
381
|
+
* (the right-hand side of `eq` / elements of an `in` haystack / elements of
|
|
382
|
+
* a `literal_vec`). Mirrors `literalScalarForSelector` for OZ extensions and
|
|
383
|
+
* uses the selector's own `scalarType` for `arg` selectors. */
|
|
384
|
+
function selectorScalarType(selector) {
|
|
385
|
+
if (selector.kind === 'arg')
|
|
386
|
+
return selector.scalarType;
|
|
387
|
+
return literalScalarForSelector(selector.kind);
|
|
388
|
+
}
|
|
389
|
+
/** Walk a condition tree and throw if any oracle_price leaf is found anywhere
|
|
390
|
+
* inside. Used by `lowerCondition` for `not`, `or`, and `in` - the three
|
|
391
|
+
* positions where oracle leaves would not be direct children of the
|
|
392
|
+
* top-level `and`. */
|
|
393
|
+
function assertNoOracleDescendants(node) {
|
|
394
|
+
if (containsOracle(node)) {
|
|
395
|
+
throw toolError('ORACLE_LEAF_INVALID_POSITION', 'oracle_price leaves must sit directly under the top-level `and`');
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
function containsOracle(node) {
|
|
399
|
+
switch (node.op) {
|
|
400
|
+
case 'compare':
|
|
401
|
+
return node.compare.selector.kind === 'oracle_price';
|
|
402
|
+
case 'in':
|
|
403
|
+
return node.selector.kind === 'oracle_price';
|
|
404
|
+
case 'eq_seq':
|
|
405
|
+
return node.selector.kind === 'oracle_price';
|
|
406
|
+
case 'not':
|
|
407
|
+
return containsOracle(node.child);
|
|
408
|
+
case 'and':
|
|
409
|
+
case 'or':
|
|
410
|
+
return node.children.some(containsOracle);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
/** Reject a value that targets the smart account's own address (a self-call
|
|
414
|
+
* is a structural privilege-escalation hole the interpreter forbids). */
|
|
415
|
+
function assertNotSelfCallAddress(value, config) {
|
|
416
|
+
if (value === config.smartAccountAddress) {
|
|
417
|
+
throw toolError('SCOPE_SELF_CALL', `value \`${value}\` is the smart account's own address (self-call in an allowlist / compare is rejected)`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
/** Tighten-only oracle-params gate. Any widening value (above the wasm-level
|
|
421
|
+
* defaults) throws ORACLE_PARAMS_OUT_OF_RANGE at compile time. */
|
|
422
|
+
function assertOracleParamsTighten(params) {
|
|
423
|
+
if (!params)
|
|
424
|
+
return;
|
|
425
|
+
if (params.maxStalenessSeconds !== undefined &&
|
|
426
|
+
params.maxStalenessSeconds > ORACLE_DEFAULTS.maxStalenessSeconds) {
|
|
427
|
+
throw toolError('ORACLE_PARAMS_OUT_OF_RANGE', `maxStalenessSeconds ${params.maxStalenessSeconds} exceeds default ${ORACLE_DEFAULTS.maxStalenessSeconds}`);
|
|
428
|
+
}
|
|
429
|
+
if (params.maxDeviationBps !== undefined &&
|
|
430
|
+
params.maxDeviationBps > ORACLE_DEFAULTS.maxDeviationBps) {
|
|
431
|
+
throw toolError('ORACLE_PARAMS_OUT_OF_RANGE', `maxDeviationBps ${params.maxDeviationBps} exceeds default ${ORACLE_DEFAULTS.maxDeviationBps}`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
/** Human-readable descriptor for a construct the interpreter adapter cannot
|
|
435
|
+
* express. Mirrors the OZ adapter's `describeCondition` for parity. */
|
|
436
|
+
function describeCondition(cond) {
|
|
437
|
+
switch (cond.op) {
|
|
438
|
+
case 'in':
|
|
439
|
+
return `value allowlist on ${describeSelector(cond.selector)} (predicate DSL)`;
|
|
440
|
+
case 'eq_seq':
|
|
441
|
+
return `exact ordered sequence on ${describeSelector(cond.selector)} (predicate DSL)`;
|
|
442
|
+
case 'not':
|
|
443
|
+
return 'negated condition (predicate DSL)';
|
|
444
|
+
case 'and':
|
|
445
|
+
case 'or':
|
|
446
|
+
return `nested ${cond.op} condition (predicate DSL)`;
|
|
447
|
+
case 'compare': {
|
|
448
|
+
const s = cond.compare.selector;
|
|
449
|
+
switch (s.kind) {
|
|
450
|
+
case 'oracle_price':
|
|
451
|
+
return `oracle price condition on ${s.asset}`;
|
|
452
|
+
case 'invocation_count':
|
|
453
|
+
return `invocation-count window (${s.windowSeconds}s) condition`;
|
|
454
|
+
case 'window_spent':
|
|
455
|
+
return `spend-window comparison with operator '${cond.compare.operator}'`;
|
|
456
|
+
case 'amount':
|
|
457
|
+
return `per-call amount comparison on ${s.token}`;
|
|
458
|
+
case 'arg':
|
|
459
|
+
return `argument comparison on arg ${s.argIndex}`;
|
|
460
|
+
case 'calldata':
|
|
461
|
+
return 'EVM calldata comparison';
|
|
462
|
+
case 'value':
|
|
463
|
+
return 'tx.value comparison';
|
|
464
|
+
case 'now':
|
|
465
|
+
case 'valid_until':
|
|
466
|
+
return 'time comparison';
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
function describeSelector(s) {
|
|
472
|
+
switch (s.kind) {
|
|
473
|
+
case 'arg':
|
|
474
|
+
return `arg ${s.argIndex}`;
|
|
475
|
+
case 'amount':
|
|
476
|
+
return `amount(${s.token})`;
|
|
477
|
+
case 'window_spent':
|
|
478
|
+
return `window_spent(${s.token})`;
|
|
479
|
+
case 'oracle_price':
|
|
480
|
+
return `oracle_price(${s.asset})`;
|
|
481
|
+
case 'invocation_count':
|
|
482
|
+
return `invocation_count(${s.windowSeconds}s)`;
|
|
483
|
+
case 'calldata':
|
|
484
|
+
return `calldata[${s.offset}:${s.offset + s.length}]`;
|
|
485
|
+
default:
|
|
486
|
+
return s.kind;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
function simulateStub() {
|
|
490
|
+
return {
|
|
491
|
+
backend: 'ts-model',
|
|
492
|
+
permitted: null,
|
|
493
|
+
evaluations: [],
|
|
494
|
+
notes: ['stub: real permit/deny semantics wiring is a later phase'],
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
/** Canonical JSON with recursively sorted object keys (stable across runs). */
|
|
498
|
+
function canonicalStringify(value) {
|
|
499
|
+
return JSON.stringify(sortKeys(value));
|
|
500
|
+
}
|
|
501
|
+
function sortKeys(value) {
|
|
502
|
+
if (Array.isArray(value))
|
|
503
|
+
return value.map(sortKeys);
|
|
504
|
+
if (value && typeof value === 'object') {
|
|
505
|
+
const out = {};
|
|
506
|
+
for (const key of Object.keys(value).sort()) {
|
|
507
|
+
out[key] = sortKeys(value[key]);
|
|
508
|
+
}
|
|
509
|
+
return out;
|
|
510
|
+
}
|
|
511
|
+
return value;
|
|
512
|
+
}
|
|
513
|
+
/** Build a synthetic Error carrying the ToolError code/severity/retryable
|
|
514
|
+
* fields the existing ToolError shape uses (the encoder pattern). Callers
|
|
515
|
+
* inspect `e.code` in catch blocks. */
|
|
516
|
+
function toolError(code, message) {
|
|
517
|
+
const err = new Error(message);
|
|
518
|
+
err.code = code;
|
|
519
|
+
err.severity = 'error';
|
|
520
|
+
err.retryable = false;
|
|
521
|
+
return err;
|
|
522
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createInterpreterAdapter, type InterpreterAdapterConfig, ORACLE_DEFAULTS, PLACEHOLDER_INTERPRETER_ADDRESS, } from './adapter.ts';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
// src/adapters/oz/adapter.ts - the OZ Accounts CustodyAdapter
|
|
1
|
+
// src/adapters/oz/adapter.ts - the OZ Accounts CustodyAdapter.
|
|
2
2
|
//
|
|
3
3
|
// Compiles a PolicyIR to an OZ `ProposedPolicy` using OZ built-in policy
|
|
4
|
-
// primitives
|
|
4
|
+
// primitives. Only the constructs OZ can express natively are lowered:
|
|
5
5
|
// scope.contract -> ContextRuleType.call_contract (else default)
|
|
6
6
|
// expiry.validUntilLedger -> ContextRuleDraft.validUntilLedger
|
|
7
7
|
// window_spent(t,w) <= L -> `spending_limit` primitive
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// Anything needing a capability this backend lacks (oracle price, invocation
|
|
10
10
|
// count, per-arg comparison/allowlist, guard, nested boolean predicate) is NOT
|
|
11
11
|
// emitted: it is named in `uncovered` and `covered` is set false. Nothing is
|
|
12
|
-
// silently dropped - the uncovered constructs are
|
|
12
|
+
// silently dropped - the uncovered constructs are named instead.
|
|
13
13
|
//
|
|
14
14
|
// OZ built-in policy instance addresses are per-network deploy artifacts we do
|
|
15
15
|
// not have yet (install is a later phase). They are injected via config; week-1
|
|
@@ -84,8 +84,8 @@ function lowerRule(rule, config) {
|
|
|
84
84
|
const uncovered = [];
|
|
85
85
|
const policyRefs = [];
|
|
86
86
|
// scope -> context rule type. OZ scopes by contract (CallContract); a finer
|
|
87
|
-
// method-level restriction is a predicate concern and must be flagged
|
|
88
|
-
// because CallContract alone permits other methods on the same contract
|
|
87
|
+
// method-level restriction is a predicate concern and must be flagged as not
|
|
88
|
+
// covered because CallContract alone permits other methods on the same contract
|
|
89
89
|
// (e.g. an unbounded approve alongside a capped transfer).
|
|
90
90
|
const contextRuleType = rule.scope.contract !== undefined
|
|
91
91
|
? { kind: 'call_contract', contract: rule.scope.contract }
|
|
@@ -110,11 +110,11 @@ function lowerRule(rule, config) {
|
|
|
110
110
|
uncovered.push('time expiry given as a unix timestamp (OZ context rules expire by ledger sequence; supply expiry.validUntilLedger)');
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
|
-
// guard ->
|
|
113
|
+
// guard -> not covered (applicability predicates are not an OZ built-in).
|
|
114
114
|
if (rule.guard) {
|
|
115
115
|
uncovered.push(`guard: ${describeCondition(rule.guard)}`);
|
|
116
116
|
}
|
|
117
|
-
// constraints -> spending_limit where they match; else
|
|
117
|
+
// constraints -> spending_limit where they match; else not covered. The OZ
|
|
118
118
|
// spending_limit policy takes `{ spending_limit: i128, period_ledgers: u32 }`
|
|
119
119
|
// and has NO token param: it only accepts a CallContract context rule
|
|
120
120
|
// (OnlyCallContractAllowed) and limits transfers of that context's contract,
|
|
@@ -144,7 +144,7 @@ function lowerRule(rule, config) {
|
|
|
144
144
|
}
|
|
145
145
|
// approval.threshold -> simple/weighted threshold primitive. A threshold < 1
|
|
146
146
|
// is not a real M-of-N gate (0 approvals authorises everything), so refuse to
|
|
147
|
-
// emit a no-op primitive and flag it
|
|
147
|
+
// emit a no-op primitive and flag it as not covered instead.
|
|
148
148
|
if (rule.approval) {
|
|
149
149
|
if (!Number.isInteger(rule.approval.threshold) || rule.approval.threshold < 1) {
|
|
150
150
|
uncovered.push(`approval threshold ${rule.approval.threshold} is not a positive integer (a 0 or negative threshold is not an M-of-N gate)`);
|
|
@@ -203,17 +203,19 @@ function matchSpendingLimit(c) {
|
|
|
203
203
|
return null;
|
|
204
204
|
return { token: selector.token, limit: value, windowSeconds: selector.windowSeconds };
|
|
205
205
|
}
|
|
206
|
-
/** Human-readable descriptor for a construct the OZ
|
|
207
|
-
* express, used to populate `uncovered
|
|
206
|
+
/** Human-readable descriptor for a construct the OZ built-in-primitive backend
|
|
207
|
+
* cannot express, used to populate `uncovered`. */
|
|
208
208
|
function describeCondition(cond) {
|
|
209
209
|
switch (cond.op) {
|
|
210
210
|
case 'in':
|
|
211
|
-
return `value allowlist on ${describeSelector(cond.selector)} (arg allowlist
|
|
211
|
+
return `value allowlist on ${describeSelector(cond.selector)} (arg allowlist)`;
|
|
212
|
+
case 'eq_seq':
|
|
213
|
+
return `exact ordered sequence on ${describeSelector(cond.selector)} (OZ built-ins cannot express an exact vector)`;
|
|
212
214
|
case 'not':
|
|
213
|
-
return 'negated condition (predicate DSL
|
|
215
|
+
return 'negated condition (predicate DSL)';
|
|
214
216
|
case 'and':
|
|
215
217
|
case 'or':
|
|
216
|
-
return `nested ${cond.op} condition (predicate DSL
|
|
218
|
+
return `nested ${cond.op} condition (predicate DSL)`;
|
|
217
219
|
case 'compare': {
|
|
218
220
|
const s = cond.compare.selector;
|
|
219
221
|
switch (s.kind) {
|
|
@@ -224,16 +226,16 @@ function describeCondition(cond) {
|
|
|
224
226
|
case 'window_spent':
|
|
225
227
|
return `spend-window comparison with operator '${cond.compare.operator}' (only 'lte' lowers to spending_limit)`;
|
|
226
228
|
case 'amount':
|
|
227
|
-
return `per-call amount comparison on ${s.token} (predicate DSL
|
|
229
|
+
return `per-call amount comparison on ${s.token} (predicate DSL)`;
|
|
228
230
|
case 'arg':
|
|
229
|
-
return `argument comparison on arg ${s.argIndex} (predicate DSL
|
|
231
|
+
return `argument comparison on arg ${s.argIndex} (predicate DSL)`;
|
|
230
232
|
case 'calldata':
|
|
231
|
-
return 'EVM calldata comparison (predicate DSL
|
|
233
|
+
return 'EVM calldata comparison (predicate DSL)';
|
|
232
234
|
case 'value':
|
|
233
|
-
return 'tx.value comparison (predicate DSL
|
|
235
|
+
return 'tx.value comparison (predicate DSL)';
|
|
234
236
|
case 'now':
|
|
235
237
|
case 'valid_until':
|
|
236
|
-
return 'time comparison (predicate DSL
|
|
238
|
+
return 'time comparison (predicate DSL)';
|
|
237
239
|
}
|
|
238
240
|
}
|
|
239
241
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type CompileGateResult = {
|
|
2
|
+
code: 'COMPILE_OK';
|
|
3
|
+
} | {
|
|
4
|
+
code: 'COMPILE_GATE_FAILED';
|
|
5
|
+
stderr: string;
|
|
6
|
+
} | {
|
|
7
|
+
code: 'SKIPPED';
|
|
8
|
+
reason: string;
|
|
9
|
+
};
|
|
10
|
+
export interface CompileGateOpts {
|
|
11
|
+
/** Caller-controlled abort signal; aborting before toolchain probe resolves
|
|
12
|
+
* the gate to `SKIPPED` rather than starting a cargo invocation. */
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
/** Override the `cargo` binary (mostly for tests). */
|
|
15
|
+
cargoBin?: string;
|
|
16
|
+
/** Override the target directory (mostly for tests). */
|
|
17
|
+
tempDir?: string;
|
|
18
|
+
}
|
|
19
|
+
/** Probe the host for the Rust toolchain the gate requires. We require BOTH
|
|
20
|
+
* `cargo` AND `stellar` (the Soroban CLI) because a real gate run invokes
|
|
21
|
+
* `stellar contract build`; absent either, the gate returns `SKIPPED`. */
|
|
22
|
+
export declare function hasRustToolchain(): Promise<boolean>;
|
|
23
|
+
/** Compile-check a Rust source string against the pinned Soroban SDK.
|
|
24
|
+
*
|
|
25
|
+
* - Toolchain absent -> `{ code: 'SKIPPED', reason }` (NOT a failure).
|
|
26
|
+
* - Toolchain present, source compiles -> `{ code: 'COMPILE_OK' }`.
|
|
27
|
+
* - Toolchain present, source broken -> `{ code: 'COMPILE_GATE_FAILED', stderr }`.
|
|
28
|
+
*
|
|
29
|
+
* The gate writes the source to a temporary crate and runs `cargo check`
|
|
30
|
+
* against it; the crate is cleaned up whether or not compilation succeeds.
|
|
31
|
+
* The caller is expected to surface `COMPILE_GATE_FAILED.stderr` to the
|
|
32
|
+
* user (the LLM agent) so the user can iterate. */
|
|
33
|
+
export declare function compileCheck(rustSource: string, opts?: CompileGateOpts): Promise<CompileGateResult>;
|