@crediolabs/policy-synth 0.1.12 → 0.1.13

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.
@@ -18,11 +18,21 @@
18
18
  // No business logic. No retries. No session state. The same call shape can
19
19
  // drive the CLI (which calls into the same core directly without MCP).
20
20
  Object.defineProperty(exports, "__esModule", { value: true });
21
- exports.ToolErrorSchema = exports.SynthesizePolicyInputSchema = exports.RecordTransactionInputSchema = exports.RecordedTransactionSchema = exports.OzAdapterConfigSchema = exports.NetworkSchema = exports.MandateSpecSchema = exports.InterpreterOptionsSchema = exports.ComposeUserResponsesSchema = void 0;
21
+ exports.VerifyPolicyInputSchema = exports.ToolErrorSchema = exports.SynthesizePolicyInputSchema = exports.SimulatePolicyInputSchema = exports.RevokePolicyInputSchema = exports.RecordTransactionInputSchema = exports.RecordedTransactionSchema = exports.PredicateNodeSchema = exports.PredicateLeafSchema = exports.PINNED_INTERPRETER_WASM_SHA256 = exports.PINNED_INTERPRETER_TESTNET_ADDRESS = exports.PINNED_INTERPRETER_GRAMMAR_VERSION = exports.OzAdapterConfigSchema = exports.OraclePriceFixtureSchema = exports.NetworkSchema = exports.MandateSpecSchema = exports.InterpreterOptionsSchema = exports.InstallPolicyInputSchema = exports.GetInterpreterInfoInputSchema = exports.ComposeUserResponsesSchema = void 0;
22
22
  exports.runRecordTransaction = runRecordTransaction;
23
23
  exports.runSynthesizePolicy = runSynthesizePolicy;
24
+ exports.runSimulatePolicy = runSimulatePolicy;
25
+ exports.runVerifyPolicy = runVerifyPolicy;
26
+ exports.runInstallPolicy = runInstallPolicy;
27
+ exports.runRevokePolicy = runRevokePolicy;
28
+ exports.runGetInterpreterInfo = runGetInterpreterInfo;
24
29
  exports.caughtError = caughtError;
30
+ const node_crypto_1 = require("node:crypto");
31
+ const stellar_sdk_1 = require("@stellar/stellar-sdk");
25
32
  const index_ts_1 = require("../index.js");
33
+ const build_install_policy_ts_1 = require("../install/build-install-policy.js");
34
+ const get_interpreter_info_ts_1 = require("../install/get-interpreter-info.js");
35
+ const index_ts_2 = require("../verify/index.js");
26
36
  const schemas_ts_1 = require("./schemas.js");
27
37
  // Re-export the underlying Zod schemas so the MCP package (and any other
28
38
  // downstream consumer) can import the canonical input shapes from the same
@@ -30,14 +40,25 @@ const schemas_ts_1 = require("./schemas.js");
30
40
  // truth - MCP tool shapes are derived from them.
31
41
  var schemas_ts_2 = require("./schemas.js");
32
42
  Object.defineProperty(exports, "ComposeUserResponsesSchema", { enumerable: true, get: function () { return schemas_ts_2.ComposeUserResponsesSchema; } });
43
+ Object.defineProperty(exports, "GetInterpreterInfoInputSchema", { enumerable: true, get: function () { return schemas_ts_2.GetInterpreterInfoInputSchema; } });
44
+ Object.defineProperty(exports, "InstallPolicyInputSchema", { enumerable: true, get: function () { return schemas_ts_2.InstallPolicyInputSchema; } });
33
45
  Object.defineProperty(exports, "InterpreterOptionsSchema", { enumerable: true, get: function () { return schemas_ts_2.InterpreterOptionsSchema; } });
34
46
  Object.defineProperty(exports, "MandateSpecSchema", { enumerable: true, get: function () { return schemas_ts_2.MandateSpecSchema; } });
35
47
  Object.defineProperty(exports, "NetworkSchema", { enumerable: true, get: function () { return schemas_ts_2.NetworkSchema; } });
