@crediolabs/policy-synth 0.1.12 → 0.1.14

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.
@@ -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.SynthesizePolicyInputSchema = exports.SynthesizePolicyRecordingInputSchema = exports.InterpreterOptionsSchema = exports.SynthesizePolicyMandateInputSchema = exports.RecordTransactionInputSchema = exports.OzAdapterConfigSchema = exports.ComposeUserResponsesSchema = exports.MandateSpecSchema = 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.NETWORK_PASSPHRASES = exports.DEFAULT_AUTH_VALID_UNTIL_LEDGERS = exports.TESTNET_RPC_URL = exports.PINNED_INTERPRETER_GRAMMAR_VERSION = exports.PINNED_INTERPRETER_WASM_SHA256 = exports.PINNED_INTERPRETER_TESTNET_ADDRESS = exports.VerifyPolicyInputSchema = exports.SimulatePolicyInputSchema = exports.OraclePriceFixtureSchema = exports.PredicateNodeSchema = exports.PredicateLeafSchema = exports.SynthesizePolicyInputSchema = exports.SynthesizePolicyRecordingInputSchema = exports.InterpreterOptionsSchema = exports.SynthesizePolicyMandateInputSchema = exports.RecordTransactionInputSchema = exports.OzAdapterConfigSchema = exports.ComposeUserResponsesSchema = exports.MandateSpecSchema = 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
@@ -222,6 +222,294 @@ exports.SynthesizePolicyInputSchema = zod_1.z.discriminatedUnion('source', [
222
222
  exports.SynthesizePolicyMandateInputSchema,
223
223
  exports.SynthesizePolicyRecordingInputSchema,
224
224
  ]);
