@crediolabs/policy-synth 0.1.18 → 0.3.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.
Files changed (159) hide show
  1. package/README.md +6 -5
  2. package/dist/adapters/interpreter/adapter.d.ts +19 -25
  3. package/dist/adapters/interpreter/adapter.js +54 -487
  4. package/dist/adapters/interpreter/index.d.ts +1 -1
  5. package/dist/adapters/interpreter/index.js +1 -1
  6. package/dist/adapters/oz/adapter.js +1 -15
  7. package/dist/errors.d.ts +1 -1
  8. package/dist/index.d.ts +1 -4
  9. package/dist/index.js +1 -4
  10. package/dist/install/build-add-context-rule.d.ts +3 -11
  11. package/dist/install/build-add-context-rule.js +2 -97
  12. package/dist/install/build-install-policy.d.ts +4 -53
  13. package/dist/install/build-install-policy.js +44 -123
  14. package/dist/ir/index.d.ts +1 -1
  15. package/dist/ir/types.d.ts +7 -76
  16. package/dist/ir/types.js +0 -2
  17. package/dist/predicate/decode.d.ts +1 -1
  18. package/dist/predicate/decode.js +2 -70
  19. package/dist/predicate/encode.js +45 -229
  20. package/dist/predicate/from-json.js +1 -42
  21. package/dist/record/decode.js +3 -8
  22. package/dist/review-card/builder.d.ts +4 -3
  23. package/dist/review-card/builder.js +18 -162
  24. package/dist/review-card/cross-check.js +2 -105
  25. package/dist/review-card/render-leaf.d.ts +4 -0
  26. package/dist/review-card/render-leaf.js +47 -0
  27. package/dist/run/index.d.ts +35 -53
  28. package/dist/run/index.js +115 -156
  29. package/dist/run/schemas.d.ts +168 -1650
  30. package/dist/run/schemas.js +36 -199
  31. package/dist/seams/types.d.ts +0 -2
  32. package/dist/simulate/deny-cases.d.ts +26 -0
  33. package/dist/simulate/deny-cases.js +282 -0
  34. package/dist/simulate/evaluate.d.ts +18 -0
  35. package/dist/simulate/evaluate.js +271 -0
  36. package/dist/simulate/index.d.ts +4 -0
  37. package/dist/simulate/index.js +8 -0
  38. package/dist/synth/compose-from-recording.d.ts +24 -51
  39. package/dist/synth/compose-from-recording.js +120 -282
  40. package/dist/synth/deny-cases.d.ts +5 -7
  41. package/dist/synth/deny-cases.js +23 -373
  42. package/dist/synth/evaluate.d.ts +0 -21
  43. package/dist/synth/evaluate.js +56 -336
  44. package/dist/synth/index.d.ts +0 -5
  45. package/dist/synth/index.js +0 -5
  46. package/dist/synth/permit-context.d.ts +2 -9
  47. package/dist/synth/permit-context.js +1 -82
  48. package/dist/synth/synthesize-from-recording.d.ts +1 -29
  49. package/dist/synth/synthesize-from-recording.js +41 -356
  50. package/dist/types.d.ts +8 -60
  51. package/dist/types.js +5 -2
  52. package/dist/verify/simulate.d.ts +1 -14
  53. package/dist/verify/simulate.js +2 -88
  54. package/dist/verify/verify.d.ts +1 -7
  55. package/dist/verify/verify.js +0 -8
  56. package/dist-cjs/adapters/interpreter/adapter.d.ts +19 -25
  57. package/dist-cjs/adapters/interpreter/adapter.js +56 -489
  58. package/dist-cjs/adapters/interpreter/index.d.ts +1 -1
  59. package/dist-cjs/adapters/interpreter/index.js +1 -2
  60. package/dist-cjs/adapters/oz/adapter.js +1 -15
  61. package/dist-cjs/errors.d.ts +1 -1
  62. package/dist-cjs/index.d.ts +1 -4
  63. package/dist-cjs/index.js +1 -4
  64. package/dist-cjs/install/build-add-context-rule.d.ts +3 -11
  65. package/dist-cjs/install/build-add-context-rule.js +1 -96
  66. package/dist-cjs/install/build-install-policy.d.ts +4 -53
  67. package/dist-cjs/install/build-install-policy.js +44 -122
  68. package/dist-cjs/ir/index.d.ts +1 -1
  69. package/dist-cjs/ir/types.d.ts +7 -76
  70. package/dist-cjs/ir/types.js +0 -2
  71. package/dist-cjs/predicate/decode.d.ts +1 -1
  72. package/dist-cjs/predicate/decode.js +2 -70
  73. package/dist-cjs/predicate/encode.js +45 -229
  74. package/dist-cjs/predicate/from-json.js +1 -42
  75. package/dist-cjs/record/decode.js +3 -8
  76. package/dist-cjs/review-card/builder.d.ts +4 -3
  77. package/dist-cjs/review-card/builder.js +23 -167
  78. package/dist-cjs/review-card/cross-check.js +7 -110
  79. package/dist-cjs/review-card/render-leaf.d.ts +4 -0
  80. package/dist-cjs/review-card/render-leaf.js +52 -0
  81. package/dist-cjs/run/index.d.ts +35 -53
  82. package/dist-cjs/run/index.js +116 -162
  83. package/dist-cjs/run/schemas.d.ts +168 -1650
  84. package/dist-cjs/run/schemas.js +37 -200
  85. package/dist-cjs/seams/types.d.ts +0 -2
  86. package/dist-cjs/simulate/deny-cases.d.ts +26 -0
  87. package/dist-cjs/simulate/deny-cases.js +286 -0
  88. package/dist-cjs/simulate/evaluate.d.ts +18 -0
  89. package/dist-cjs/simulate/evaluate.js +274 -0
  90. package/dist-cjs/simulate/index.d.ts +4 -0
  91. package/dist-cjs/simulate/index.js +13 -0
  92. package/dist-cjs/synth/compose-from-recording.d.ts +24 -51
  93. package/dist-cjs/synth/compose-from-recording.js +120 -282
  94. package/dist-cjs/synth/deny-cases.d.ts +5 -7
  95. package/dist-cjs/synth/deny-cases.js +23 -374
  96. package/dist-cjs/synth/evaluate.d.ts +0 -21
  97. package/dist-cjs/synth/evaluate.js +57 -337
  98. package/dist-cjs/synth/index.d.ts +0 -5
  99. package/dist-cjs/synth/index.js +1 -11
  100. package/dist-cjs/synth/permit-context.d.ts +2 -9
  101. package/dist-cjs/synth/permit-context.js +1 -82
  102. package/dist-cjs/synth/synthesize-from-recording.d.ts +1 -29
  103. package/dist-cjs/synth/synthesize-from-recording.js +39 -354
  104. package/dist-cjs/types.d.ts +8 -60
  105. package/dist-cjs/types.js +6 -3
  106. package/dist-cjs/verify/simulate.d.ts +1 -14
  107. package/dist-cjs/verify/simulate.js +2 -88
  108. package/dist-cjs/verify/verify.d.ts +1 -7
  109. package/dist-cjs/verify/verify.js +0 -8
  110. package/package.json +4 -4
  111. package/src/adapters/interpreter/adapter.ts +76 -572
  112. package/src/errors.ts +0 -19
  113. package/src/index.ts +1 -4
  114. package/src/install/build-add-context-rule.ts +4 -127
  115. package/src/install/build-install-policy.ts +93 -214
  116. package/src/predicate/decode.ts +2 -70
  117. package/src/predicate/encode.ts +49 -261
  118. package/src/predicate/from-json.ts +1 -43
  119. package/src/record/decode.ts +3 -8
  120. package/src/review-card/builder.ts +19 -168
  121. package/src/review-card/cross-check.ts +3 -105
  122. package/src/review-card/render-leaf.ts +48 -0
  123. package/src/run/index.ts +130 -186
  124. package/src/run/schemas.ts +38 -214
  125. package/src/simulate/deny-cases.ts +334 -0
  126. package/src/simulate/evaluate.ts +284 -0
  127. package/src/simulate/index.ts +11 -0
  128. package/src/synth/compose-from-recording.ts +148 -347
  129. package/src/synth/index.ts +0 -13
  130. package/src/synth/synthesize-from-recording.ts +43 -456
  131. package/src/types.ts +13 -49
  132. package/dist/install/build-install-predicate.d.ts +0 -96
  133. package/dist/install/build-install-predicate.js +0 -444
  134. package/dist-cjs/install/build-install-predicate.d.ts +0 -96
  135. package/dist-cjs/install/build-install-predicate.js +0 -479
  136. package/src/adapters/interpreter/index.ts +0 -8
  137. package/src/adapters/oz/adapter.ts +0 -376
  138. package/src/adapters/oz/index.ts +0 -9
  139. package/src/codegen/compile-gate.ts +0 -167
  140. package/src/codegen/index.ts +0 -17
  141. package/src/codegen/template.ts +0 -165
  142. package/src/ir/index.ts +0 -13
  143. package/src/ir/types.ts +0 -132
  144. package/src/mandate/index.ts +0 -4
  145. package/src/mandate/to-ir.ts +0 -71
  146. package/src/mandate/types.ts +0 -21
  147. package/src/seams/index.ts +0 -11
  148. package/src/seams/types.ts +0 -81
  149. package/src/synth/deny-cases.ts +0 -663
  150. package/src/synth/evaluate.ts +0 -613
  151. package/src/synth/harness.ts +0 -68
  152. package/src/synth/minimize.ts +0 -48
  153. package/src/synth/permit-context.ts +0 -136
  154. package/src/synth/predicate-literals.ts +0 -27
  155. package/src/synth/synthesize-from-mandate.ts +0 -82
  156. package/src/verify/envelope.ts +0 -28
  157. package/src/verify/index.ts +0 -5
  158. package/src/verify/simulate.ts +0 -311
  159. package/src/verify/verify.ts +0 -243
