@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/src/run/index.ts CHANGED
@@ -21,16 +21,12 @@ import { createHash } from 'node:crypto'
21
21
  import { rpc } from '@stellar/stellar-sdk'
22
22
  import {
23
23
  type ErrorCode,
24
- type MandateSpec,
25
24
  type Network,
26
- type OzAdapterConfig,
27
25
  type PredicateNode,
28
26
  type ProposedPolicy,
29
- placeholderOzConfig,
30
27
  type RecordedTransaction,
31
28
  recordTransaction,
32
29
  type SynthesizeFromRecordingOptions,
33
- synthesizeFromMandate,
34
30
  synthesizeFromRecording,
35
31
  type ToolError,
36
32
  type ToolResponse,
@@ -44,8 +40,7 @@ import {
44
40
  rpcClientFromServer,
45
41
  } from '../install/build-install-policy.ts'
46
42
  import { getInterpreterInfo } from '../install/get-interpreter-info.ts'
47
- import type { SimulationResult } from '../verify/envelope.ts'
48
- import { simulatePolicy, verifyPolicy } from '../verify/index.ts'
43
+ import { type EvalContext, evaluate, generateCases } from '../simulate/index.ts'
49
44
  import {
50
45
  type GetInterpreterInfoInput,
51
46
  GetInterpreterInfoInputSchema,
@@ -87,10 +82,7 @@ export {
87
82
  InstallPolicyInputSchema,
88
83
  InterpreterOptionsSchema,
89
84
  MAINNET_RPC_URL,
90
- MandateSpecSchema,
91
85
  NetworkSchema,
92
- OraclePriceFixtureSchema,
93
- OzAdapterConfigSchema,
94
86
  PINNED_INTERPRETER_ADDRESS_BY_NETWORK,
95
87
  PINNED_INTERPRETER_GRAMMAR_VERSION,
96
88
  PINNED_INTERPRETER_MAINNET_ADDRESS,
@@ -102,11 +94,9 @@ export {
102
94
  RecordTransactionInputSchema,
103
95
  RevokePolicyInputSchema,
104
96
  RPC_URL_BY_NETWORK,
105
- SimulatePolicyInputSchema,
106
97
  SynthesizePolicyInputSchema,
107
98
  TESTNET_RPC_URL,
108
99
  ToolErrorSchema,
109
- VerifyPolicyInputSchema,
110
100
  } from './schemas.ts'
111
101
 
112
102
  export type RunRecordTransactionInput = RecordTransactionInput
@@ -152,10 +142,7 @@ export async function runRecordTransaction(
152
142
  ): Promise<ToolResponse<RecordedTransaction>> {
153
143
  const parsed = RecordTransactionInputSchema.safeParse(raw)
154
144
  if (!parsed.success) {
155
- return {
156
- ok: false,
157
- error: validationError('record_transaction', parsed.error.issues),
158
- }
145
+ return { ok: false, error: validationError('record_transaction', parsed.error.issues) }
159
146
  }
160
147
  const input: RecordTransactionInput = parsed.data
161
148
  // Strip the wire-only `confidenceOverride` and pass the rest straight through.
@@ -170,15 +157,11 @@ export async function runRecordTransaction(
170
157
  try {
171
158
  return await recordTransaction(coreInput)
172
159
  } catch (e) {
173
- return {
174
- ok: false,
175
- error: caughtError('record_transaction', 'RECORDING_FAILED', e),
176
- }
160
+ return toolFailure('record_transaction', e)
177
161
  }
178
162
  }
179
163
 
180
164
  /** `synthesize_policy` body - discriminated union on `source`:
181
- * - `mandate` -> synthesizeFromMandate
182
165
  * - `recording` -> synthesizeFromRecording
183
166
  * Exposing BOTH front-ends through ONE tool keeps the MCP surface tiny while
184
167
  * letting the agent pick the deterministic or the inferred path. The CLI
@@ -193,157 +176,37 @@ export async function runSynthesizePolicy(raw: unknown): Promise<
193
176
  ToolResponse<ProposedPolicy> & {
194
177
  explain?: {
195
178
  predicateTree: PredicateNode | null
196
- simulation: SimulationResult
197
179
  }
198
180
  }
199
181
  > {
200
182
  const parsed = SynthesizePolicyInputSchema.safeParse(raw)
201
183
  if (!parsed.success) {
202
- return {
203
- ok: false,
204
- error: validationError('synthesize_policy', parsed.error.issues),
205
- }
184
+ return { ok: false, error: validationError('synthesize_policy', parsed.error.issues) }
206
185
  }
207
186
  const input = parsed.data
208
- const ozConfig: OzAdapterConfig = resolveOzConfig(input)
209
187
 
210
188
  try {
211
- if (input.source === 'mandate') {
212
- // Zod's optional fields widen to `T | undefined`, which the core's
213
- // exact-optional MandateSpec rejects; the schema already validated the
214
- // shape, so assert it (same pattern as the recordedTx cast below).
215
- return await synthesizeFromMandate(
216
- input.mandate as MandateSpec,
217
- ozConfig,
218
- input.explain === true ? { explain: true } : {}
219
- )
220
- }
221
- // recording source
222
189
  const recorded: RecordedTransaction = input.recordedTx as RecordedTransaction
223
- return await synthesizeFromRecording(
224
- recorded,
225
- {
226
- network: input.network,
227
- ...(input.userResponses !== undefined ? { userResponses: input.userResponses } : {}),
228
- ...(input.confidenceOverride !== undefined
229
- ? { confidenceOverride: input.confidenceOverride }
230
- : {}),
231
- ...(input.interpreter !== undefined ? { interpreter: input.interpreter } : {}),
232
- ...(input.explain === true ? { explain: true } : {}),
233
- } as SynthesizeFromRecordingOptions,
234
- ozConfig
235
- )
236
- } catch (e) {
237
- return {
238
- ok: false,
239
- error: caughtError('synthesize_policy', 'SYNTHESIS_ERROR', e),
240
- }
241
- }
242
- }
243
-
244
- function resolveOzConfig(input: SynthesizePolicyInput): OzAdapterConfig {
245
- if (input.ozConfig) return input.ozConfig
246
- // The mandate path is network-agnostic; fall back to mainnet so the
247
- // placeholder OZ instance addresses are deterministic.
248
- return placeholderOzConfig('mainnet')
249
- }
250
-
251
- /** `simulate_policy` body - thin wrapper over `simulatePolicy`. The engine
252
- * already returns fail-closed `{ok:false, error}` for runtime failures
253
- * (SIMULATION_ERROR), so the try/catch envelope is for raw SDK throws
254
- * only - same pattern as the other two wrappers. The predicate is
255
- * passed inline (stateless by design; no `proposed_policy_id` lookup). */
256
- export async function runSimulatePolicy(raw: unknown): Promise<ToolResponse<SimulationResult>> {
257
- const parsed = SimulatePolicyInputSchema.safeParse(raw)
258
- if (!parsed.success) {
259
- return {
260
- ok: false,
261
- error: validationError('simulate_policy', parsed.error.issues),
262
- }
263
- }
264
- const input: SimulatePolicyInput = parsed.data
265
- try {
266
- // The recursive PredicateNodeSchema + ContractInvocationSchema are
267
- // typed `z.ZodType<unknown>` to survive TS's circular inference; the
268
- // engine wants typed `PredicateNode | null` + `RecordedTransaction`.
269
- // The schema already validated the shape, so assert through the
270
- // unknown back to the core types. Same pattern as the recordedTx
271
- // cast in `runSynthesizePolicy`.
272
- return simulatePolicy(
273
- input.predicate as PredicateNode | null,
274
- input.permitTx as RecordedTransaction,
275
- {
276
- ...(input.validUntilLedger !== undefined
277
- ? { validUntilLedger: input.validUntilLedger }
278
- : {}),
279
- ...(input.oraclePricesByAsset !== undefined
280
- ? { oraclePricesByAsset: input.oraclePricesByAsset }
281
- : {}),
282
- }
283
- )
284
- } catch (e) {
285
- return {
286
- ok: false,
287
- error: caughtError('simulate_policy', 'SIMULATION_ERROR', e),
288
- }
289
- }
290
- }
291
-
292
- /** `verify_policy` body - thin wrapper over `verifyPolicy`. The engine
293
- * already returns `{ok:false, error}` with code VERIFICATION_FAILED when
294
- * the minimality check fails; the try/catch envelope is for raw SDK
295
- * throws only. Mirrors `runSimulatePolicy` exactly. */
296
- export async function runVerifyPolicy(raw: unknown): Promise<ToolResponse<true>> {
297
- const parsed = VerifyPolicyInputSchema.safeParse(raw)
298
- if (!parsed.success) {
299
- return {
300
- ok: false,
301
- error: validationError('verify_policy', parsed.error.issues),
302
- }
303
- }
304
- const input: VerifyPolicyInput = parsed.data
305
- try {
306
- // Same cast as runSimulatePolicy: the recursive schemas are typed
307
- // `unknown`; the engine wants typed `PredicateNode` +
308
- // `RecordedTransaction`. The schema already validated the shape.
309
- return verifyPolicy(input.predicate as PredicateNode, input.permitTx as RecordedTransaction, {
310
- ...(input.validUntilLedger !== undefined ? { validUntilLedger: input.validUntilLedger } : {}),
311
- ...(input.oraclePricesByAsset !== undefined
312
- ? { oraclePricesByAsset: input.oraclePricesByAsset }
190
+ return await synthesizeFromRecording(recorded, {
191
+ network: input.network,
192
+ ...(input.userResponses !== undefined ? { userResponses: input.userResponses } : {}),
193
+ ...(input.confidenceOverride !== undefined
194
+ ? { confidenceOverride: input.confidenceOverride }
313
195
  : {}),
314
- })
196
+ ...(input.interpreter !== undefined ? { interpreter: input.interpreter } : {}),
197
+ ...(input.explain === true ? { explain: true } : {}),
198
+ } as SynthesizeFromRecordingOptions)
315
199
  } catch (e) {
316
- return {
317
- ok: false,
318
- error: caughtError('verify_policy', 'VERIFICATION_FAILED', e),
319
- }
200
+ return toolFailure('synthesize_policy', e)
320
201
  }
321
202
  }
322
203
 
323
- /** `install_policy` body - thin wrapper over `buildInstallPolicyXdr`.
324
- * Returns the unsigned Soroban transaction envelope (base64 XDR) the
325
- * wallet signs. The wallet signature IS the user-confirmation step - no
326
- * `action_id` two-call pair (the server is stateless, see server.ts:10-12).
327
- * One call installs the policy outright: `add_context_rule` carries the
328
- * predicate to the interpreter in its `policies` install_param, so no
329
- * separate `interpreter.install` call is needed or possible.
330
- *
331
- * Default-deny: an interpreter policy address other than the pinned
332
- * interpreter for the selected network is REFUSED (the smart account
333
- * would delegate to an interpreter the caller controls); the same
334
- * applies to a non-pinned RPC URL (the auth nonce the wallet signs
335
- * comes from the RPC). Both gates accept an explicit opt-in flag.
336
- * Pin selection follows `input.network` (defaults to `testnet` so the
337
- * pre-mainnet callers keep working unchanged). */
338
204
  export async function runInstallPolicy(
339
205
  raw: unknown
340
206
  ): Promise<ToolResponse<BuildInstallPolicyResult>> {
341
207
  const parsed = InstallPolicyInputSchema.safeParse(raw)
342
208
  if (!parsed.success) {
343
- return {
344
- ok: false,
345
- error: validationError('install_policy', parsed.error.issues),
346
- }
209
+ return { ok: false, error: validationError('install_policy', parsed.error.issues) }
347
210
  }
348
211
  const input: InstallPolicyInput = parsed.data
349
212
  const network: Network = input.network ?? 'testnet'
@@ -372,10 +235,7 @@ export async function runInstallPolicy(
372
235
  try {
373
236
  rpcClient = buildRpcClientFromInput(input.rpcUrl, network)
374
237
  } catch (e) {
375
- return {
376
- ok: false,
377
- error: caughtError('install_policy', 'INSTALL_BUILD_FAILED', e),
378
- }
238
+ return toolFailure('install_policy', e)
379
239
  }
380
240
  try {
381
241
  const interpreterPolicy = input.rule.policies.find((p) => p.kind === 'interpreter')
@@ -396,10 +256,7 @@ export async function runInstallPolicy(
396
256
  })
397
257
  return { ok: true, data: result }
398
258
  } catch (e) {
399
- return {
400
- ok: false,
401
- error: caughtError('install_policy', 'INSTALL_BUILD_FAILED', e),
402
- }
259
+ return toolFailure('install_policy', e)
403
260
  }
404
261
  }
405
262
 
@@ -417,10 +274,7 @@ export async function runRevokePolicy(
417
274
  ): Promise<ToolResponse<BuildRevokePolicyResult>> {
418
275
  const parsed = RevokePolicyInputSchema.safeParse(raw)
419
276
  if (!parsed.success) {
420
- return {
421
- ok: false,
422
- error: validationError('revoke_policy', parsed.error.issues),
423
- }
277
+ return { ok: false, error: validationError('revoke_policy', parsed.error.issues) }
424
278
  }
425
279
  const input: RevokePolicyInput = parsed.data
426
280
  const network: Network = input.network ?? 'testnet'
@@ -439,10 +293,7 @@ export async function runRevokePolicy(
439
293
  try {
440
294
  rpcClient = buildRpcClientFromInput(input.rpcUrl, network)
441
295
  } catch (e) {
442
- return {
443
- ok: false,
444
- error: caughtError('revoke_policy', 'REVOKE_BUILD_FAILED', e),
445
- }
296
+ return toolFailure('revoke_policy', e)
446
297
  }
447
298
  try {
448
299
  const result = await buildRevokePolicyXdr({
@@ -455,10 +306,7 @@ export async function runRevokePolicy(
455
306
  })
456
307
  return { ok: true, data: result }
457
308
  } catch (e) {
458
- return {
459
- ok: false,
460
- error: caughtError('revoke_policy', 'REVOKE_BUILD_FAILED', e),
461
- }
309
+ return toolFailure('revoke_policy', e)
462
310
  }
463
311
  }
464
312
 
@@ -481,15 +329,114 @@ export async function runRevokePolicy(
481
329
  * a non-pinned `rpcUrl` would silently bind the caller to a host they
482
330
  * picked. The pin is enforced here too, with the same `allowUnpinnedRpcUrl`
483
331
  * opt-in as install/revoke. */
332
+ /** The call a predicate is evaluated against: the single top-level invocation
333
+ * the smart account authorises. `Policy::enforce` receives one `Context`, not
334
+ * a sub-invocation tree, so simulating anything deeper would claim a
335
+ * guarantee the contract does not make. */
336
+ function evalContextFromRecording(tx: RecordedTransaction): EvalContext | null {
337
+ const top = tx.invocations[0]
338
+ if (!top) return null
339
+ return { contract: top.contract, fn: top.fn, args: top.args }
340
+ }
341
+
342
+ /** Both `simulate_policy` and `verify_policy` evaluate against a context derived
343
+ * from `permitTx`; neither has anything to evaluate when the recording carries
344
+ * no top-level invocation. */
345
+ function noInvocationError(toolName: 'simulate_policy' | 'verify_policy'): ToolError {
346
+ return {
347
+ code: TOOL_ERROR_CODE[toolName],
348
+ message: `${toolName}: permitTx carries no invocation to evaluate`,
349
+ severity: 'error',
350
+ retryable: false,
351
+ }
352
+ }
353
+
354
+ /** `simulate_policy` body - evaluate a predicate against one recorded call.
355
+ *
356
+ * The evaluator is a second implementation of the on-chain semantics, and the
357
+ * conformance harness asserts it agrees with the Rust interpreter case for
358
+ * case. A verdict here is therefore a claim about what the contract would do,
359
+ * not a guess. */
360
+ export function runSimulatePolicy(raw: unknown): ToolResponse<{
361
+ permitted: boolean
362
+ reason: string | null
363
+ call: { contract: string; fn: string; argCount: number }
364
+ }> {
365
+ const parsed = SimulatePolicyInputSchema.safeParse(raw)
366
+ if (!parsed.success) {
367
+ return { ok: false, error: validationError('simulate_policy', parsed.error.issues) }
368
+ }
369
+ const input: SimulatePolicyInput = parsed.data
370
+ const ctx = evalContextFromRecording(input.permitTx as RecordedTransaction)
371
+ if (!ctx) return { ok: false, error: noInvocationError('simulate_policy') }
372
+ try {
373
+ const res = evaluate(input.predicate as PredicateNode, ctx)
374
+ return {
375
+ ok: true,
376
+ data: {
377
+ permitted: res.permit,
378
+ reason: res.permit ? null : res.reason,
379
+ call: { contract: ctx.contract, fn: ctx.fn, argCount: ctx.args.length },
380
+ },
381
+ }
382
+ } catch (e) {
383
+ return toolFailure('simulate_policy', e)
384
+ }
385
+ }
386
+
387
+ /** `verify_policy` body - the permit case plus a generated deny case per
388
+ * dimension.
389
+ *
390
+ * Two failure modes are being checked, and they are not the same thing. A
391
+ * permit case that denies means the policy is too STRICT: it would refuse the
392
+ * very transaction it was synthesised from. A deny case that permits means it
393
+ * is too LOOSE: some mutation of that transaction still gets through. `ok` is
394
+ * true only when neither holds. */
395
+ export function runVerifyPolicy(raw: unknown): ToolResponse<{
396
+ ok: boolean
397
+ permit: { permitted: boolean; reason: string | null }
398
+ denies: Array<{ dimension: string; denied: boolean; reason: string | null }>
399
+ dimensionsCovered: number
400
+ }> {
401
+ const parsed = VerifyPolicyInputSchema.safeParse(raw)
402
+ if (!parsed.success) {
403
+ return { ok: false, error: validationError('verify_policy', parsed.error.issues) }
404
+ }
405
+ const input: VerifyPolicyInput = parsed.data
406
+ const ctx = evalContextFromRecording(input.permitTx as RecordedTransaction)
407
+ if (!ctx) return { ok: false, error: noInvocationError('verify_policy') }
408
+ try {
409
+ const predicate = input.predicate as PredicateNode
410
+ const cases = generateCases(predicate, ctx)
411
+ const permitRes = evaluate(predicate, cases.permit)
412
+ const denies = cases.denies.map((d) => {
413
+ const r = evaluate(predicate, d.ctx)
414
+ return {
415
+ dimension: d.dimension,
416
+ denied: !r.permit,
417
+ reason: r.permit ? null : r.reason,
418
+ }
419
+ })
420
+ return {
421
+ ok: true,
422
+ data: {
423
+ ok: permitRes.permit && denies.every((d) => d.denied),
424
+ permit: { permitted: permitRes.permit, reason: permitRes.permit ? null : permitRes.reason },
425
+ denies,
426
+ dimensionsCovered: denies.length,
427
+ },
428
+ }
429
+ } catch (e) {
430
+ return toolFailure('verify_policy', e)
431
+ }
432
+ }
433
+
484
434
  export async function runGetInterpreterInfo(
485
435
  raw: unknown
486
436
  ): Promise<ToolResponse<ReturnType<typeof getInterpreterInfo>>> {
487
437
  const parsed = GetInterpreterInfoInputSchema.safeParse(raw)
488
438
  if (!parsed.success) {
489
- return {
490
- ok: false,
491
- error: validationError('get_interpreter_info', parsed.error.issues),
492
- }
439
+ return { ok: false, error: validationError('get_interpreter_info', parsed.error.issues) }
493
440
  }
494
441
  const input: GetInterpreterInfoInput = parsed.data
495
442
  const network: Network = input.network ?? 'testnet'
@@ -528,10 +475,7 @@ export async function runGetInterpreterInfo(
528
475
  })
529
476
  return { ok: true, data: info }
530
477
  } catch (e) {
531
- return {
532
- ok: false,
533
- error: caughtError('get_interpreter_info', 'RECORDING_FAILED', e),
534
- }
478
+ return toolFailure('get_interpreter_info', e)
535
479
  }
536
480
  }
537
481
 
@@ -597,14 +541,8 @@ function enforceRpcPin(
597
541
  network: Network
598
542
  ): ToolError | null {
599
543
  if (!rpcUrl || rpcUrl === expectedRpc || allowUnpinned === true) return null
600
- const code: ErrorCode =
601
- toolName === 'install_policy'
602
- ? 'INSTALL_BUILD_FAILED'
603
- : toolName === 'revoke_policy'
604
- ? 'REVOKE_BUILD_FAILED'
605
- : 'RECORDING_FAILED'
606
544
  return {
607
- code,
545
+ code: TOOL_ERROR_CODE[toolName],
608
546
  message: `${toolName}: rpcUrl must equal the pinned ${expectedRpc} (${network}); set allowUnpinnedRpcUrl: true to opt in to a custom endpoint`,
609
547
  severity: 'error',
610
548
  retryable: false,
@@ -636,6 +574,12 @@ function validationError(
636
574
  * error in `details` for the agent to inspect. Exported as a test-only seam
637
575
  * so the suite in run/index.test.ts can drive the envelope path without
638
576
  * standing up a full recordTransaction pipeline. */
577
+ /** The fail-closed return every tool body uses in its `catch`. Taking only the
578
+ * tool name keeps the tool -> error-code mapping in TOOL_ERROR_CODE alone. */
579
+ function toolFailure(toolName: RunToolName, e: unknown): { ok: false; error: ToolError } {
580
+ return { ok: false, error: caughtError(toolName, TOOL_ERROR_CODE[toolName], e) }
581
+ }
582
+
639
583
  export function caughtError(toolName: RunToolName, code: ErrorCode, e: unknown): ToolError {
640
584
  return {
641
585
  code,