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