package/dist/run/index.js CHANGED
@@ -18,16 +18,16 @@
18
18
  // drive the CLI (which calls into the same core directly without MCP).
19
19
  import { createHash } from 'node:crypto';
20
20
  import { rpc } from '@stellar/stellar-sdk';
21
- import { placeholderOzConfig, recordTransaction, synthesizeFromMandate, synthesizeFromRecording, } from "../index.js";
21
+ import { recordTransaction, synthesizeFromRecording, } from "../index.js";
22
22
  import { buildInstallPolicyXdr, buildRevokePolicyXdr, rpcClientFromServer, } from "../install/build-install-policy.js";
23
23
  import { getInterpreterInfo } from "../install/get-interpreter-info.js";
24
- import { simulatePolicy, verifyPolicy } from "../verify/index.js";
24
+ import { evaluate, generateCases } from "../simulate/index.js";
25
25
  import { GetInterpreterInfoInputSchema, InstallPolicyInputSchema, NETWORK_PASSPHRASES, PINNED_INTERPRETER_ADDRESS_BY_NETWORK, PINNED_INTERPRETER_GRAMMAR_VERSION, PINNED_INTERPRETER_WASM_SHA256, RecordTransactionInputSchema, RevokePolicyInputSchema, RPC_URL_BY_NETWORK, SimulatePolicyInputSchema, SynthesizePolicyInputSchema, VerifyPolicyInputSchema, } from "./schemas.js";