48
+ Object.defineProperty(exports, "OraclePriceFixtureSchema", { enumerable: true, get: function () { return schemas_ts_2.OraclePriceFixtureSchema; } });
36
49
  Object.defineProperty(exports, "OzAdapterConfigSchema", { enumerable: true, get: function () { return schemas_ts_2.OzAdapterConfigSchema; } });
50
+ Object.defineProperty(exports, "PINNED_INTERPRETER_GRAMMAR_VERSION", { enumerable: true, get: function () { return schemas_ts_2.PINNED_INTERPRETER_GRAMMAR_VERSION; } });
51
+ Object.defineProperty(exports, "PINNED_INTERPRETER_TESTNET_ADDRESS", { enumerable: true, get: function () { return schemas_ts_2.PINNED_INTERPRETER_TESTNET_ADDRESS; } });
52
+ Object.defineProperty(exports, "PINNED_INTERPRETER_WASM_SHA256", { enumerable: true, get: function () { return schemas_ts_2.PINNED_INTERPRETER_WASM_SHA256; } });
53
+ Object.defineProperty(exports, "PredicateLeafSchema", { enumerable: true, get: function () { return schemas_ts_2.PredicateLeafSchema; } });
54
+ Object.defineProperty(exports, "PredicateNodeSchema", { enumerable: true, get: function () { return schemas_ts_2.PredicateNodeSchema; } });
37
55
  Object.defineProperty(exports, "RecordedTransactionSchema", { enumerable: true, get: function () { return schemas_ts_2.RecordedTransactionSchema; } });
38
56
  Object.defineProperty(exports, "RecordTransactionInputSchema", { enumerable: true, get: function () { return schemas_ts_2.RecordTransactionInputSchema; } });
57
+ Object.defineProperty(exports, "RevokePolicyInputSchema", { enumerable: true, get: function () { return schemas_ts_2.RevokePolicyInputSchema; } });
58
+ Object.defineProperty(exports, "SimulatePolicyInputSchema", { enumerable: true, get: function () { return schemas_ts_2.SimulatePolicyInputSchema; } });
39
59
  Object.defineProperty(exports, "SynthesizePolicyInputSchema", { enumerable: true, get: function () { return schemas_ts_2.SynthesizePolicyInputSchema; } });
40
60
  Object.defineProperty(exports, "ToolErrorSchema", { enumerable: true, get: function () { return schemas_ts_2.ToolErrorSchema; } });