225
+ // ===== PredicateNode / PredicateLeaf =====
226
+ //
227
+ // Hand-written zod mirrors of the predicate grammar in types.ts:132-180.
228
+ // Recursive (`and`/`or`/`not` nest), so the schemas use the same
229
+ // `z.lazy` + explicit `z.ZodType<unknown>` annotation that ScValSchema
230
+ // already uses - without the annotation TS's circular type inference
231
+ // breaks the recursive reference. i128-shaped numerics (`num`, `den`)
232
+ // are decimal STRINGS, never JS numbers, matching the on-chain i128
233
+ // convention used everywhere else in this file. The grammar is the
234
+ // single source of truth (types.ts); a drift test in this package
235
+ // asserts the two stay in step.
236
+ /** PredicateLeaf mirrors the core `PredicateLeaf` union. Every variant
237
+ * is a `kind` discriminator; literals carry their primitive value inline.
238
+ * `literal_vec.elements` is recursive (a vec may contain literals that
239
+ * themselves nest) so the leaf schema is lazily self-referential. */
240
+ exports.PredicateLeafSchema = zod_1.z.lazy(() => zod_1.z.union([
241
+ zod_1.z.object({ kind: zod_1.z.literal('call_contract') }),
242
+ zod_1.z.object({ kind: zod_1.z.literal('call_fn') }),
243
+ zod_1.z.object({ kind: zod_1.z.literal('call_arg'), index: zod_1.z.number().int().nonnegative() }),
244
+ zod_1.z.object({ kind: zod_1.z.literal('call_arg_len'), index: zod_1.z.number().int().nonnegative() }),
245
+ zod_1.z.object({
246
+ kind: zod_1.z.literal('call_arg_field'),
247
+ index: zod_1.z.number().int().nonnegative(),
248
+ element: zod_1.z.number().int().nonnegative(),
249
+ field: zod_1.z.string(),
250
+ }),
251
+ // num/den are i128 on chain; JSON numbers lose precision past 2^53,
252
+ // so the wire is a decimal string. The contract refuses `den == 0` and
253
+ // `num <= 0` / `den <= 0` at install (types.ts:162-163 + the install
254
+ // gate); mirroring that bound here closes the gap where simulate /
255
+ // verify green-light a policy the install gate will reject. The
256
+ // positive-only regex is the cheapest unambiguous check.
257
+ zod_1.z.object({
258
+ kind: zod_1.z.literal('call_arg_scaled'),
259
+ index: zod_1.z.number().int().nonnegative(),
260
+ num: zod_1.z.string().regex(/^[1-9][0-9]*$/),
261
+ den: zod_1.z.string().regex(/^[1-9][0-9]*$/),
262
+ }),
263
+ zod_1.z.object({ kind: zod_1.z.literal('amount'), token: zod_1.z.string() }),
264
+ zod_1.z.object({
265
+ kind: zod_1.z.literal('window_spent'),
266
+ token: zod_1.z.string(),
267
+ windowSeconds: zod_1.z.number().int().positive(),
268
+ }),
269
+ zod_1.z.object({ kind: zod_1.z.literal('now') }),
270
+ zod_1.z.object({ kind: zod_1.z.literal('valid_until') }),
271
+ zod_1.z.object({
272
+ kind: zod_1.z.literal('invocation_count_in_window'),
273
+ windowSecs: zod_1.z.number().int().positive(),
274
+ }),
275
+ zod_1.z.object({ kind: zod_1.z.literal('oracle_price'), asset: zod_1.z.string() }),
276
+ zod_1.z.object({
277
+ kind: zod_1.z.literal('oracle_threshold'),
278
+ value: zod_1.z.string().regex(/^-?[0-9]+$/),
279
+ decimals: zod_1.z.number().int().nonnegative(),
280
+ }),
281
+ zod_1.z.object({ kind: zod_1.z.literal('literal_address'), value: zod_1.z.string() }),
282
+ zod_1.z.object({ kind: zod_1.z.literal('literal_i128'), value: zod_1.z.string().regex(/^-?[0-9]+$/) }),
283
+ zod_1.z.object({ kind: zod_1.z.literal('literal_symbol'), value: zod_1.z.string() }),
284
+ // `literal_u32` is u32 on chain. A negative value would wrap, and a
285
+ // value above U32_MAX would be refused at the encode boundary
286
+ // anyway. Bound at the schema so the wire cannot carry a value the
287
+ // contract will reject.
288
+ zod_1.z.object({
289
+ kind: zod_1.z.literal('literal_u32'),
290
+ value: zod_1.z.number().int().nonnegative().max(U32_MAX),
291
+ }),
292
+ zod_1.z.object({ kind: zod_1.z.literal('literal_u64'), value: zod_1.z.string().regex(/^[0-9]+$/) }),
293
+ // `literal_bytes` is hex on chain. `Buffer.from(value, 'hex')` silently
294
+ // drops non-hex chars AND yields an empty buffer when the whole input
295
+ // is non-hex - so a user passing 'zzzz' would get a predicate that
296
+ // compares against empty bytes instead of being rejected. The
297
+ // strict even-length hex regex closes that gap at the boundary.
298
+ zod_1.z.object({
299
+ kind: zod_1.z.literal('literal_bytes'),
300
+ value: zod_1.z
301
+ .string()
302
+ .regex(/^[0-9a-fA-F]*$/)
303
+ .refine((v) => v.length % 2 === 0, 'must be even-length hex'),
304
+ }),
305
+ // elements are themselves PredicateLeaf; lazy to break the cycle.
306
+ zod_1.z.object({ kind: zod_1.z.literal('literal_vec'), elements: zod_1.z.array(exports.PredicateLeafSchema) }),
307
+ ]));
308
+ /** PredicateNode mirrors the core `PredicateNode` union. Recursive through
309
+ * `and`/`or`/`not`; the lazy + annotation pattern keeps the recursion
310
+ * type-safe. */
311
+ exports.PredicateNodeSchema = zod_1.z.lazy(() => zod_1.z.union([
312
+ zod_1.z.object({ op: zod_1.z.literal('and'), children: zod_1.z.array(exports.PredicateNodeSchema) }),
313
+ zod_1.z.object({ op: zod_1.z.literal('or'), children: zod_1.z.array(exports.PredicateNodeSchema) }),
314
+ zod_1.z.object({ op: zod_1.z.literal('not'), child: exports.PredicateNodeSchema }),
315
+ zod_1.z.object({
316
+ op: zod_1.z.literal('eq'),
317
+ left: exports.PredicateLeafSchema,
318
+ right: exports.PredicateLeafSchema,
319
+ }),
320
+ zod_1.z.object({
321
+ op: zod_1.z.enum(['lt', 'lte', 'gt', 'gte']),
322
+ left: exports.PredicateLeafSchema,
323
+ right: exports.PredicateLeafSchema,
324
+ }),
325
+ zod_1.z.object({
326
+ op: zod_1.z.literal('in'),
327
+ needle: exports.PredicateLeafSchema,
328
+ haystack: zod_1.z.array(exports.PredicateLeafSchema),
329
+ }),
330
+ ]));
331
+ // ===== simulate_policy / verify_policy =====
332
+ //
333
+ // The oracle fixture shape mirrors `SimulateOptions` / `VerifyOptions`
334
+ // in packages/policy-synth/src/verify/{simulate,verify}.ts - the same
335
+ // sorted set of error enum values is used by both engines. u32
336
+ // `validUntilLedger` is bounded at the boundary so a hand-crafted
337
+ // payload cannot leak a u32 overflow into the engine's permit context.
338
+ /** Oracle price fixture - mirror of the entry type in `SimulateOptions` /
339
+ * `VerifyOptions`. Discriminated by presence of `error` vs `price`. */
340
+ exports.OraclePriceFixtureSchema = zod_1.z.union([
341
+ zod_1.z.object({
342
+ price: zod_1.z.string().regex(/^[0-9]+$/),
343
+ timestampSeconds: zod_1.z.number().int().nonnegative(),
344
+ }),
345
+ zod_1.z.object({
346
+ error: zod_1.z.enum(['stale', 'missing', 'deviation', 'paused', 'decimals', 'fingerprint']),
347
+ }),
348
+ ]);
349
+ exports.SimulatePolicyInputSchema = zod_1.z.object({
350
+ // simulatePolicy accepts a null predicate (OZ-only policy); verifyPolicy
351
+ // does not. The asymmetry is mirrored at the schema boundary.
352
+ predicate: exports.PredicateNodeSchema.nullable(),
353
+ permitTx: exports.RecordedTransactionSchema,
354
+ validUntilLedger: zod_1.z.number().int().positive().max(U32_MAX).optional(),
355
+ oraclePricesByAsset: zod_1.z.record(zod_1.z.string(), exports.OraclePriceFixtureSchema).optional(),
356
+ });
357
+ exports.VerifyPolicyInputSchema = zod_1.z.object({
358
+ // verifyPolicy requires a non-null predicate; the engine refuses
359
+ // `null` outright. The schema mirrors that contract.
360
+ predicate: exports.PredicateNodeSchema,
361
+ permitTx: exports.RecordedTransactionSchema,
362
+ validUntilLedger: zod_1.z.number().int().positive().max(U32_MAX).optional(),
363
+ oraclePricesByAsset: zod_1.z.record(zod_1.z.string(), exports.OraclePriceFixtureSchema).optional(),
364
+ });
365
+ // ===== install_policy / revoke_policy / get_interpreter_info =====
366
+ //
367
+ // The install/revoke pair drives the OZ smart-account admin flow. Both surface
368
+ // unsigned Soroban transaction XDRs (base64) and the tool body NEVER signs:
369
+ // the wallet signature IS the user-confirmation step. See `run/index.ts` for
370
+ // the rationale (no `action_id` two-call pair; the MCP server is stateless).
371
+ //
372
+ // `get_interpreter_info` is a read-only fingerprint lookup. It returns the
373
+ // pinned address + grammar version + wasm hash AND, when requested, runs an
374
+ // optional `grammar_version()` RPC call to check whether the deployed contract
375
+ // matches the pin. A mismatch is worth MORE than a fabricated audit field.
376
+ //
377
+ // All three input schemas live BELOW the MandateSpecSchemaForRule declaration
378
+ // so the const reference there is not in the temporal dead zone (no JS hoisting
379
+ // for `const`).
380
+ /** Minimal rule-draft schema for the install input. Mirrors ContextRuleDraft
381
+ * from the core (`types.ts`); we keep this hand-rolled + flat rather than
382
+ * importing the recursive discriminated union because the wire shape is
383
+ * fixed (the user supplies one rule, not a nested AST). Declared BEFORE
384
+ * InstallPolicyInputSchema below so the const reference there is not in
385
+ * the temporal dead zone (no JS hoisting for `const`). */
386
+ const MAX_SIGNERS_PER_RULE = 15;
387
+ const MAX_POLICIES_PER_RULE = 5;
388
+ const MandateSpecSchemaForRule = zod_1.z
389
+ .object({
390
+ contextRuleType: zod_1.z.discriminatedUnion('kind', [
391
+ zod_1.z.object({ kind: zod_1.z.literal('default') }),
392
+ zod_1.z.object({ kind: zod_1.z.literal('call_contract'), contract: zod_1.z.string() }),
393
+ zod_1.z.object({ kind: zod_1.z.literal('create_contract'), wasmHash: zod_1.z.string() }),
394
+ ]),
395
+ name: zod_1.z.string().min(1),
396
+ validUntilLedger: zod_1.z.number().int().positive().max(U32_MAX).nullable(),
397
+ signers: zod_1.z
398
+ .array(zod_1.z.discriminatedUnion('kind', [
399
+ zod_1.z.object({ kind: zod_1.z.literal('delegated'), address: zod_1.z.string() }),
400
+ zod_1.z.object({
401
+ kind: zod_1.z.literal('external'),
402
+ verifier: zod_1.z.string(),
403
+ keyBytes: zod_1.z.string(),
404
+ }),
405
+ ]))
406
+ .max(MAX_SIGNERS_PER_RULE),
407
+ policies: zod_1.z
408
+ .array(zod_1.z.discriminatedUnion('kind', [
409
+ zod_1.z.object({
410
+ kind: zod_1.z.literal('interpreter'),
411
+ interpreterAddress: zod_1.z.string(),
412
+ predicateBlobBase64: zod_1.z.string().min(1),
413
+ }),
414
+ zod_1.z.object({
415
+ kind: zod_1.z.literal('oz_builtin'),
416
+ primitive: zod_1.z.object({
417
+ primitive: zod_1.z.enum(['spending_limit', 'simple_threshold', 'weighted_threshold']),
418
+ params: zod_1.z.record(zod_1.z.unknown()),
419
+ }),
420
+ instanceAddress: zod_1.z.string(),
421
+ }),
422
+ ]))
423
+ .max(MAX_POLICIES_PER_RULE),
424
+ })
425
+ .passthrough()
426
+ .refine((v) => !(v.signers.length === 0 && v.policies.length === 0), 'a rule with no signers and no policies is refused at install (NoSignersAndPolicies)');
427
+ // install/revoke/get_info schemas live HERE (after the helper) so the const
428
+ // reference resolves at module init. They are the new tools on top of the
429
+ // existing four.
430
+ /** Pinned interpreter address (testnet). Mirrors DEPLOYMENTS.md:156-157.
431
+ * Single source for the MCP layer; do not embed elsewhere. */
432
+ exports.PINNED_INTERPRETER_TESTNET_ADDRESS = 'CDR4NLV22STCXFGZPNKDQTEANWLF7LZ6AJLY6B7CLJXKHDZGYJWIOKGP';
433
+ /** Pinned interpreter wasm sha256 (hex). Mirrors DEPLOYMENTS.md:156-157. */
434
+ exports.PINNED_INTERPRETER_WASM_SHA256 = '6e6c13d93e197aa380303a42cd120f5ddb080dd36ef2a343ee1dbd04ca52a443';
435
+ /** The grammar version the interpreter enforces (matches SELF_VERSION in
436
+ * packages/policy-interpreter/src/version.rs). */
437
+ exports.PINNED_INTERPRETER_GRAMMAR_VERSION = 1;
438
+ /** Default Soroban RPC for the install / revoke / info tools. The recorder
439
+ * keeps its own copy in record/rpc.ts because it hands back a fetcher rather
440
+ * than a Server; the two are deliberately different surfaces, so this is
441
+ * pinned beside the addresses it is used with rather than shared. */
442
+ exports.TESTNET_RPC_URL = 'https://soroban-testnet.stellar.org';
443
+ /** Soroban `valid_until` window (ledgers) added to the latest ledger when
444
+ * building the auth entry. ~25 minutes at 5s/ledger. */
445
+ exports.DEFAULT_AUTH_VALID_UNTIL_LEDGERS = 300;
446
+ /** Stellar network passphrases. Pinned here so the XDR envelope uses the
447
+ * matching passphrase when the wallet signs (a mismatch yields invalid
448
+ * hashes). */
449
+ exports.NETWORK_PASSPHRASES = {
450
+ testnet: 'Test SDF Network ; September 2015',
451
+ mainnet: 'Public Global Stellar Network ; September 2015',
452
+ };
453
+ exports.InstallPolicyInputSchema = zod_1.z
454
+ .object({
455
+ /** The smart account contract address (C...) that will receive the rule. */
456
+ smartAccount: zod_1.z.string(),
457
+ /** The signer that authorises the install (G... wallet). Used only for
458
+ * sequence number + auth nonce simulation; never persisted, never signed. */
459
+ sourceAccount: zod_1.z.string(),
460
+ /** The proposed rule draft. Mirrors the core `ContextRuleDraft` shape. */
461
+ rule: MandateSpecSchemaForRule,
462
+ /** Per-rule install nonce; 1 for a fresh install. */
463
+ installNonce: zod_1.z.number().int().positive(),
464
+ /** Optional RPC URL override. Defaults to the public testnet endpoint
465
+ * (`TESTNET_RPC_URL`); the override is refused unless
466
+ * `allowUnpinnedRpcUrl: true`, because the auth nonce and
467
+ * rootInvocation in the response come from whichever RPC answered. */
468
+ rpcUrl: zod_1.z.string().url().optional(),
469
+ /** Opt-in to using a non-pinned RPC URL. Without this flag the tool
470
+ * refuses any `rpcUrl` other than `TESTNET_RPC_URL` because the
471
+ * caller's auth-digest binds to whatever the RPC returned. */
472
+ allowUnpinnedRpcUrl: zod_1.z.boolean().optional(),
473
+ /** Opt-in to pointing the rule's interpreter policy at any address
474
+ * other than the pinned testnet interpreter. Default-deny: a caller
475
+ * that controls the interpreter can permit everything. */
476
+ allowUnpinnedInterpreter: zod_1.z.boolean().optional(),
477
+ /** Base fee in stroops; defaults to BASE_FEE (100). */
478
+ baseFee: zod_1.z.number().int().positive().optional(),
479
+ })
480
+ .refine((v) => Boolean(v.smartAccount) && Boolean(v.sourceAccount), {
481
+ message: 'smartAccount and sourceAccount are required',
482
+ });
483
+ exports.RevokePolicyInputSchema = zod_1.z
484
+ .object({
485
+ /** The smart account contract address (C...). */
486
+ smartAccount: zod_1.z.string(),
487
+ /** The wallet that will sign the removal. The ACCOUNT decides whether it
488
+ * accepts that signer; this schema does not assert a rule it cannot
489
+ * verify, since the account's source is not in this repo. Proven on
490
+ * testnet: the account's deployer can revoke. */
491
+ sourceAccount: zod_1.z.string(),
492
+ /** The rule id to remove from the smart account + interpreter. */
493
+ ruleId: zod_1.z.number().int().nonnegative(),
494
+ /** Optional RPC URL override; same pin as install. */
495
+ rpcUrl: zod_1.z.string().url().optional(),
496
+ /** Opt-in to using a non-pinned RPC URL. Same semantics as install. */
497
+ allowUnpinnedRpcUrl: zod_1.z.boolean().optional(),
498
+ /** Base fee in stroops; defaults to BASE_FEE (100). */
499
+ baseFee: zod_1.z.number().int().positive().optional(),
500
+ })
501
+ .refine((v) => Boolean(v.smartAccount) && Boolean(v.sourceAccount), {
502
+ message: 'smartAccount and sourceAccount are required',
503
+ });
504
+ exports.GetInterpreterInfoInputSchema = zod_1.z.object({
505
+ /** Network to query. The pin differs per network; defaults to testnet. */
506
+ network: exports.NetworkSchema.optional(),
507
+ /** When true, perform an optional live `grammar_version()` RPC call to
508
+ * verify the deployed contract matches the pin. */
509
+ verifyLive: zod_1.z.boolean().optional(),
510
+ /** Optional RPC URL override. */
511
+ rpcUrl: zod_1.z.string().url().optional(),
512
+ });
225
513
  // ===== Error envelope (canonical) =====
226
514
  //
227
515
  // Mirrors ToolError from packages/policy-synth/src/errors.ts. We use a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crediolabs/policy-synth",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
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",