@crediolabs/policy-synth 0.3.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/interpreter/adapter.d.ts +2 -2
- package/dist/adapters/interpreter/adapter.js +11 -3
- package/dist/errors.d.ts +6 -1
- package/dist/install/authority-overlap.d.ts +101 -0
- package/dist/install/authority-overlap.js +239 -0
- package/dist/install/build-add-context-rule.d.ts +1 -1
- package/dist/install/index.d.ts +1 -0
- package/dist/install/index.js +4 -0
- package/dist/install/read-account-rules.d.ts +79 -0
- package/dist/install/read-account-rules.js +241 -0
- package/dist/predicate/decode.js +22 -1
- package/dist/predicate/encode.js +52 -5
- package/dist/predicate/from-json.js +14 -1
- package/dist/record/index.d.ts +10 -0
- package/dist/record/index.js +32 -1
- package/dist/record/rpc.d.ts +4 -0
- package/dist/record/rpc.js +4 -1
- package/dist/registry/identify.d.ts +10 -1
- package/dist/registry/identify.js +4 -1
- package/dist/registry/on-chain-spec.d.ts +37 -0
- package/dist/registry/on-chain-spec.js +152 -0
- package/dist/review-card/builder.js +40 -0
- package/dist/review-card/cross-check.js +34 -0
- package/dist/review-card/render-leaf.d.ts +1 -1
- package/dist/review-card/render-leaf.js +7 -0
- package/dist/run/index.d.ts +25 -4
- package/dist/run/index.js +108 -4
- package/dist/run/schemas.d.ts +350 -6
- package/dist/run/schemas.js +95 -6
- package/dist/simulate/deny-cases.js +11 -0
- package/dist/simulate/evaluate.js +86 -5
- package/dist/synth/declare.d.ts +43 -0
- package/dist/synth/declare.js +127 -0
- package/dist/synth/index.d.ts +1 -0
- package/dist/synth/index.js +1 -0
- package/dist/synth/synthesize-from-recording.js +1 -1
- package/dist/types.d.ts +21 -1
- package/dist/types.js +1 -1
- package/dist-cjs/adapters/interpreter/adapter.d.ts +2 -2
- package/dist-cjs/adapters/interpreter/adapter.js +11 -3
- package/dist-cjs/errors.d.ts +6 -1
- package/dist-cjs/install/authority-overlap.d.ts +101 -0
- package/dist-cjs/install/authority-overlap.js +248 -0
- package/dist-cjs/install/build-add-context-rule.d.ts +1 -1
- package/dist-cjs/install/index.d.ts +1 -0
- package/dist-cjs/install/index.js +13 -2
- package/dist-cjs/install/read-account-rules.d.ts +79 -0
- package/dist-cjs/install/read-account-rules.js +252 -0
- package/dist-cjs/predicate/decode.js +22 -1
- package/dist-cjs/predicate/encode.js +52 -5
- package/dist-cjs/predicate/from-json.js +14 -1
- package/dist-cjs/record/index.d.ts +10 -0
- package/dist-cjs/record/index.js +31 -0
- package/dist-cjs/record/rpc.d.ts +4 -0
- package/dist-cjs/record/rpc.js +7 -3
- package/dist-cjs/registry/identify.d.ts +10 -1
- package/dist-cjs/registry/identify.js +4 -0
- package/dist-cjs/registry/on-chain-spec.d.ts +37 -0
- package/dist-cjs/registry/on-chain-spec.js +159 -0
- package/dist-cjs/review-card/builder.js +40 -0
- package/dist-cjs/review-card/cross-check.js +34 -0
- package/dist-cjs/review-card/render-leaf.d.ts +1 -1
- package/dist-cjs/review-card/render-leaf.js +7 -0
- package/dist-cjs/run/index.d.ts +25 -4
- package/dist-cjs/run/index.js +108 -2
- package/dist-cjs/run/schemas.d.ts +350 -6
- package/dist-cjs/run/schemas.js +96 -7
- package/dist-cjs/simulate/deny-cases.js +11 -0
- package/dist-cjs/simulate/evaluate.js +86 -5
- package/dist-cjs/synth/declare.d.ts +43 -0
- package/dist-cjs/synth/declare.js +130 -0
- package/dist-cjs/synth/index.d.ts +1 -0
- package/dist-cjs/synth/index.js +3 -1
- package/dist-cjs/synth/synthesize-from-recording.js +1 -1
- package/dist-cjs/types.d.ts +21 -1
- package/dist-cjs/types.js +1 -1
- package/package.json +1 -1
- package/src/adapters/interpreter/adapter.ts +13 -5
- package/src/errors.ts +5 -0
- package/src/install/authority-overlap.ts +323 -0
- package/src/install/index.ts +20 -0
- package/src/install/read-account-rules.ts +313 -0
- package/src/predicate/decode.ts +22 -1
- package/src/predicate/encode.ts +55 -5
- package/src/predicate/from-json.ts +14 -1
- package/src/record/index.ts +59 -2
- package/src/record/rpc.ts +4 -1
- package/src/registry/identify.ts +4 -1
- package/src/registry/on-chain-spec.ts +168 -0
- package/src/review-card/builder.ts +45 -2
- package/src/review-card/cross-check.ts +35 -1
- package/src/review-card/render-leaf.ts +8 -1
- package/src/run/index.ts +125 -2
- package/src/run/schemas.ts +100 -6
- package/src/simulate/deny-cases.ts +12 -1
- package/src/simulate/evaluate.ts +101 -9
- package/src/synth/declare.ts +206 -0
- package/src/synth/index.ts +5 -0
- package/src/synth/synthesize-from-recording.ts +1 -1
- package/src/types.ts +16 -1
package/dist-cjs/run/schemas.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// imports them here so its tool-shape bindings stay in step; the CLI imports
|
|
17
17
|
// them here so it can build the same args envelope the MCP transport builds.
|
|
18
18
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
|
-
exports.ToolErrorSchema = exports.GetInterpreterInfoInputSchema = exports.RevokePolicyInputSchema = exports.InstallPolicyInputSchema = exports.NETWORK_PASSPHRASES = exports.RPC_URL_BY_NETWORK = exports.PINNED_INTERPRETER_ADDRESS_BY_NETWORK = exports.MAINNET_RPC_URL = exports.TESTNET_RPC_URL = exports.PINNED_INTERPRETER_GRAMMAR_VERSION = exports.PINNED_INTERPRETER_WASM_SHA256 = exports.PINNED_INTERPRETER_MAINNET_ADDRESS = exports.PINNED_INTERPRETER_TESTNET_ADDRESS = exports.VerifyPolicyInputSchema = exports.SimulatePolicyInputSchema = exports.PredicateNodeSchema = exports.PredicateLeafSchema = exports.SynthesizePolicyInputSchema = exports.InterpreterOptionsSchema = exports.RecordTransactionInputSchema = exports.ComposeUserResponsesSchema = exports.RecordedTransactionSchema = exports.ParseConfidenceSchema = exports.OnChainEventSchema = exports.TokenMovementSchema = exports.ContractInvocationSchema = exports.ScValSchema = exports.NetworkSchema = void 0;
|
|
19
|
+
exports.ToolErrorSchema = exports.GetInterpreterInfoInputSchema = exports.RevokePolicyInputSchema = exports.InstallPolicyInputSchema = exports.DeclarePolicyInputSchema = exports.NETWORK_PASSPHRASES = exports.RPC_URL_BY_NETWORK = exports.PINNED_INTERPRETER_ADDRESS_BY_NETWORK = exports.MAINNET_RPC_URL = exports.TESTNET_RPC_URL = exports.PINNED_INTERPRETER_GRAMMAR_VERSION = exports.PINNED_INTERPRETER_WASM_SHA256 = exports.PINNED_INTERPRETER_MAINNET_ADDRESS = exports.PINNED_INTERPRETER_TESTNET_ADDRESS = exports.ObservedRuleSchema = exports.VerifyPolicyInputSchema = exports.SimulatePolicyInputSchema = exports.PredicateNodeSchema = exports.PredicateLeafSchema = exports.SynthesizePolicyInputSchema = exports.InterpreterOptionsSchema = exports.RecordTransactionInputSchema = exports.ComposeUserResponsesSchema = exports.RecordedTransactionSchema = exports.ParseConfidenceSchema = exports.OnChainEventSchema = exports.TokenMovementSchema = exports.ContractInvocationSchema = exports.ScValSchema = exports.NetworkSchema = void 0;
|
|
20
20
|
const zod_1 = require("zod");
|
|
21
21
|
const address_ts_1 = require("../synth/address.js");
|
|
22
22
|
/** Soroban `valid_until` is a u32 ledger sequence; a value above this cannot be
|
|
@@ -180,6 +180,15 @@ exports.PredicateLeafSchema = zod_1.z.lazy(() => zod_1.z.union([
|
|
|
180
180
|
element: zod_1.z.number().int().nonnegative(),
|
|
181
181
|
field: zod_1.z.string(),
|
|
182
182
|
}),
|
|
183
|
+
// num/den are i128 decimal strings, matching `literal_i128`. The regex
|
|
184
|
+
// is the boundary guard; the ratio's SIGN is checked at encode, where
|
|
185
|
+
// the message can explain that a negative ratio inverts the comparison.
|
|
186
|
+
zod_1.z.object({
|
|
187
|
+
kind: zod_1.z.literal('call_arg_scaled'),
|
|
188
|
+
index: zod_1.z.number().int().nonnegative(),
|
|
189
|
+
num: zod_1.z.string().regex(/^-?[0-9]+$/),
|
|
190
|
+
den: zod_1.z.string().regex(/^-?[0-9]+$/),
|
|
191
|
+
}),
|
|
183
192
|
zod_1.z.object({ kind: zod_1.z.literal('literal_address'), value: zod_1.z.string() }),
|
|
184
193
|
zod_1.z.object({ kind: zod_1.z.literal('literal_i128'), value: zod_1.z.string().regex(/^-?[0-9]+$/) }),
|
|
185
194
|
zod_1.z.object({ kind: zod_1.z.literal('literal_symbol'), value: zod_1.z.string() }),
|
|
@@ -198,16 +207,32 @@ exports.PredicateLeafSchema = zod_1.z.lazy(() => zod_1.z.union([
|
|
|
198
207
|
* `and`; the lazy + annotation pattern keeps the recursion type-safe. */
|
|
199
208
|
exports.PredicateNodeSchema = zod_1.z.lazy(() => zod_1.z.union([
|
|
200
209
|
zod_1.z.object({ op: zod_1.z.literal('and'), children: zod_1.z.array(exports.PredicateNodeSchema) }),
|
|
210
|
+
zod_1.z.object({ op: zod_1.z.literal('or'), children: zod_1.z.array(exports.PredicateNodeSchema) }),
|
|
201
211
|
zod_1.z.object({
|
|
202
212
|
op: zod_1.z.literal('eq'),
|
|
203
213
|
left: exports.PredicateLeafSchema,
|
|
204
214
|
right: exports.PredicateLeafSchema,
|
|
205
215
|
}),
|
|
216
|
+
zod_1.z.object({
|
|
217
|
+
op: zod_1.z.literal('lt'),
|
|
218
|
+
left: exports.PredicateLeafSchema,
|
|
219
|
+
right: exports.PredicateLeafSchema,
|
|
220
|
+
}),
|
|
206
221
|
zod_1.z.object({
|
|
207
222
|
op: zod_1.z.literal('lte'),
|
|
208
223
|
left: exports.PredicateLeafSchema,
|
|
209
224
|
right: exports.PredicateLeafSchema,
|
|
210
225
|
}),
|
|
226
|
+
zod_1.z.object({
|
|
227
|
+
op: zod_1.z.literal('gt'),
|
|
228
|
+
left: exports.PredicateLeafSchema,
|
|
229
|
+
right: exports.PredicateLeafSchema,
|
|
230
|
+
}),
|
|
231
|
+
zod_1.z.object({
|
|
232
|
+
op: zod_1.z.literal('gte'),
|
|
233
|
+
left: exports.PredicateLeafSchema,
|
|
234
|
+
right: exports.PredicateLeafSchema,
|
|
235
|
+
}),
|
|
211
236
|
zod_1.z.object({
|
|
212
237
|
op: zod_1.z.literal('in'),
|
|
213
238
|
needle: exports.PredicateLeafSchema,
|
|
@@ -249,6 +274,26 @@ exports.VerifyPolicyInputSchema = exports.SimulatePolicyInputSchema;
|
|
|
249
274
|
* the temporal dead zone (no JS hoisting for `const`). */
|
|
250
275
|
const MAX_SIGNERS_PER_RULE = 15;
|
|
251
276
|
const MAX_POLICIES_PER_RULE = 5;
|
|
277
|
+
const SignerDraftSchema = zod_1.z.discriminatedUnion('kind', [
|
|
278
|
+
zod_1.z.object({ kind: zod_1.z.literal('delegated'), address: zod_1.z.string() }),
|
|
279
|
+
zod_1.z.object({ kind: zod_1.z.literal('external'), verifier: zod_1.z.string(), keyBytes: zod_1.z.string() }),
|
|
280
|
+
]);
|
|
281
|
+
const ContextTypeSchema = zod_1.z.discriminatedUnion('kind', [
|
|
282
|
+
zod_1.z.object({ kind: zod_1.z.literal('default') }),
|
|
283
|
+
zod_1.z.object({ kind: zod_1.z.literal('call_contract'), contract: zod_1.z.string() }),
|
|
284
|
+
zod_1.z.object({ kind: zod_1.z.literal('create_contract'), wasmHash: zod_1.z.string() }),
|
|
285
|
+
]);
|
|
286
|
+
/** A rule ALREADY on the account, as the caller observed it. Supplying these
|
|
287
|
+
* turns on the cross-rule authority scan: a signer belonging to several rules
|
|
288
|
+
* picks which one applies, so a predicate only constrains a key when the
|
|
289
|
+
* policed rule is the only rule that key is on. */
|
|
290
|
+
exports.ObservedRuleSchema = zod_1.z.object({
|
|
291
|
+
id: zod_1.z.number().int().nonnegative(),
|
|
292
|
+
contextType: ContextTypeSchema,
|
|
293
|
+
signers: zod_1.z.array(SignerDraftSchema),
|
|
294
|
+
policyAddresses: zod_1.z.array(zod_1.z.string()),
|
|
295
|
+
predicate: exports.PredicateNodeSchema.optional(),
|
|
296
|
+
});
|
|
252
297
|
const ContextRuleDraftSchema = zod_1.z
|
|
253
298
|
.object({
|
|
254
299
|
contextRuleType: zod_1.z.discriminatedUnion('kind', [
|
|
@@ -283,23 +328,23 @@ const ContextRuleDraftSchema = zod_1.z
|
|
|
283
328
|
// existing four.
|
|
284
329
|
/** Pinned interpreter address (testnet).
|
|
285
330
|
* Single source for the MCP layer; do not embed elsewhere. */
|
|
286
|
-
exports.PINNED_INTERPRETER_TESTNET_ADDRESS = '
|
|
287
|
-
/** Pinned interpreter address (mainnet), redeployed 2026-08-22 from a reproducible build. The mainnet
|
|
331
|
+
exports.PINNED_INTERPRETER_TESTNET_ADDRESS = 'CCBHVZ6HGGV7C4SNHCZ3S5665Z2WEMHTMBAEPO4XW6PKON464BEBANU5';
|
|
332
|
+
/** Pinned interpreter address (mainnet), redeployed 2026-08-22 for grammar 4, from a reproducible build. The mainnet
|
|
288
333
|
* interpreter IS the binary exercised on testnet - both instances were created
|
|
289
334
|
* from the same uploaded wasm hash (see PINNED_INTERPRETER_WASM_SHA256), and
|
|
290
|
-
* both were read back with `grammar_version()` returning
|
|
335
|
+
* both were read back with `grammar_version()` returning 4. The address differs
|
|
291
336
|
* because instance ids are network-scoped. UNAUDITED at the time of writing.
|
|
292
337
|
*
|
|
293
338
|
* These four constants move together or not at all. The grammar version and
|
|
294
339
|
* wasm hash are single values covering BOTH networks, so re-pinning one network
|
|
295
340
|
* alone would have the builder emit a version the other network refuses - with
|
|
296
341
|
* a green test run, since `grammar-version-parity.test.ts` would then pass. */
|
|
297
|
-
exports.PINNED_INTERPRETER_MAINNET_ADDRESS = '
|
|
342
|
+
exports.PINNED_INTERPRETER_MAINNET_ADDRESS = 'CDN755TDYZM3ZQ5OXTJ6TIBUBWZV2KRI2BYJPBXD2MVWED4STT3VBN52';
|
|
298
343
|
/** Pinned interpreter wasm sha256 (hex). */
|
|
299
|
-
exports.PINNED_INTERPRETER_WASM_SHA256 = '
|
|
344
|
+
exports.PINNED_INTERPRETER_WASM_SHA256 = 'b5ba1e35ccf20cd8c13c3a2c3098bf337033a92bcaf475d63c03ddc0cba0fcae';
|
|
300
345
|
/** The grammar version the interpreter enforces (matches SELF_VERSION in
|
|
301
346
|
* contracts/policy-interpreter/src/version.rs). */
|
|
302
|
-
exports.PINNED_INTERPRETER_GRAMMAR_VERSION =
|
|
347
|
+
exports.PINNED_INTERPRETER_GRAMMAR_VERSION = 4;
|
|
303
348
|
/** Default Soroban RPC for the install / revoke / info tools. The recorder
|
|
304
349
|
* keeps its own copy in record/rpc.ts because it hands back a fetcher rather
|
|
305
350
|
* than a Server; the two are deliberately different surfaces, so this is
|
|
@@ -338,8 +383,52 @@ exports.NETWORK_PASSPHRASES = {
|
|
|
338
383
|
// `sourceAccount` is the signing wallet (G...).
|
|
339
384
|
const STELLAR_CONTRACT_ADDRESS = /^C[2-7A-Z]{55}$/;
|
|
340
385
|
const STELLAR_ACCOUNT_ADDRESS = /^G[2-7A-Z]{55}$/;
|
|
386
|
+
// ===== declare_policy =====
|
|
387
|
+
//
|
|
388
|
+
// The declarative front-end: the constraint stated outright, with no
|
|
389
|
+
// transaction to decode. Deliberately NOT a revival of the removed
|
|
390
|
+
// `MandateSpec` - that carried a rolling `spendingLimit` the interpreter
|
|
391
|
+
// cannot evaluate and an `approvalThreshold` needing OZ primitives nobody
|
|
392
|
+
// deployed. Only fields grammar 3 can actually enforce appear here.
|
|
393
|
+
exports.DeclarePolicyInputSchema = zod_1.z
|
|
394
|
+
.object({
|
|
395
|
+
fn: zod_1.z.string().min(1, 'fn must name the method to pin'),
|
|
396
|
+
contract: zod_1.z
|
|
397
|
+
.string()
|
|
398
|
+
.regex(STELLAR_CONTRACT_ADDRESS, 'contract must be a Stellar contract address (C...)')
|
|
399
|
+
.optional(),
|
|
400
|
+
/** Smallest unit, unsigned decimal STRING - an i128 is wider than
|
|
401
|
+
* Number.MAX_SAFE_INTEGER, so a number here would silently round. */
|
|
402
|
+
maxAmount: zod_1.z
|
|
403
|
+
.string()
|
|
404
|
+
.regex(/^[0-9]+$/, 'maxAmount must be an unsigned integer in the smallest unit')
|
|
405
|
+
.optional(),
|
|
406
|
+
amountArgIndex: zod_1.z.number().int().nonnegative().max(U32_MAX).optional(),
|
|
407
|
+
recipients: zod_1.z.array(zod_1.z.string()).min(1, 'recipients must not be empty').optional(),
|
|
408
|
+
recipientArgIndex: zod_1.z.number().int().nonnegative().max(U32_MAX).optional(),
|
|
409
|
+
allowZeroCap: zod_1.z.boolean().optional(),
|
|
410
|
+
/** Minimum output as a ratio of the call's own input. num/den are decimal
|
|
411
|
+
* STRINGS for the same reason maxAmount is: an i128 ratio does not
|
|
412
|
+
* survive a JS number. */
|
|
413
|
+
minOutputRatio: zod_1.z
|
|
414
|
+
.object({
|
|
415
|
+
num: zod_1.z.string().regex(/^[0-9]+$/, 'num must be an unsigned integer'),
|
|
416
|
+
den: zod_1.z.string().regex(/^[0-9]+$/, 'den must be an unsigned integer'),
|
|
417
|
+
inputArgIndex: zod_1.z.number().int().nonnegative().max(U32_MAX),
|
|
418
|
+
outputArgIndex: zod_1.z.number().int().nonnegative().max(U32_MAX),
|
|
419
|
+
})
|
|
420
|
+
.strict()
|
|
421
|
+
.optional(),
|
|
422
|
+
})
|
|
423
|
+
.strict();
|
|
341
424
|
exports.InstallPolicyInputSchema = zod_1.z
|
|
342
425
|
.object({
|
|
426
|
+
/** Rules already on the account. Supplying them turns on the cross-rule
|
|
427
|
+
* authority scan, which reports every existing rule a signer of this
|
|
428
|
+
* install could name INSTEAD - including an unpoliced one, against which
|
|
429
|
+
* the predicate never runs. Absent means the scan is skipped, and the
|
|
430
|
+
* result says so rather than reporting "no overlaps found". */
|
|
431
|
+
existingRules: zod_1.z.array(exports.ObservedRuleSchema).optional(),
|
|
343
432
|
/** The smart account contract address (C...) that will receive the rule. */
|
|
344
433
|
smartAccount: zod_1.z
|
|
345
434
|
.string()
|
|
@@ -205,11 +205,22 @@ function visit(node, facts) {
|
|
|
205
205
|
for (const child of node.children)
|
|
206
206
|
visit(child, facts);
|
|
207
207
|
return;
|
|
208
|
+
// NOT descended into. A deny case works by violating ONE constraint and
|
|
209
|
+
// asserting the predicate refuses the call. Violating one branch of an
|
|
210
|
+
// `or` proves nothing, because another branch can still permit, so the
|
|
211
|
+
// generated case would either fail or pass for the wrong reason. A sound
|
|
212
|
+
// deny case for a disjunction must violate EVERY branch at once, which
|
|
213
|
+
// this generator does not construct - so it emits none.
|
|
214
|
+
case 'or':
|
|
215
|
+
return;
|
|
208
216
|
case 'in':
|
|
209
217
|
facts.memberships.push(node);
|
|
210
218
|
return;
|
|
211
219
|
case 'eq':
|
|
220
|
+
case 'lt':
|
|
212
221
|
case 'lte':
|
|
222
|
+
case 'gt':
|
|
223
|
+
case 'gte':
|
|
213
224
|
facts.comparisons.push(node);
|
|
214
225
|
}
|
|
215
226
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// src/simulate/evaluate.ts - TypeScript reference evaluator for grammar version
|
|
2
|
+
// src/simulate/evaluate.ts - TypeScript reference evaluator for grammar version 4.
|
|
3
3
|
//
|
|
4
4
|
// Pure function. Determinism: same `(predicate, ctx)` -> byte-identical result,
|
|
5
5
|
// no clock, no randomness. Deny order (deny on FIRST violation, stable reason):
|
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
// 3. `in` membership; empty haystack ALWAYS denies -> 'NOT_IN_ALLOWLIST'
|
|
10
10
|
// 4. otherwise permit.
|
|
11
11
|
//
|
|
12
|
-
// Grammar version
|
|
13
|
-
// Grammar version
|
|
12
|
+
// Grammar version 4 nodes: and, or, eq, lt, lte, gt, gte, in
|
|
13
|
+
// Grammar version 4 leaves: call_contract, call_fn, call_arg(i),
|
|
14
14
|
// call_arg_len(i), call_arg_field(i, element, field),
|
|
15
|
+
// call_arg_scaled(i, num, den),
|
|
15
16
|
// literal_address, literal_i128, literal_symbol, literal_u32, literal_vec
|
|
16
|
-
// Grammar version
|
|
17
|
-
// UNSUPPORTED_NODE, NOT_IN_ALLOWLIST
|
|
17
|
+
// Grammar version 4 deny reasons: ARG_MISMATCH, CONTRACT_SCOPE,
|
|
18
|
+
// ARITHMETIC_OVERFLOW, UNSUPPORTED_NODE, NOT_IN_ALLOWLIST, SLIPPAGE_FLOOR
|
|
18
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
19
20
|
exports.evaluate = evaluate;
|
|
20
21
|
/** Evaluate a `PredicateNode` against the candidate call described by `ctx`.
|
|
@@ -36,8 +37,25 @@ function walk(node, ctx) {
|
|
|
36
37
|
}
|
|
37
38
|
return lastDeny ?? { permit: true };
|
|
38
39
|
}
|
|
40
|
+
// Permits on the first branch that holds; when none does, reports the
|
|
41
|
+
// FIRST branch's reason. Mirrors `Node::Or` in the Rust evaluator, which
|
|
42
|
+
// the conformance suite pins.
|
|
43
|
+
case 'or': {
|
|
44
|
+
let firstDeny = null;
|
|
45
|
+
for (const child of node.children) {
|
|
46
|
+
const r = walk(child, ctx);
|
|
47
|
+
if (r.permit)
|
|
48
|
+
return r;
|
|
49
|
+
if (firstDeny === null)
|
|
50
|
+
firstDeny = r;
|
|
51
|
+
}
|
|
52
|
+
return firstDeny ?? { permit: false, reason: 'UNSUPPORTED_NODE' };
|
|
53
|
+
}
|
|
39
54
|
case 'eq':
|
|
55
|
+
case 'lt':
|
|
40
56
|
case 'lte':
|
|
57
|
+
case 'gt':
|
|
58
|
+
case 'gte':
|
|
41
59
|
return evalCompare(node.op, node.left, node.right, ctx);
|
|
42
60
|
case 'in':
|
|
43
61
|
return evalIn(node.needle, node.haystack, ctx);
|
|
@@ -45,6 +63,16 @@ function walk(node, ctx) {
|
|
|
45
63
|
}
|
|
46
64
|
/** Comparison leaf evaluation. */
|
|
47
65
|
function evalCompare(op, left, right, ctx) {
|
|
66
|
+
// Scaled operands first, so the dedicated reasons reach the caller instead
|
|
67
|
+
// of a generic mismatch. Right-hand dispatch leads because
|
|
68
|
+
// `out >= in * num / den` is the canonical swap form. Mirrors the order in
|
|
69
|
+
// `eval_compare` on the Rust side.
|
|
70
|
+
if (right.kind === 'call_arg_scaled') {
|
|
71
|
+
return evalScaledCompare(op, left, right, true, ctx);
|
|
72
|
+
}
|
|
73
|
+
if (left.kind === 'call_arg_scaled') {
|
|
74
|
+
return evalScaledCompare(op, right, left, false, ctx);
|
|
75
|
+
}
|
|
48
76
|
// CONTRACT_SCOPE on call_contract eq
|
|
49
77
|
if (left.kind === 'call_contract' && op === 'eq') {
|
|
50
78
|
if (right.kind !== 'literal_address')
|
|
@@ -99,6 +127,53 @@ function evalCompare(op, left, right, ctx) {
|
|
|
99
127
|
// Unknown leaf/op combination - structural fail-closed.
|
|
100
128
|
return { permit: false, reason: 'UNSUPPORTED_NODE' };
|
|
101
129
|
}
|
|
130
|
+
/** i128 bounds. The contract computes in i128 and denies on overflow, so the
|
|
131
|
+
* reference has to draw the same line or the two layers disagree on inputs
|
|
132
|
+
* near the boundary. */
|
|
133
|
+
const I128_MIN = -(2n ** 127n);
|
|
134
|
+
const I128_MAX = 2n ** 127n - 1n;
|
|
135
|
+
/** Comparison where one side is `call_arg_scaled`. Mirrors
|
|
136
|
+
* `eval_scaled_arg_compare`: `args[index] * num / den` truncating toward
|
|
137
|
+
* zero, ARITHMETIC_OVERFLOW on arithmetic that does not fit or a zero
|
|
138
|
+
* denominator, SLIPPAGE_FLOOR on a comparison that simply fails. */
|
|
139
|
+
function evalScaledCompare(op, other, scaled, scaledOnRight, ctx) {
|
|
140
|
+
// Chaining two computed operands has no meaning a review card could state.
|
|
141
|
+
if (other.kind === 'call_arg_scaled')
|
|
142
|
+
return { permit: false, reason: 'UNSUPPORTED_NODE' };
|
|
143
|
+
// Could not READ the operand is a different failure from read-and-missed.
|
|
144
|
+
const input = argNumericBigInt(ctx.args[scaled.index]);
|
|
145
|
+
if (input === null)
|
|
146
|
+
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
147
|
+
let num;
|
|
148
|
+
let den;
|
|
149
|
+
try {
|
|
150
|
+
num = BigInt(scaled.num);
|
|
151
|
+
den = BigInt(scaled.den);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
155
|
+
}
|
|
156
|
+
if (den === 0n)
|
|
157
|
+
return { permit: false, reason: 'ARITHMETIC_OVERFLOW' };
|
|
158
|
+
const product = input * num;
|
|
159
|
+
if (product < I128_MIN || product > I128_MAX) {
|
|
160
|
+
return { permit: false, reason: 'ARITHMETIC_OVERFLOW' };
|
|
161
|
+
}
|
|
162
|
+
// BigInt division already truncates toward zero, matching i128 semantics.
|
|
163
|
+
const quotient = product / den;
|
|
164
|
+
if (quotient < I128_MIN || quotient > I128_MAX) {
|
|
165
|
+
return { permit: false, reason: 'ARITHMETIC_OVERFLOW' };
|
|
166
|
+
}
|
|
167
|
+
const otherVal = other.kind === 'call_arg'
|
|
168
|
+
? argNumericBigInt(ctx.args[other.index])
|
|
169
|
+
: literalNumericBigInt(other);
|
|
170
|
+
if (otherVal === null)
|
|
171
|
+
return { permit: false, reason: 'ARG_MISMATCH' };
|
|
172
|
+
const [a, b] = scaledOnRight ? [otherVal, quotient] : [quotient, otherVal];
|
|
173
|
+
return bigintCmp(op, a.toString(), b.toString())
|
|
174
|
+
? { permit: true }
|
|
175
|
+
: { permit: false, reason: 'SLIPPAGE_FLOOR' };
|
|
176
|
+
}
|
|
102
177
|
/** Per-ScVal equality. Handles literal_vec as an EXACT ordered sequence:
|
|
103
178
|
* compare element-by-element in order; deny if length or any element differs.
|
|
104
179
|
* Opaque args (`type: 'other'`) fail closed. */
|
|
@@ -268,7 +343,13 @@ function bigintCmp(op, aStr, bStr) {
|
|
|
268
343
|
switch (op) {
|
|
269
344
|
case 'eq':
|
|
270
345
|
return a === b;
|
|
346
|
+
case 'lt':
|
|
347
|
+
return a < b;
|
|
271
348
|
case 'lte':
|
|
272
349
|
return a <= b;
|
|
350
|
+
case 'gt':
|
|
351
|
+
return a > b;
|
|
352
|
+
case 'gte':
|
|
353
|
+
return a >= b;
|
|
273
354
|
}
|
|
274
355
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { PredicateNode } from '../types.ts';
|
|
2
|
+
export interface PolicyDeclaration {
|
|
3
|
+
/** Method to pin. Required: a predicate with no selector leaf constrains
|
|
4
|
+
* nothing and the contract refuses it at install. */
|
|
5
|
+
fn: string;
|
|
6
|
+
/** Contract to pin, already resolved to a `C...` address. */
|
|
7
|
+
contract?: string;
|
|
8
|
+
/** Upper bound on the call's amount argument, in the token's SMALLEST
|
|
9
|
+
* unit as an unsigned decimal string (25 XLM = "250000000"). */
|
|
10
|
+
maxAmount?: string;
|
|
11
|
+
/** Which argument carries the amount. Defaults to the SEP-41 position. */
|
|
12
|
+
amountArgIndex?: number;
|
|
13
|
+
/** Recipient allowlist. */
|
|
14
|
+
recipients?: string[];
|
|
15
|
+
/** Which argument carries the recipient. Defaults to the SEP-41 position. */
|
|
16
|
+
recipientArgIndex?: number;
|
|
17
|
+
/** A cap of "0" denies every call, so it is refused unless asked for
|
|
18
|
+
* explicitly. A rule that permits nothing is a plausible thing to want and
|
|
19
|
+
* an implausible thing to want by accident. */
|
|
20
|
+
allowZeroCap?: boolean;
|
|
21
|
+
/** Minimum output as a ratio of the call's own input: the output argument
|
|
22
|
+
* must be at least `input * num / den`.
|
|
23
|
+
*
|
|
24
|
+
* A swap's acceptable output depends on the size of the trade, so a fixed
|
|
25
|
+
* floor would pin the policy to one trade size. The ratio is DECLARED, never
|
|
26
|
+
* inferred from a recording: a recorded rate is a price at one moment, and
|
|
27
|
+
* freezing it as policy would deny ordinary trades later. */
|
|
28
|
+
minOutputRatio?: {
|
|
29
|
+
num: string;
|
|
30
|
+
den: string;
|
|
31
|
+
inputArgIndex: number;
|
|
32
|
+
outputArgIndex: number;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export interface DeclaredPredicate {
|
|
36
|
+
predicate: PredicateNode;
|
|
37
|
+
/** Assumptions the caller should check. Never empty when an argument index
|
|
38
|
+
* was defaulted rather than supplied. */
|
|
39
|
+
warnings: string[];
|
|
40
|
+
}
|
|
41
|
+
/** Lower a declared constraint to a grammar-3 predicate. Pure and total:
|
|
42
|
+
* the same declaration always produces the same predicate. */
|
|
43
|
+
export declare function declarePredicate(d: PolicyDeclaration): DeclaredPredicate;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/synth/declare.ts - build a predicate from a DECLARED constraint.
|
|
3
|
+
//
|
|
4
|
+
// The second synthesis front-end, beside `synthesizeFromRecording`. That one
|
|
5
|
+
// infers a predicate from a transaction that happened; this one takes the
|
|
6
|
+
// constraint the user states outright. No transaction, no decoding, no
|
|
7
|
+
// inference, no RPC: the same declaration always lowers to a byte-identical
|
|
8
|
+
// predicate.
|
|
9
|
+
//
|
|
10
|
+
// This is the surviving half of the removed `MandateSpec`. Two of that type's
|
|
11
|
+
// five fields lowered to things that do not exist - `spendingLimit` became a
|
|
12
|
+
// `window_spent` compare the interpreter cannot evaluate (it is handed one
|
|
13
|
+
// call and keeps no state), and `approvalThreshold` needed OZ built-in policy
|
|
14
|
+
// contracts that were never deployed. Both are deliberately absent here, and
|
|
15
|
+
// the per-call `maxAmount` bound below is the honest replacement for the
|
|
16
|
+
// first: it constrains the amount in THIS call rather than implying a rolling
|
|
17
|
+
// total nothing tracks.
|
|
18
|
+
//
|
|
19
|
+
// What a declaration can say maps one-to-one onto grammar 4:
|
|
20
|
+
// fn -> eq(call_fn, literal_symbol)
|
|
21
|
+
// contract -> eq(call_contract, literal_address)
|
|
22
|
+
// maxAmount -> lte(call_arg(i), literal_i128)
|
|
23
|
+
// recipients -> in(call_arg(j), [literal_address, ...])
|
|
24
|
+
// minOutputRatio-> gte(call_arg(out), call_arg_scaled(in, num, den))
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.declarePredicate = declarePredicate;
|
|
27
|
+
const address_ts_1 = require("./address.js");
|
|
28
|
+
/** Argument positions of the SEP-41 `transfer(from, to, amount)` shape. A
|
|
29
|
+
* declaration that names a different method almost certainly has different
|
|
30
|
+
* positions, which is why using either default emits a warning naming the
|
|
31
|
+
* index it assumed - a bound on the wrong argument constrains something the
|
|
32
|
+
* user did not mean and fails silently. */
|
|
33
|
+
const SEP41_RECIPIENT_ARG = 1;
|
|
34
|
+
const SEP41_AMOUNT_ARG = 2;
|
|
35
|
+
/** Lower a declared constraint to a grammar-3 predicate. Pure and total:
|
|
36
|
+
* the same declaration always produces the same predicate. */
|
|
37
|
+
function declarePredicate(d) {
|
|
38
|
+
if (!d.fn || d.fn.trim() === '') {
|
|
39
|
+
throw declareError('SYNTHESIS_ERROR', 'a declaration needs `fn`: the method to pin');
|
|
40
|
+
}
|
|
41
|
+
const warnings = [];
|
|
42
|
+
const children = [
|
|
43
|
+
{ op: 'eq', left: { kind: 'call_fn' }, right: { kind: 'literal_symbol', value: d.fn } },
|
|
44
|
+
];
|
|
45
|
+
if (d.contract !== undefined) {
|
|
46
|
+
if (!(0, address_ts_1.isStellarAddress)(d.contract) || !d.contract.startsWith('C')) {
|
|
47
|
+
throw declareError('SYNTHESIS_ERROR', `contract must be a Stellar contract address (C...), got ${d.contract}`);
|
|
48
|
+
}
|
|
49
|
+
children.push({
|
|
50
|
+
op: 'eq',
|
|
51
|
+
left: { kind: 'call_contract' },
|
|
52
|
+
right: { kind: 'literal_address', value: d.contract },
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
if (d.recipients !== undefined) {
|
|
56
|
+
if (d.recipients.length === 0) {
|
|
57
|
+
throw declareError('SYNTHESIS_ERROR', 'recipients was supplied but empty; an empty `in` haystack is refused at decode. Omit it to leave recipients unconstrained.');
|
|
58
|
+
}
|
|
59
|
+
for (const r of d.recipients) {
|
|
60
|
+
if (!(0, address_ts_1.isStellarAddress)(r)) {
|
|
61
|
+
throw declareError('SYNTHESIS_ERROR', `recipient is not a Stellar address: ${r}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const idx = d.recipientArgIndex ?? SEP41_RECIPIENT_ARG;
|
|
65
|
+
if (d.recipientArgIndex === undefined) {
|
|
66
|
+
warnings.push(`recipient allowlist bound to call_arg(${idx}), the SEP-41 \`transfer\` position. If \`${d.fn}\` carries the recipient elsewhere this constrains the wrong argument - pass recipientArgIndex.`);
|
|
67
|
+
}
|
|
68
|
+
children.push({
|
|
69
|
+
op: 'in',
|
|
70
|
+
needle: { kind: 'call_arg', index: idx },
|
|
71
|
+
haystack: d.recipients.map((value) => ({ kind: 'literal_address', value })),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
if (d.maxAmount !== undefined) {
|
|
75
|
+
if (!/^[0-9]+$/.test(d.maxAmount)) {
|
|
76
|
+
throw declareError('SYNTHESIS_ERROR', `maxAmount must be an unsigned integer in the token's smallest unit, got "${d.maxAmount}" (25 XLM = "250000000")`);
|
|
77
|
+
}
|
|
78
|
+
if (d.maxAmount === '0' && d.allowZeroCap !== true) {
|
|
79
|
+
throw declareError('SYNTHESIS_ERROR', 'maxAmount "0" denies every call: no amount satisfies the bound. Set allowZeroCap to declare that deliberately.');
|
|
80
|
+
}
|
|
81
|
+
const idx = d.amountArgIndex ?? SEP41_AMOUNT_ARG;
|
|
82
|
+
if (d.amountArgIndex === undefined) {
|
|
83
|
+
warnings.push(`amount cap bound to call_arg(${idx}), the SEP-41 \`transfer\` position. If \`${d.fn}\` carries the amount elsewhere this caps the wrong argument - pass amountArgIndex.`);
|
|
84
|
+
}
|
|
85
|
+
children.push({
|
|
86
|
+
op: 'lte',
|
|
87
|
+
left: { kind: 'call_arg', index: idx },
|
|
88
|
+
right: { kind: 'literal_i128', value: d.maxAmount },
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
if (d.minOutputRatio !== undefined) {
|
|
92
|
+
const { num, den, inputArgIndex, outputArgIndex } = d.minOutputRatio;
|
|
93
|
+
if (!/^[0-9]+$/.test(num) || !/^[0-9]+$/.test(den)) {
|
|
94
|
+
throw declareError('SYNTHESIS_ERROR', `minOutputRatio num/den must be unsigned integers, got "${num}"/"${den}" (a 1% slippage tolerance is num "99", den "100")`);
|
|
95
|
+
}
|
|
96
|
+
// Both are refused on chain at install (INVALID_SCALED_RATIO). Refusing
|
|
97
|
+
// here too means the caller learns before a transaction is built.
|
|
98
|
+
if (den === '0') {
|
|
99
|
+
throw declareError('SYNTHESIS_ERROR', 'minOutputRatio.den is zero: the ratio has no value');
|
|
100
|
+
}
|
|
101
|
+
if (num === '0') {
|
|
102
|
+
throw declareError('SYNTHESIS_ERROR', 'minOutputRatio.num is zero: the floor would be zero, which constrains nothing. Omit it instead.');
|
|
103
|
+
}
|
|
104
|
+
if (inputArgIndex === outputArgIndex) {
|
|
105
|
+
throw declareError('SYNTHESIS_ERROR', `minOutputRatio bounds arg[${inputArgIndex}] against itself, which is true for any ratio at or below 1 and false above it - never a slippage floor. Pass the distinct input and output positions.`);
|
|
106
|
+
}
|
|
107
|
+
if (BigInt(num) > BigInt(den)) {
|
|
108
|
+
// Demanding MORE out than went in is not slippage protection; it is a
|
|
109
|
+
// rule that denies every honest trade. Loud beats a policy that never
|
|
110
|
+
// permits.
|
|
111
|
+
warnings.push(`minOutputRatio ${num}/${den} is above 1: it requires the output to EXCEED the input, which no ordinary swap satisfies. Check the ratio is not inverted.`);
|
|
112
|
+
}
|
|
113
|
+
children.push({
|
|
114
|
+
op: 'gte',
|
|
115
|
+
left: { kind: 'call_arg', index: outputArgIndex },
|
|
116
|
+
right: { kind: 'call_arg_scaled', index: inputArgIndex, num, den },
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
// A single conjunct is emitted bare. `and` with one child encodes to
|
|
120
|
+
// different bytes than the child alone, and the extra node buys nothing.
|
|
121
|
+
const predicate = children.length === 1 ? children[0] : { op: 'and', children };
|
|
122
|
+
return { predicate, warnings };
|
|
123
|
+
}
|
|
124
|
+
function declareError(code, message) {
|
|
125
|
+
const err = new Error(message);
|
|
126
|
+
err.code = code;
|
|
127
|
+
err.severity = 'error';
|
|
128
|
+
err.retryable = false;
|
|
129
|
+
throw err;
|
|
130
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { isStellarAddress } from './address.ts';
|
|
2
2
|
export { type ComposeOptions, type ComposeResult, composeFromRecording, } from './compose-from-recording.ts';
|
|
3
|
+
export { type DeclaredPredicate, declarePredicate, type PolicyDeclaration, } from './declare.ts';
|
|
3
4
|
export { type IntentFacts, lower } from './lower.ts';
|
|
4
5
|
export { type DecideScopeOptions, decideScope, type ScopeDecision, scopeToContextRuleType, } from './scope.ts';
|
|
5
6
|
export { type __TestInterpreterAdapterOptions, type SynthesizeFromRecordingOptions, synthesizeFromRecording, } from './synthesize-from-recording.ts';
|
package/dist-cjs/synth/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// src/synth/index.ts - re-export the synthesizer front-ends.
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
exports.synthesizeFromRecording = exports.scopeToContextRuleType = exports.decideScope = exports.lower = exports.composeFromRecording = exports.isStellarAddress = void 0;
|
|
4
|
+
exports.synthesizeFromRecording = exports.scopeToContextRuleType = exports.decideScope = exports.lower = exports.declarePredicate = exports.composeFromRecording = exports.isStellarAddress = void 0;
|
|
5
5
|
// Exported because `swapRecipientAllowlist` is part of the public synthesis
|
|
6
6
|
// input: a caller assembling one needs the same validator the schema applies,
|
|
7
7
|
// and re-deriving it elsewhere means a second address check that can drift.
|
|
@@ -9,6 +9,8 @@ var address_ts_1 = require("./address.js");
|
|
|
9
9
|
Object.defineProperty(exports, "isStellarAddress", { enumerable: true, get: function () { return address_ts_1.isStellarAddress; } });
|
|
10
10
|
var compose_from_recording_ts_1 = require("./compose-from-recording.js");
|
|
11
11
|
Object.defineProperty(exports, "composeFromRecording", { enumerable: true, get: function () { return compose_from_recording_ts_1.composeFromRecording; } });
|
|
12
|
+
var declare_ts_1 = require("./declare.js");
|
|
13
|
+
Object.defineProperty(exports, "declarePredicate", { enumerable: true, get: function () { return declare_ts_1.declarePredicate; } });
|
|
12
14
|
var lower_ts_1 = require("./lower.js");
|
|
13
15
|
Object.defineProperty(exports, "lower", { enumerable: true, get: function () { return lower_ts_1.lower; } });
|
|
14
16
|
var scope_ts_1 = require("./scope.js");
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// src/synth/synthesize-from-recording.ts - recording-path orchestrator.
|
|
3
3
|
//
|
|
4
4
|
// `synthesizeFromRecording` INFERS a bounded policy from a `RecordedTransaction`
|
|
5
|
-
// via the
|
|
5
|
+
// via the composer + interpreter adapter pair.
|
|
6
6
|
// Flow: validate -> parseConfidence gate -> lower -> decideScope -> composeFromRecording
|
|
7
7
|
// -> interpreter compile + self-verify.
|
|
8
8
|
// Same (tx, opts) -> byte-identical ProposedPolicy (no randomness, clock, globals).
|
package/dist-cjs/types.d.ts
CHANGED
|
@@ -124,7 +124,7 @@ export type PolicyRef = {
|
|
|
124
124
|
* `contracts/policy-interpreter/src/version.rs`. Every value this package puts on
|
|
125
125
|
* the wire derives from here; `grammar-version-parity.test.ts` pins it to the
|
|
126
126
|
* contract so a skew cannot pass a green test run. */
|
|
127
|
-
export declare const GRAMMAR_VERSION:
|
|
127
|
+
export declare const GRAMMAR_VERSION: 4;
|
|
128
128
|
/** A serialised predicate document stored against a (smart_account, rule_id) pair. */
|
|
129
129
|
export interface PolicyDocument {
|
|
130
130
|
/** Grammar version baked into the interpreter wasm. Fail-closed strict match at install
|
|
@@ -146,14 +146,29 @@ export interface PolicyDocument {
|
|
|
146
146
|
export type PredicateNode = {
|
|
147
147
|
op: 'and';
|
|
148
148
|
children: PredicateNode[];
|
|
149
|
+
} | {
|
|
150
|
+
op: 'or';
|
|
151
|
+
children: PredicateNode[];
|
|
149
152
|
} | {
|
|
150
153
|
op: 'eq';
|
|
151
154
|
left: PredicateLeaf;
|
|
152
155
|
right: PredicateLeaf;
|
|
156
|
+
} | {
|
|
157
|
+
op: 'lt';
|
|
158
|
+
left: PredicateLeaf;
|
|
159
|
+
right: PredicateLeaf;
|
|
153
160
|
} | {
|
|
154
161
|
op: 'lte';
|
|
155
162
|
left: PredicateLeaf;
|
|
156
163
|
right: PredicateLeaf;
|
|
164
|
+
} | {
|
|
165
|
+
op: 'gt';
|
|
166
|
+
left: PredicateLeaf;
|
|
167
|
+
right: PredicateLeaf;
|
|
168
|
+
} | {
|
|
169
|
+
op: 'gte';
|
|
170
|
+
left: PredicateLeaf;
|
|
171
|
+
right: PredicateLeaf;
|
|
157
172
|
} | {
|
|
158
173
|
op: 'in';
|
|
159
174
|
needle: PredicateLeaf;
|
|
@@ -174,6 +189,11 @@ export type PredicateLeaf = {
|
|
|
174
189
|
index: number;
|
|
175
190
|
element: number;
|
|
176
191
|
field: string;
|
|
192
|
+
} | {
|
|
193
|
+
kind: 'call_arg_scaled';
|
|
194
|
+
index: number;
|
|
195
|
+
num: string;
|
|
196
|
+
den: string;
|
|
177
197
|
} | {
|
|
178
198
|
kind: 'literal_address';
|
|
179
199
|
value: string;
|
package/dist-cjs/types.js
CHANGED
|
@@ -55,4 +55,4 @@ exports.PREDICATE_CAPS = {
|
|
|
55
55
|
* `contracts/policy-interpreter/src/version.rs`. Every value this package puts on
|
|
56
56
|
* the wire derives from here; `grammar-version-parity.test.ts` pins it to the
|
|
57
57
|
* contract so a skew cannot pass a green test run. */
|
|
58
|
-
exports.GRAMMAR_VERSION =
|
|
58
|
+
exports.GRAMMAR_VERSION = 4;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@crediolabs/policy-synth",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Off-chain TypeScript synthesis core for the OZ Accounts Policy Builder. Records Soroban transactions, synthesises the minimal policy that permits exactly that flow, verifies it, and returns an unsigned install transaction.",
|
|
6
6
|
"type": "module",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// src/adapters/interpreter/adapter.ts - the interpreter-policy
|
|
1
|
+
// src/adapters/interpreter/adapter.ts - the interpreter-policy adapter.
|
|
2
2
|
//
|
|
3
|
-
// Compiles a
|
|
3
|
+
// Compiles a `ComposedRule` to a single interpreter `PolicyDocument` + `PolicyRef`
|
|
4
4
|
// carrying the canonical predicate encoding from `predicate/encode.ts`. This is
|
|
5
5
|
// the only backend; the compose step lowers every constraint to it.
|
|
6
6
|
//
|
|
@@ -41,9 +41,9 @@ export interface InterpreterAdapterConfig {
|
|
|
41
41
|
smartAccountAddress: string
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
/** The result of compiling a
|
|
44
|
+
/** The result of compiling a `ComposedRule` for this backend. */
|
|
45
45
|
export interface CompileResult {
|
|
46
|
-
/** false => some
|
|
46
|
+
/** false => some constraint this backend cannot express (see `uncovered`). */
|
|
47
47
|
covered: boolean
|
|
48
48
|
/** Human-readable list of unsupported constructs. */
|
|
49
49
|
uncovered: string[]
|
|
@@ -195,14 +195,22 @@ function assertNoSelfCall(node: PredicateNode, config: InterpreterAdapterConfig)
|
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
197
|
switch (node.op) {
|
|
198
|
+
// Both boolean nodes recurse. `or` is listed explicitly rather than left
|
|
199
|
+
// to a default branch: this is a security check, and a new node kind must
|
|
200
|
+
// fail to compile here rather than silently skip it.
|
|
198
201
|
case 'and':
|
|
202
|
+
case 'or':
|
|
199
203
|
for (const c of node.children) assertNoSelfCall(c, config)
|
|
200
204
|
return
|
|
201
205
|
case 'in':
|
|
202
206
|
// The needle is a selector; only the haystack carries literals.
|
|
203
207
|
for (const h of node.haystack) checkLeaf(h)
|
|
204
208
|
return
|
|
205
|
-
|
|
209
|
+
case 'eq':
|
|
210
|
+
case 'lt':
|
|
211
|
+
case 'lte':
|
|
212
|
+
case 'gt':
|
|
213
|
+
case 'gte':
|
|
206
214
|
checkLeaf(node.left)
|
|
207
215
|
checkLeaf(node.right)
|
|
208
216
|
}
|