61
+ Object.defineProperty(exports, "VerifyPolicyInputSchema", { enumerable: true, get: function () { return schemas_ts_2.VerifyPolicyInputSchema; } });
41
62
  /** `record_transaction` body - wraps `recordTransaction`. The tool input
42
63
  * matches the core RecordInput minus the injected `fetcher` (the transport
43
64
  * layer does not own the RPC). Returns the core ToolResponse unchanged.
@@ -132,11 +153,323 @@ function resolveOzConfig(input) {
132
153
  // placeholder OZ instance addresses are deterministic.
133
154
  return (0, index_ts_1.placeholderOzConfig)('mainnet');
134
155
  }
156
+ /** `simulate_policy` body - thin wrapper over `simulatePolicy`. The engine
157
+ * already returns fail-closed `{ok:false, error}` for runtime failures
158
+ * (SIMULATION_ERROR), so the try/catch envelope is for raw SDK throws
159
+ * only - same pattern as the other two wrappers. The predicate is
160
+ * passed inline (stateless by design; no `proposed_policy_id` lookup). */
161
+ async function runSimulatePolicy(raw) {
162
+ const parsed = schemas_ts_1.SimulatePolicyInputSchema.safeParse(raw);
163
+ if (!parsed.success) {
164
+ return {
165
+ ok: false,
166
+ error: validationError('simulate_policy', parsed.error.issues),
167
+ };
168
+ }
169
+ const input = parsed.data;
170
+ try {
171
+ // The recursive PredicateNodeSchema + ContractInvocationSchema are
172
+ // typed `z.ZodType<unknown>` to survive TS's circular inference; the
173
+ // engine wants typed `PredicateNode | null` + `RecordedTransaction`.
174
+ // The schema already validated the shape, so assert through the
175
+ // unknown back to the core types. Same pattern as the recordedTx
176
+ // cast in `runSynthesizePolicy`.
177
+ return (0, index_ts_2.simulatePolicy)(input.predicate, input.permitTx, {
178
+ ...(input.validUntilLedger !== undefined
179
+ ? { validUntilLedger: input.validUntilLedger }
180
+ : {}),
181
+ ...(input.oraclePricesByAsset !== undefined
182
+ ? { oraclePricesByAsset: input.oraclePricesByAsset }
183
+ : {}),
184
+ });
185
+ }
186
+ catch (e) {
187
+ return {
188
+ ok: false,
189
+ error: caughtError('simulate_policy', 'SIMULATION_ERROR', e),
190
+ };
191
+ }
192
+ }
193
+ /** `verify_policy` body - thin wrapper over `verifyPolicy`. The engine
194
+ * already returns `{ok:false, error}` with code VERIFICATION_FAILED when
195
+ * the minimality check fails; the try/catch envelope is for raw SDK
196
+ * throws only. Mirrors `runSimulatePolicy` exactly. */
197
+ async function runVerifyPolicy(raw) {
198
+ const parsed = schemas_ts_1.VerifyPolicyInputSchema.safeParse(raw);
199
+ if (!parsed.success) {
200
+ return {
201
+ ok: false,
202
+ error: validationError('verify_policy', parsed.error.issues),
203
+ };
204
+ }
205
+ const input = parsed.data;
206
+ try {
207
+ // Same cast as runSimulatePolicy: the recursive schemas are typed
208
+ // `unknown`; the engine wants typed `PredicateNode` +
209
+ // `RecordedTransaction`. The schema already validated the shape.
210
+ return (0, index_ts_2.verifyPolicy)(input.predicate, input.permitTx, {
211
+ ...(input.validUntilLedger !== undefined ? { validUntilLedger: input.validUntilLedger } : {}),
212
+ ...(input.oraclePricesByAsset !== undefined
213
+ ? { oraclePricesByAsset: input.oraclePricesByAsset }
214
+ : {}),
215
+ });
216
+ }
217
+ catch (e) {
218
+ return {
219
+ ok: false,
220
+ error: caughtError('verify_policy', 'VERIFICATION_FAILED', e),
221
+ };
222
+ }
223
+ }
224
+ /** `install_policy` body - thin wrapper over `buildInstallPolicyXdr`.
225
+ * Returns the unsigned Soroban transaction envelope (base64 XDR) the
226
+ * wallet signs. The wallet signature IS the user-confirmation step - no
227
+ * `action_id` two-call pair (the server is stateless, see server.ts:10-12).
228
+ * Per design decision 4, only CALL 1 (account.add_context_rule) is
229
+ * emitted; CALL 2 (interpreter.install) requires the rule id the account
230
+ * assigns in call 1 and is documented under `followUp` in the response.
231
+ *
232
+ * Default-deny: an interpreter policy address other than the pinned
233
+ * testnet interpreter is REFUSED (the smart account would delegate to
234
+ * an interpreter the caller controls); the same applies to a non-pinned
235
+ * RPC URL (the auth nonce the wallet signs comes from the RPC). Both
236
+ * gates accept an explicit opt-in flag. */
237
+ async function runInstallPolicy(raw) {
238
+ const parsed = schemas_ts_1.InstallPolicyInputSchema.safeParse(raw);
239
+ if (!parsed.success) {
240
+ return {
241
+ ok: false,
242
+ error: validationError('install_policy', parsed.error.issues),
243
+ };
244
+ }
245
+ const input = parsed.data;
246
+ // ---- Pinning gates (default-deny) ----
247
+ const pinningError = enforceInterpreterPin(input.rule.policies, input.allowUnpinnedInterpreter);
248
+ if (pinningError) {
249
+ return { ok: false, error: pinningError };
250
+ }
251
+ if (input.rpcUrl && input.rpcUrl !== schemas_ts_1.TESTNET_RPC_URL && input.allowUnpinnedRpcUrl !== true) {
252
+ return {
253
+ ok: false,
254
+ error: {
255
+ code: 'INSTALL_BUILD_FAILED',
256
+ message: `install_policy: rpcUrl must equal the pinned TESTNET_RPC_URL; set allowUnpinnedRpcUrl: true to opt in to a custom endpoint`,
257
+ severity: 'error',
258
+ retryable: false,
259
+ remediation: { toolCall: { name: 'install_policy', args: {} } },
260
+ },
261
+ };
262
+ }
263
+ let rpcClient;
264
+ try {
265
+ rpcClient = buildRpcClientFromInput(input.rpcUrl);
266
+ }
267
+ catch (e) {
268
+ return {
269
+ ok: false,
270
+ error: caughtError('install_policy', 'INSTALL_BUILD_FAILED', e),
271
+ };
272
+ }
273
+ try {
274
+ const interpreterPolicy = input.rule.policies.find((p) => p.kind === 'interpreter');
275
+ const encodedPredicate = interpreterPolicy?.predicateBlobBase64 ?? '';
276
+ const predicateHash = (0, node_crypto_1.createHash)('sha256')
277
+ .update(Buffer.from(encodedPredicate, 'base64'))
278
+ .digest('hex');
279
+ const result = await (0, build_install_policy_ts_1.buildInstallPolicyXdr)({
280
+ smartAccount: input.smartAccount,
281
+ sourceAccount: input.sourceAccount,
282
+ networkPassphrase: schemas_ts_1.NETWORK_PASSPHRASES.testnet,
283
+ rule: input.rule,
284
+ installNonce: input.installNonce,
285
+ encodedPredicate,
286
+ predicateHash,
287
+ rpc: rpcClient,
288
+ ...(input.baseFee !== undefined ? { baseFee: input.baseFee } : {}),
289
+ });
290
+ return { ok: true, data: result };
291
+ }
292
+ catch (e) {
293
+ return {
294
+ ok: false,
295
+ error: caughtError('install_policy', 'INSTALL_BUILD_FAILED', e),
296
+ };
297
+ }
298
+ }
299
+ /** `revoke_policy` body - thin wrapper over `buildRevokePolicyXdr`.
300
+ * Emits an unsigned XDR for `account.remove_context_rule(ruleId)`; the
301
+ * smart account itself handles uninstalling each attached policy. Auth
302
+ * is master-only; the source account MUST be the master signer set.
303
+ *
304
+ * Same RPC pin as install: a non-pinned `rpcUrl` is refused unless
305
+ * `allowUnpinnedRpcUrl: true`. Revoke does not carry an interpreter
306
+ * policy payload, so the interpreter pin is not re-checked here. */
307
+ async function runRevokePolicy(raw) {
308
+ const parsed = schemas_ts_1.RevokePolicyInputSchema.safeParse(raw);
309
+ if (!parsed.success) {
310
+ return {
311
+ ok: false,
312
+ error: validationError('revoke_policy', parsed.error.issues),
313
+ };
314
+ }
315
+ const input = parsed.data;
316
+ if (input.rpcUrl && input.rpcUrl !== schemas_ts_1.TESTNET_RPC_URL && input.allowUnpinnedRpcUrl !== true) {
317
+ return {
318
+ ok: false,
319
+ error: {
320
+ code: 'REVOKE_BUILD_FAILED',
321
+ message: `revoke_policy: rpcUrl must equal the pinned TESTNET_RPC_URL; set allowUnpinnedRpcUrl: true to opt in to a custom endpoint`,
322
+ severity: 'error',
323
+ retryable: false,
324
+ remediation: { toolCall: { name: 'revoke_policy', args: {} } },
325
+ },
326
+ };
327
+ }
328
+ let rpcClient;
329
+ try {
330
+ rpcClient = buildRpcClientFromInput(input.rpcUrl);
331
+ }
332
+ catch (e) {
333
+ return {
334
+ ok: false,
335
+ error: caughtError('revoke_policy', 'REVOKE_BUILD_FAILED', e),
336
+ };
337
+ }
338
+ try {
339
+ const result = await (0, build_install_policy_ts_1.buildRevokePolicyXdr)({
340
+ smartAccount: input.smartAccount,
341
+ sourceAccount: input.sourceAccount,
342
+ ruleId: input.ruleId,
343
+ networkPassphrase: schemas_ts_1.NETWORK_PASSPHRASES.testnet,
344
+ rpc: rpcClient,
345
+ ...(input.baseFee !== undefined ? { baseFee: input.baseFee } : {}),
346
+ });
347
+ return { ok: true, data: result };
348
+ }
349
+ catch (e) {
350
+ return {
351
+ ok: false,
352
+ error: caughtError('revoke_policy', 'REVOKE_BUILD_FAILED', e),
353
+ };
354
+ }
355
+ }
356
+ /** `get_interpreter_info` body - thin wrapper over `getInterpreterInfo`.
357
+ * Returns the pinned deployment fingerprint + an optional live
358
+ * `grammar_version()` comparison. The audit field is deliberately
359
+ * OMITTED (phase-04's "audit #44" has no source of truth in the repo -
360
+ * fabricating it would be a lie on a security surface; the live
361
+ * mismatch check is worth MORE). */
362
+ async function runGetInterpreterInfo(raw) {
363
+ const parsed = schemas_ts_1.GetInterpreterInfoInputSchema.safeParse(raw);
364
+ if (!parsed.success) {
365
+ return {
366
+ ok: false,
367
+ error: validationError('get_interpreter_info', parsed.error.issues),
368
+ };
369
+ }
370
+ const input = parsed.data;
371
+ const network = input.network ?? 'testnet';
372
+ // Asking about mainnet used to return the TESTNET pin with `network:
373
+ // 'mainnet'` stamped on it. DEPLOYMENTS.md:3 says plainly "Testnet only.
374
+ // Nothing is deployed to mainnet yet", so that answer described a contract
375
+ // that does not exist at an address that is not on that network. This tool
376
+ // exists to tell a caller what they are installing against; inventing a
377
+ // mainnet deployment is the same failure as inventing an audit reference.
378
+ if (network === 'mainnet') {
379
+ return {
380
+ ok: false,
381
+ error: {
382
+ code: 'RECORDING_FAILED',
383
+ message: 'get_interpreter_info: no interpreter is deployed to mainnet. The pinned address and wasm hash are testnet-only; mainnet deployment is gated on the interpreter audit.',
384
+ severity: 'error',
385
+ retryable: false,
386
+ remediation: {
387
+ toolCall: { name: 'get_interpreter_info', args: { network: 'testnet' } },
388
+ },
389
+ },
390
+ };
391
+ }
392
+ try {
393
+ let deployedGrammarVersion;
394
+ if (input.verifyLive === true) {
395
+ const rpcClient = buildRpcClientFromInput(input.rpcUrl);
396
+ deployedGrammarVersion = await rpcClient.getContractVersion(schemas_ts_1.PINNED_INTERPRETER_TESTNET_ADDRESS);
397
+ }
398
+ const info = (0, get_interpreter_info_ts_1.getInterpreterInfo)({
399
+ pinnedAddress: schemas_ts_1.PINNED_INTERPRETER_TESTNET_ADDRESS,
400
+ pinnedGrammarVersion: schemas_ts_1.PINNED_INTERPRETER_GRAMMAR_VERSION,
401
+ pinnedWasmHash: schemas_ts_1.PINNED_INTERPRETER_WASM_SHA256,
402
+ network,
403
+ ...(deployedGrammarVersion !== undefined ? { deployedGrammarVersion } : {}),
404
+ });
405
+ return { ok: true, data: info };
406
+ }
407
+ catch (e) {
408
+ return {
409
+ ok: false,
410
+ error: caughtError('get_interpreter_info', 'RECORDING_FAILED', e),
411
+ };
412
+ }
413
+ }
414
+ /** Build an InstallRpcClient from an optional URL override, falling back
415
+ * to the public testnet RPC (matching the design-decision rule that we
416
+ * reuse `record/rpc.ts` and never hard-code a different RPC client). */
417
+ function buildRpcClientFromInput(urlOverride) {
418
+ if (urlOverride) {
419
+ return (0, build_install_policy_ts_1.rpcClientFromServer)(new stellar_sdk_1.rpc.Server(urlOverride, { allowHttp: false }), schemas_ts_1.NETWORK_PASSPHRASES.testnet);
420
+ }
421
+ // Default: public testnet RPC. Matches the brief; the mainnet pin would
422
+ // need a separate deploy and is out of scope for this tool.
423
+ //
424
+ // NOT `createRpcServer` - that returns an RpcFetcher, a bare
425
+ // `(hash) => Promise<SorobanTxResponse|null>` for the RECORDER. Passing it
426
+ // here produced a client whose `getAccount` was undefined, so every live
427
+ // call died with "server.getAccount is not a function". The install path
428
+ // needs the full Server surface.
429
+ return (0, build_install_policy_ts_1.rpcClientFromServer)(new stellar_sdk_1.rpc.Server(schemas_ts_1.TESTNET_RPC_URL, { allowHttp: false }), schemas_ts_1.NETWORK_PASSPHRASES.testnet);
430
+ }
431
+ /** Default-deny: refuse any interpreter policy whose address differs from
432
+ * the pinned testnet interpreter. An interpreter the caller controls
433
+ * can permit anything, so the smart account's authorization must bind
434
+ * to the pinned contract unless the caller explicitly opts in via
435
+ * `allowUnpinnedInterpreter`. OZ built-in policies are not interpreters
436
+ * and pass through unchanged. Returns a ToolError to surface through
437
+ * the run-layer envelope, or null when the policies are all pinned. */
438
+ function enforceInterpreterPin(policies, allowUnpinned) {
439
+ for (const p of policies) {
440
+ if (p.kind !== 'interpreter')
441
+ continue;
442
+ if (p.interpreterAddress === schemas_ts_1.PINNED_INTERPRETER_TESTNET_ADDRESS)
443
+ continue;
444
+ if (allowUnpinned === true)
445
+ continue;
446
+ return {
447
+ code: 'INSTALL_BUILD_FAILED',
448
+ message: `install_policy: interpreter policy address ${p.interpreterAddress} != pinned ${schemas_ts_1.PINNED_INTERPRETER_TESTNET_ADDRESS}; set allowUnpinnedInterpreter: true to opt in to a non-pinned interpreter`,
449
+ severity: 'error',
450
+ retryable: false,
451
+ remediation: { toolCall: { name: 'install_policy', args: {} } },
452
+ };
453
+ }
454
+ return null;
455
+ }
135
456
  /** Build a canonical ToolError for a Zod validation failure. The remediation
136
457
  * hint points the agent back at the right tool with an empty arg bag - the
137
458
  * tool name IS the machine-readable hint. */