26
26
  // Re-export the underlying Zod schemas so the MCP package (and any other
27
27
  // downstream consumer) can import the canonical input shapes from the same
28
28
  // module that owns the tool-body glue. The strict schemas are the source of
29
29
  // truth - MCP tool shapes are derived from them.
30
- export { ComposeUserResponsesSchema, GetInterpreterInfoInputSchema, InstallPolicyInputSchema, InterpreterOptionsSchema, MAINNET_RPC_URL, MandateSpecSchema, NetworkSchema, OraclePriceFixtureSchema, OzAdapterConfigSchema, PINNED_INTERPRETER_ADDRESS_BY_NETWORK, PINNED_INTERPRETER_GRAMMAR_VERSION, PINNED_INTERPRETER_MAINNET_ADDRESS, PINNED_INTERPRETER_TESTNET_ADDRESS, PINNED_INTERPRETER_WASM_SHA256, PredicateLeafSchema, PredicateNodeSchema, RecordedTransactionSchema, RecordTransactionInputSchema, RevokePolicyInputSchema, RPC_URL_BY_NETWORK, SimulatePolicyInputSchema, SynthesizePolicyInputSchema, TESTNET_RPC_URL, ToolErrorSchema, VerifyPolicyInputSchema, } from "./schemas.js";
30
+ export { ComposeUserResponsesSchema, GetInterpreterInfoInputSchema, InstallPolicyInputSchema, InterpreterOptionsSchema, MAINNET_RPC_URL, NetworkSchema, PINNED_INTERPRETER_ADDRESS_BY_NETWORK, PINNED_INTERPRETER_GRAMMAR_VERSION, PINNED_INTERPRETER_MAINNET_ADDRESS, PINNED_INTERPRETER_TESTNET_ADDRESS, PINNED_INTERPRETER_WASM_SHA256, PredicateLeafSchema, PredicateNodeSchema, RecordedTransactionSchema, RecordTransactionInputSchema, RevokePolicyInputSchema, RPC_URL_BY_NETWORK, SynthesizePolicyInputSchema, TESTNET_RPC_URL, ToolErrorSchema, } from "./schemas.js";
31
31
  /** Map every tool name to its canonical domain error code. Replaces a 7-way
32
32
  * if/else so adding a tool adds one line here rather than a new branch in
33
33
  * each envelope call. */