138
459
  function validationError(toolName, issues) {
139
- const code = toolName === 'record_transaction' ? 'RECORDING_FAILED' : 'SYNTHESIS_ERROR';
460
+ const code = toolName === 'record_transaction'
461
+ ? 'RECORDING_FAILED'
462
+ : toolName === 'synthesize_policy'
463
+ ? 'SYNTHESIS_ERROR'
464
+ : toolName === 'simulate_policy'
465
+ ? 'SIMULATION_ERROR'
466
+ : toolName === 'verify_policy'
467
+ ? 'VERIFICATION_FAILED'
468
+ : toolName === 'install_policy'
469
+ ? 'INSTALL_BUILD_FAILED'
470
+ : toolName === 'revoke_policy'
471
+ ? 'REVOKE_BUILD_FAILED'
472
+ : 'RECORDING_FAILED';
140
473
  return {
141
474
  code,
142
475
  message: `${toolName}: invalid input: ${issues
@@ -217,8 +550,11 @@ function safeStringify(v) {
217
550
  return value.toString();
218
551
  if (typeof value === 'function')
219
552
  return `[function ${value.name || 'anonymous'}]`;
553
+ // `stack` is a server-controlled diagnostic. Stack traces from a host
554
+ // we do not own are reconnaissance, not a signal the caller can act on.
555
+ // Surface `name` + `message` only.
220
556
  if (value instanceof Error) {
221
- return { name: value.name, message: value.message, stack: value.stack };
557
+ return { name: value.name, message: value.message };
222
558
  }
223
559
  if (value !== null && typeof value === 'object') {
224
560
  if (seen.has(value))