@@ -54,10 +54,7 @@ const TOOL_ERROR_CODE = {
54
54
  export async function runRecordTransaction(raw) {
55
55
  const parsed = RecordTransactionInputSchema.safeParse(raw);
56
56
  if (!parsed.success) {
57
- return {
58
- ok: false,
59
- error: validationError('record_transaction', parsed.error.issues),
60
- };
57
+ return { ok: false, error: validationError('record_transaction', parsed.error.issues) };
61
58
  }
62
59
  const input = parsed.data;
63
60
  // Strip the wire-only `confidenceOverride` and pass the rest straight through.
@@ -73,14 +70,10 @@ export async function runRecordTransaction(raw) {
73
70
  return await recordTransaction(coreInput);
74
71
  }
75
72
  catch (e) {
76
- return {
77
- ok: false,
78
- error: caughtError('record_transaction', 'RECORDING_FAILED', e),
79
- };
73
+ return toolFailure('record_transaction', e);
80
74
  }
81
75
  }
82
76
  /** `synthesize_policy` body - discriminated union on `source`:
83
- * - `mandate` -> synthesizeFromMandate
84
77
  * - `recording` -> synthesizeFromRecording
85
78
  * Exposing BOTH front-ends through ONE tool keeps the MCP surface tiny while
86
79
  * letting the agent pick the deterministic or the inferred path. The CLI
@@ -94,21 +87,10 @@ export async function runRecordTransaction(raw) {
94
87
  export async function runSynthesizePolicy(raw) {
95
88
  const parsed = SynthesizePolicyInputSchema.safeParse(raw);
96
89
  if (!parsed.success) {
97
- return {
98
- ok: false,
99
- error: validationError('synthesize_policy', parsed.error.issues),
100
- };
90
+ return { ok: false, error: validationError('synthesize_policy', parsed.error.issues) };
101
91
  }
102
92
  const input = parsed.data;
103
- const ozConfig = resolveOzConfig(input);
104
93
  try {
105
- if (input.source === 'mandate') {
106
- // Zod's optional fields widen to `T | undefined`, which the core's
107
- // exact-optional MandateSpec rejects; the schema already validated the
108
- // shape, so assert it (same pattern as the recordedTx cast below).
109
- return await synthesizeFromMandate(input.mandate, ozConfig, input.explain === true ? { explain: true } : {});
110
- }
111
- // recording source
112
94
  const recorded = input.recordedTx;
113
95
  return await synthesizeFromRecording(recorded, {
114
96
  network: input.network,
@@ -118,112 +100,16 @@ export async function runSynthesizePolicy(raw) {
118
100
  : {}),
119
101
  ...(input.interpreter !== undefined ? { interpreter: input.interpreter } : {}),
120
102
  ...(input.explain === true ? { explain: true } : {}),
121
- }, ozConfig);
122
- }
123
- catch (e) {
124
- return {
125
- ok: false,
126
- error: caughtError('synthesize_policy', 'SYNTHESIS_ERROR', e),
127
- };
128
- }
129
- }
130
- function resolveOzConfig(input) {
131
- if (input.ozConfig)
132
- return input.ozConfig;
133
- // The mandate path is network-agnostic; fall back to mainnet so the
134
- // placeholder OZ instance addresses are deterministic.
135
- return placeholderOzConfig('mainnet');
136
- }
137
- /** `simulate_policy` body - thin wrapper over `simulatePolicy`. The engine
138
- * already returns fail-closed `{ok:false, error}` for runtime failures
139
- * (SIMULATION_ERROR), so the try/catch envelope is for raw SDK throws
140
- * only - same pattern as the other two wrappers. The predicate is
141
- * passed inline (stateless by design; no `proposed_policy_id` lookup). */
142
- export async function runSimulatePolicy(raw) {
143
- const parsed = SimulatePolicyInputSchema.safeParse(raw);
144
- if (!parsed.success) {
145
- return {
146
- ok: false,
147
- error: validationError('simulate_policy', parsed.error.issues),
148
- };
149
- }
150
- const input = parsed.data;
151
- try {
152
- // The recursive PredicateNodeSchema + ContractInvocationSchema are
153
- // typed `z.ZodType<unknown>` to survive TS's circular inference; the
154
- // engine wants typed `PredicateNode | null` + `RecordedTransaction`.
155
- // The schema already validated the shape, so assert through the
156
- // unknown back to the core types. Same pattern as the recordedTx
157
- // cast in `runSynthesizePolicy`.
158
- return simulatePolicy(input.predicate, input.permitTx, {
159
- ...(input.validUntilLedger !== undefined
160
- ? { validUntilLedger: input.validUntilLedger }
161
- : {}),
162
- ...(input.oraclePricesByAsset !== undefined
163
- ? { oraclePricesByAsset: input.oraclePricesByAsset }
164
- : {}),
165
103
  });
166
104
  }
167
105
  catch (e) {
168
- return {
169
- ok: false,
170
- error: caughtError('simulate_policy', 'SIMULATION_ERROR', e),
171
- };
106
+ return toolFailure('synthesize_policy', e);
172
107
  }
173
108
  }
174
- /** `verify_policy` body - thin wrapper over `verifyPolicy`. The engine
175
- * already returns `{ok:false, error}` with code VERIFICATION_FAILED when
176
- * the minimality check fails; the try/catch envelope is for raw SDK
177
- * throws only. Mirrors `runSimulatePolicy` exactly. */
178
- export async function runVerifyPolicy(raw) {
179
- const parsed = VerifyPolicyInputSchema.safeParse(raw);
180
- if (!parsed.success) {
181
- return {
182
- ok: false,
183
- error: validationError('verify_policy', parsed.error.issues),
184
- };
185
- }
186
- const input = parsed.data;
187
- try {
188
- // Same cast as runSimulatePolicy: the recursive schemas are typed
189
- // `unknown`; the engine wants typed `PredicateNode` +
190
- // `RecordedTransaction`. The schema already validated the shape.
191
- return verifyPolicy(input.predicate, input.permitTx, {
192
- ...(input.validUntilLedger !== undefined ? { validUntilLedger: input.validUntilLedger } : {}),
193
- ...(input.oraclePricesByAsset !== undefined
194
- ? { oraclePricesByAsset: input.oraclePricesByAsset }
195
- : {}),
196
- });
197
- }
198
- catch (e) {
199
- return {
200
- ok: false,
201
- error: caughtError('verify_policy', 'VERIFICATION_FAILED', e),
202
- };
203
- }
204
- }
205
- /** `install_policy` body - thin wrapper over `buildInstallPolicyXdr`.
206
- * Returns the unsigned Soroban transaction envelope (base64 XDR) the
207
- * wallet signs. The wallet signature IS the user-confirmation step - no
208
- * `action_id` two-call pair (the server is stateless, see server.ts:10-12).
209
- * One call installs the policy outright: `add_context_rule` carries the
210
- * predicate to the interpreter in its `policies` install_param, so no
211
- * separate `interpreter.install` call is needed or possible.
212
- *
213
- * Default-deny: an interpreter policy address other than the pinned
214
- * interpreter for the selected network is REFUSED (the smart account
215
- * would delegate to an interpreter the caller controls); the same
216
- * applies to a non-pinned RPC URL (the auth nonce the wallet signs
217
- * comes from the RPC). Both gates accept an explicit opt-in flag.
218
- * Pin selection follows `input.network` (defaults to `testnet` so the
219
- * pre-mainnet callers keep working unchanged). */
220
109
  export async function runInstallPolicy(raw) {
221
110
  const parsed = InstallPolicyInputSchema.safeParse(raw);
222
111
  if (!parsed.success) {
223
- return {
224
- ok: false,
225
- error: validationError('install_policy', parsed.error.issues),
226
- };
112
+ return { ok: false, error: validationError('install_policy', parsed.error.issues) };
227
113
  }
228
114
  const input = parsed.data;
229
115
  const network = input.network ?? 'testnet';
@@ -243,10 +129,7 @@ export async function runInstallPolicy(raw) {
243
129
  rpcClient = buildRpcClientFromInput(input.rpcUrl, network);
244
130
  }
245
131
  catch (e) {
246
- return {
247
- ok: false,
248
- error: caughtError('install_policy', 'INSTALL_BUILD_FAILED', e),
249
- };
132
+ return toolFailure('install_policy', e);
250
133
  }
251
134
  try {
252
135
  const interpreterPolicy = input.rule.policies.find((p) => p.kind === 'interpreter');
@@ -268,10 +151,7 @@ export async function runInstallPolicy(raw) {
268
151
  return { ok: true, data: result };
269
152
  }
270
153
  catch (e) {
271
- return {
272
- ok: false,
273
- error: caughtError('install_policy', 'INSTALL_BUILD_FAILED', e),
274
- };
154
+ return toolFailure('install_policy', e);
275
155
  }
276
156
  }
277
157
  /** `revoke_policy` body - thin wrapper over `buildRevokePolicyXdr`.
@@ -286,10 +166,7 @@ export async function runInstallPolicy(raw) {
286
166
  export async function runRevokePolicy(raw) {
287
167
  const parsed = RevokePolicyInputSchema.safeParse(raw);
288
168
  if (!parsed.success) {
289
- return {
290
- ok: false,
291
- error: validationError('revoke_policy', parsed.error.issues),
292
- };
169
+ return { ok: false, error: validationError('revoke_policy', parsed.error.issues) };
293
170
  }
294
171
  const input = parsed.data;
295
172
  const network = input.network ?? 'testnet';
@@ -303,10 +180,7 @@ export async function runRevokePolicy(raw) {
303
180
  rpcClient = buildRpcClientFromInput(input.rpcUrl, network);
304
181
  }
305
182
  catch (e) {
306
- return {
307
- ok: false,
308
- error: caughtError('revoke_policy', 'REVOKE_BUILD_FAILED', e),
309
- };
183
+ return toolFailure('revoke_policy', e);
310
184
  }
311
185
  try {
312
186
  const result = await buildRevokePolicyXdr({
@@ -320,10 +194,7 @@ export async function runRevokePolicy(raw) {
320
194
  return { ok: true, data: result };
321
195
  }
322
196
  catch (e) {
323
- return {
324
- ok: false,
325
- error: caughtError('revoke_policy', 'REVOKE_BUILD_FAILED', e),
326
- };
197
+ return toolFailure('revoke_policy', e);
327
198
  }
328
199
  }
329
200
  /** `get_interpreter_info` body - thin wrapper over `getInterpreterInfo`.
@@ -345,14 +216,105 @@ export async function runRevokePolicy(raw) {
345
216
  * a non-pinned `rpcUrl` would silently bind the caller to a host they
346
217
  * picked. The pin is enforced here too, with the same `allowUnpinnedRpcUrl`
347
218
  * opt-in as install/revoke. */
348
- export async function runGetInterpreterInfo(raw) {
349
- const parsed = GetInterpreterInfoInputSchema.safeParse(raw);
219
+ /** The call a predicate is evaluated against: the single top-level invocation
220
+ * the smart account authorises. `Policy::enforce` receives one `Context`, not
221
+ * a sub-invocation tree, so simulating anything deeper would claim a
222
+ * guarantee the contract does not make. */
223
+ function evalContextFromRecording(tx) {
224
+ const top = tx.invocations[0];
225
+ if (!top)
226
+ return null;
227
+ return { contract: top.contract, fn: top.fn, args: top.args };
228
+ }
229
+ /** Both `simulate_policy` and `verify_policy` evaluate against a context derived
230
+ * from `permitTx`; neither has anything to evaluate when the recording carries
231
+ * no top-level invocation. */
232
+ function noInvocationError(toolName) {
233
+ return {
234
+ code: TOOL_ERROR_CODE[toolName],
235
+ message: `${toolName}: permitTx carries no invocation to evaluate`,
236
+ severity: 'error',
237
+ retryable: false,
238
+ };
239
+ }
240
+ /** `simulate_policy` body - evaluate a predicate against one recorded call.
241
+ *
242
+ * The evaluator is a second implementation of the on-chain semantics, and the
243
+ * conformance harness asserts it agrees with the Rust interpreter case for
244
+ * case. A verdict here is therefore a claim about what the contract would do,
245
+ * not a guess. */
246
+ export function runSimulatePolicy(raw) {
247
+ const parsed = SimulatePolicyInputSchema.safeParse(raw);
248
+ if (!parsed.success) {
249
+ return { ok: false, error: validationError('simulate_policy', parsed.error.issues) };
250
+ }
251
+ const input = parsed.data;
252
+ const ctx = evalContextFromRecording(input.permitTx);
253
+ if (!ctx)
254
+ return { ok: false, error: noInvocationError('simulate_policy') };
255
+ try {
256
+ const res = evaluate(input.predicate, ctx);
257
+ return {
258
+ ok: true,
259
+ data: {
260
+ permitted: res.permit,
261
+ reason: res.permit ? null : res.reason,
262
+ call: { contract: ctx.contract, fn: ctx.fn, argCount: ctx.args.length },
263
+ },
264
+ };
265
+ }
266
+ catch (e) {
267
+ return toolFailure('simulate_policy', e);
268
+ }
269
+ }
270
+ /** `verify_policy` body - the permit case plus a generated deny case per
271
+ * dimension.
272
+ *
273
+ * Two failure modes are being checked, and they are not the same thing. A
274
+ * permit case that denies means the policy is too STRICT: it would refuse the
275
+ * very transaction it was synthesised from. A deny case that permits means it
276
+ * is too LOOSE: some mutation of that transaction still gets through. `ok` is
277
+ * true only when neither holds. */
278
+ export function runVerifyPolicy(raw) {
279
+ const parsed = VerifyPolicyInputSchema.safeParse(raw);
350
280
  if (!parsed.success) {
281
+ return { ok: false, error: validationError('verify_policy', parsed.error.issues) };
282
+ }
283
+ const input = parsed.data;
284
+ const ctx = evalContextFromRecording(input.permitTx);
285
+ if (!ctx)
286
+ return { ok: false, error: noInvocationError('verify_policy') };
287
+ try {
288
+ const predicate = input.predicate;
289
+ const cases = generateCases(predicate, ctx);
290
+ const permitRes = evaluate(predicate, cases.permit);
291
+ const denies = cases.denies.map((d) => {
292
+ const r = evaluate(predicate, d.ctx);
293
+ return {
294
+ dimension: d.dimension,
295
+ denied: !r.permit,
296
+ reason: r.permit ? null : r.reason,
297
+ };
298
+ });
351
299
  return {
352
- ok: false,
353
- error: validationError('get_interpreter_info', parsed.error.issues),
300
+ ok: true,
301
+ data: {
302
+ ok: permitRes.permit && denies.every((d) => d.denied),
303
+ permit: { permitted: permitRes.permit, reason: permitRes.permit ? null : permitRes.reason },
304
+ denies,
305
+ dimensionsCovered: denies.length,
306
+ },
354
307
  };
355
308
  }
309
+ catch (e) {
310
+ return toolFailure('verify_policy', e);
311
+ }
312
+ }
313
+ export async function runGetInterpreterInfo(raw) {
314
+ const parsed = GetInterpreterInfoInputSchema.safeParse(raw);
315
+ if (!parsed.success) {
316
+ return { ok: false, error: validationError('get_interpreter_info', parsed.error.issues) };
317
+ }
356
318
  const input = parsed.data;
357
319
  const network = input.network ?? 'testnet';
358
320
  const expectedRpc = RPC_URL_BY_NETWORK[network];
@@ -385,10 +347,7 @@ export async function runGetInterpreterInfo(raw) {
385
347
  return { ok: true, data: info };
386
348
  }
387
349
  catch (e) {
388
- return {
389
- ok: false,
390
- error: caughtError('get_interpreter_info', 'RECORDING_FAILED', e),
391
- };
350
+ return toolFailure('get_interpreter_info', e);
392
351
  }
393
352
  }
394
353
  /** Build an InstallRpcClient from an optional URL override + selected
@@ -442,13 +401,8 @@ function enforceInterpreterPin(policies, allowUnpinned, expectedInterpreterAddre
442
401
  function enforceRpcPin(toolName, rpcUrl, allowUnpinned, expectedRpc, network) {
443
402
  if (!rpcUrl || rpcUrl === expectedRpc || allowUnpinned === true)
444
403
  return null;
445
- const code = toolName === 'install_policy'
446
- ? 'INSTALL_BUILD_FAILED'
447
- : toolName === 'revoke_policy'
448
- ? 'REVOKE_BUILD_FAILED'
449
- : 'RECORDING_FAILED';
450
404
  return {
451
- code,
405
+ code: TOOL_ERROR_CODE[toolName],
452
406
  message: `${toolName}: rpcUrl must equal the pinned ${expectedRpc} (${network}); set allowUnpinnedRpcUrl: true to opt in to a custom endpoint`,
453
407
  severity: 'error',
454
408
  retryable: false,
@@ -475,6 +429,11 @@ function validationError(toolName, issues) {
475
429
  * error in `details` for the agent to inspect. Exported as a test-only seam
476
430
  * so the suite in run/index.test.ts can drive the envelope path without
477
431
  * standing up a full recordTransaction pipeline. */
432
+ /** The fail-closed return every tool body uses in its `catch`. Taking only the
433
+ * tool name keeps the tool -> error-code mapping in TOOL_ERROR_CODE alone. */
434
+ function toolFailure(toolName, e) {
435
+ return { ok: false, error: caughtError(toolName, TOOL_ERROR_CODE[toolName], e) };
436
+ }
478
437
  export function caughtError(toolName, code, e) {
479
438
  return {
480
439
  code,