@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
@@ -8,8 +8,8 @@
8
8
  //
9
9
  // i128 amounts and other large integers are carried as base-10 decimal strings
10
10
  // end-to-end. Networks are pinned to the same closed set the core defines.
11
- // The discriminated union on `source` exposes BOTH synthesize_policy
12
- // front-ends through a single tool input.
11
+ // `source` is a single-variant discriminator (`recording`); it stays a union
12
+ // so an unknown value is rejected with a discriminator error.
13
13
  //
14
14
  // This module is the SINGLE source of truth for these shapes. The MCP package
15
15
  // imports them here so its tool-shape bindings stay in step; the CLI imports
@@ -113,62 +113,13 @@ export const RecordedTransactionSchema = z
113
113
  })
114
114
  .passthrough()
115
115
 
116
- /** MandateSpec mirrors the core MandateSpec. The deterministic Mandate
117
- * front-end needs no parseConfidence; the tool adapter injects the full
118
- * confidence after synthesis so the orchestrator can compare. */
119
- export const MandateSpecSchema = z
120
- .object({
121
- chain: z.literal('stellar'),
122
- contract: z.string(),
123
- method: z.string().optional(),
124
- spendingLimit: z
125
- .object({
126
- token: z.string(),
127
- limit: z.string().regex(/^[0-9]+$/),
128
- windowSeconds: z.number().int().positive(),
129
- })
130
- .optional(),
131
- // A threshold of 0 means "0 approvals", which is not a real M-of-N gate.
132
- approvalThreshold: z.number().int().positive().optional(),
133
- recipients: z.array(z.string()).optional(),
134
- expiry: z
135
- .object({
136
- validUntilLedger: z.number().int().positive().max(U32_MAX).optional(),
137
- validUntilUnixSeconds: z.number().int().positive().optional(),
138
- })
139
- .optional(),
140
- })
141
- .passthrough()
142
- // TS-F4/F6: a `recipients` allowlist is only meaningful against a SEP-41
143
- // method whose arg 1 IS the recipient (SAC/SEP-41 `transfer(from, to,
144
- // amount)` and SEP-41 `mint(to, amount)`). Without this gate the
145
- // `to-ir.ts` lowering pins the allowlist to `RECIPIENT_ARG_INDEX = 1`
146
- // for any contract+method, which would let a non-SEP-41 method's
147
- // arg[1] (e.g. an amount, an op type, an arbitrary address payload)
148
- // be silently constrained as if it were a recipient. Refusing
149
- // non-SEP-41 methods at the boundary is the fail-closed shape.
150
- .refine(
151
- (v) =>
152
- v.recipients === undefined ||
153
- v.recipients.length === 0 ||
154
- v.method === 'transfer' ||
155
- v.method === 'mint',
156
- {
157
- message:
158
- 'recipients is only valid when method is a SEP-41 method (transfer or mint); other methods do not have a recipient at arg[1]',
159
- }
160
- )
161
-
162
- /** ComposeUserResponses mirrors the core. */
163
116
  export const ComposeUserResponsesSchema = z
164
117
  .object({
165
- windowSeconds: z.number().int().positive().optional(),
166
118
  validUntilLedger: z.number().int().positive().max(U32_MAX).optional(),
167
119
  limitAmount: z
168
120
  .string()
169
121
  .regex(/^[0-9]+$/)
170
122
  .optional(),
171
- invocationLimit: z.number().int().positive().optional(),
172
123
  // Swap recipient allowlist (SoroSwap call_arg[3]). Each entry must be a
173
124
  // Stellar address (G... wallet or C... contract); supplying it REPLACES the
174
125
  // default pin to the recorded recipient. Validated with the shared StrKey
@@ -177,19 +128,10 @@ export const ComposeUserResponsesSchema = z
177
128
  .array(z.string().refine(isStellarAddress, 'must be a Stellar address (G... or C...)'))
178
129
  .optional(),
179
130
  })
180
- .passthrough()
181
-
182
- /** OzAdapterConfig - the per-network OZ built-in instance addresses. */
183
- export const OzAdapterConfigSchema = z.object({
184
- network: NetworkSchema,
185
- instances: z.object({
186
- spending_limit: z.string(),
187
- simple_threshold: z.string(),
188
- weighted_threshold: z.string(),
189
- }),
190
- })
191
-
192
- // ===== record_transaction =====
131
+ // Strict, not passthrough: a response key the synthesiser no longer reads
132
+ // (or a typo) would otherwise be accepted in silence, and the caller would
133
+ // believe they had answered a question that is still open.
134
+ .strict()
193
135
 
194
136
  export const RecordTransactionInputSchema = z
195
137
  .object({
@@ -206,48 +148,18 @@ export const RecordTransactionInputSchema = z
206
148
  })
207
149
  export type RecordTransactionInput = z.infer<typeof RecordTransactionInputSchema>
208
150
 
209
- // ===== synthesize_policy =====
210
- //
211
- // Discriminated union on `source` exposes BOTH front-ends through ONE tool.
212
- // - `source: 'mandate'` -> calls synthesizeFromMandate
213
- // - `source: 'recording'` -> calls synthesizeFromRecording
214
-
215
- export const SynthesizePolicyMandateInputSchema = z.object({
216
- source: z.literal('mandate'),
217
- mandate: MandateSpecSchema,
218
- ozConfig: OzAdapterConfigSchema.optional(),
219
- // --explain opt-in. When true, the orchestrator attaches the
220
- // in-memory predicate tree (null for the mandate path) + a minimal
221
- // honest SimulationResult to the success envelope. Absent or false
222
- // -> the success envelope is unchanged (byte-identical to today).
223
- explain: z.boolean().optional(),
224
- })
225
-
226
- /** Interpreter opt-in for the recording path. Present -> constraints OZ cannot
227
- * express (per-method scoping, invocation-count windows, oracle bounds, exact
228
- * hop paths) lower to a real interpreter predicate document instead of being
229
- * surfaced as warnings. The core deep-validates `smartAccountAddress` (a C...
230
- * contract, not the recording's G... source) and the tighten-only oracle
231
- * bounds; the schema stays light so the core owns the friendly ToolErrors. */
232
151
  export const InterpreterOptionsSchema = z.object({
233
152
  smartAccountAddress: z.string(),
234
153
  installNonce: z.number().int().positive().optional(),
235
- oracleParams: z
236
- .object({
237
- maxStalenessSeconds: z.number().int().positive().optional(),
238
- maxDeviationBps: z.number().int().positive().optional(),
239
- })
240
- .optional(),
241
154
  })
242
155
 
243
- export const SynthesizePolicyRecordingInputSchema = z.object({
156
+ export const SynthesizePolicyInputSchema = z.object({
244
157
  source: z.literal('recording'),
245
158
  recordedTx: RecordedTransactionSchema,
246
159
  network: NetworkSchema,
247
160
  userResponses: ComposeUserResponsesSchema.optional(),
248
161
  confidenceOverride: z.object({ threshold: z.number().min(0).max(1) }).optional(),
249
162
  interpreter: InterpreterOptionsSchema.optional(),
250
- ozConfig: OzAdapterConfigSchema.optional(),
251
163
  // --explain opt-in. When true, the orchestrator attaches the
252
164
  // in-memory PredicateNode + the corresponding SimulationResult
253
165
  // (real one from the self-verify pipeline when the interpreter is
@@ -259,10 +171,6 @@ export const SynthesizePolicyRecordingInputSchema = z.object({
259
171
  explain: z.boolean().optional(),
260
172
  })
261
173
 
262
- export const SynthesizePolicyInputSchema = z.discriminatedUnion('source', [
263
- SynthesizePolicyMandateInputSchema,
264
- SynthesizePolicyRecordingInputSchema,
265
- ])
266
174
  export type SynthesizePolicyInput = z.infer<typeof SynthesizePolicyInputSchema>
267
175
 
268
176
  // ===== PredicateNode / PredicateLeaf =====
@@ -293,41 +201,6 @@ export const PredicateLeafSchema: z.ZodType<unknown> = z.lazy(() =>
293
201
  element: z.number().int().nonnegative(),
294
202
  field: z.string(),
295
203
  }),
296
- // num/den are i128 on chain; JSON numbers lose precision past 2^53,
297
- // so the wire is a decimal string. The contract refuses `den == 0` and
298
- // `num <= 0` / `den <= 0` at install (types.ts:162-163 + the install
299
- // gate); mirroring that bound here closes the gap where simulate /
300
- // verify green-light a policy the install gate will reject. The
301
- // positive-only regex is the cheapest unambiguous check.
302
- z.object({
303
- kind: z.literal('call_arg_scaled'),
304
- index: z.number().int().nonnegative(),
305
- num: z.string().regex(/^[1-9][0-9]*$/),
306
- den: z.string().regex(/^[1-9][0-9]*$/),
307
- }),
308
- z.object({ kind: z.literal('amount'), token: z.string() }),
309
- z.object({
310
- kind: z.literal('window_spent'),
311
- token: z.string(),
312
- windowSeconds: z.number().int().positive(),
313
- }),
314
- z.object({ kind: z.literal('now') }),
315
- // `valid_until` is NOT in the public predicate grammar: the encoder +
316
- // decoder already throw on it, so accepting it here would let a
317
- // hand-crafted payload through the schema and only surface a 200+
318
- // character internal-commentary error at encode time. Drop it - the
319
- // policy's expiry is carried at the install layer (MandateSpec +
320
- // validUntilLedger) instead.
321
- z.object({
322
- kind: z.literal('invocation_count_in_window'),
323
- windowSecs: z.number().int().positive(),
324
- }),
325
- z.object({ kind: z.literal('oracle_price'), asset: z.string() }),
326
- z.object({
327
- kind: z.literal('oracle_threshold'),
328
- value: z.string().regex(/^-?[0-9]+$/),
329
- decimals: z.number().int().nonnegative(),
330
- }),
331
204
  z.object({ kind: z.literal('literal_address'), value: z.string() }),
332
205
  z.object({ kind: z.literal('literal_i128'), value: z.string().regex(/^-?[0-9]+$/) }),
333
206
  z.object({ kind: z.literal('literal_symbol'), value: z.string() }),
@@ -339,39 +212,23 @@ export const PredicateLeafSchema: z.ZodType<unknown> = z.lazy(() =>
339
212
  kind: z.literal('literal_u32'),
340
213
  value: z.number().int().nonnegative().max(U32_MAX),
341
214
  }),
342
- z.object({ kind: z.literal('literal_u64'), value: z.string().regex(/^[0-9]+$/) }),
343
- // `literal_bytes` is hex on chain. `Buffer.from(value, 'hex')` silently
344
- // drops non-hex chars AND yields an empty buffer when the whole input
345
- // is non-hex - so a user passing 'zzzz' would get a predicate that
346
- // compares against empty bytes instead of being rejected. The
347
- // strict even-length hex regex closes that gap at the boundary.
348
- z.object({
349
- kind: z.literal('literal_bytes'),
350
- value: z
351
- .string()
352
- .regex(/^[0-9a-fA-F]*$/)
353
- .refine((v) => v.length % 2 === 0, 'must be even-length hex'),
354
- }),
355
215
  // elements are themselves PredicateLeaf; lazy to break the cycle.
356
216
  z.object({ kind: z.literal('literal_vec'), elements: z.array(PredicateLeafSchema) }),
357
217
  ])
358
218
  )
359
219
 
360
220
  /** PredicateNode mirrors the core `PredicateNode` union. Recursive through
361
- * `and`/`or`/`not`; the lazy + annotation pattern keeps the recursion
362
- * type-safe. */
221
+ * `and`; the lazy + annotation pattern keeps the recursion type-safe. */
363
222
  export const PredicateNodeSchema: z.ZodType<unknown> = z.lazy(() =>
364
223
  z.union([
365
224
  z.object({ op: z.literal('and'), children: z.array(PredicateNodeSchema) }),
366
- z.object({ op: z.literal('or'), children: z.array(PredicateNodeSchema) }),
367
- z.object({ op: z.literal('not'), child: PredicateNodeSchema }),
368
225
  z.object({
369
226
  op: z.literal('eq'),
370
227
  left: PredicateLeafSchema,
371
228
  right: PredicateLeafSchema,
372
229
  }),
373
230
  z.object({
374
- op: z.enum(['lt', 'lte', 'gt', 'gte']),
231
+ op: z.literal('lte'),
375
232
  left: PredicateLeafSchema,
376
233
  right: PredicateLeafSchema,
377
234
  }),
@@ -385,42 +242,18 @@ export const PredicateNodeSchema: z.ZodType<unknown> = z.lazy(() =>
385
242
 
386
243
  // ===== simulate_policy / verify_policy =====
387
244
  //
388
- // The oracle fixture shape mirrors `SimulateOptions` / `VerifyOptions`
389
- // in packages/policy-synth/src/verify/{simulate,verify}.ts - the same
390
- // sorted set of error enum values is used by both engines. u32
391
- // `validUntilLedger` is bounded at the boundary so a hand-crafted
392
- // payload cannot leak a u32 overflow into the engine's permit context.
393
-
394
- /** Oracle price fixture - mirror of the entry type in `SimulateOptions` /
395
- * `VerifyOptions`. Discriminated by presence of `error` vs `price`. */
396
- export const OraclePriceFixtureSchema = z.union([
397
- z.object({
398
- price: z.string().regex(/^[0-9]+$/),
399
- timestampSeconds: z.number().int().nonnegative(),
400
- }),
401
- z.object({
402
- error: z.enum(['stale', 'missing', 'deviation', 'paused', 'decimals', 'fingerprint']),
403
- }),
404
- ])
405
-
245
+ // Both tools evaluate the same predicate against the same recording, so they
246
+ // take the same input. A null predicate used to mean "OZ built-in policies
247
+ // only"; that backend is gone, so every policy carries a predicate and there is
248
+ // nothing to simulate without one.
406
249
  export const SimulatePolicyInputSchema = z.object({
407
- // simulatePolicy accepts a null predicate (OZ-only policy); verifyPolicy
408
- // does not. The asymmetry is mirrored at the schema boundary.
409
- predicate: PredicateNodeSchema.nullable(),
250
+ predicate: PredicateNodeSchema,
410
251
  permitTx: RecordedTransactionSchema,
411
252
  validUntilLedger: z.number().int().positive().max(U32_MAX).optional(),
412
- oraclePricesByAsset: z.record(z.string(), OraclePriceFixtureSchema).optional(),
413
253
  })
414
254
  export type SimulatePolicyInput = z.infer<typeof SimulatePolicyInputSchema>
415
255
 
416
- export const VerifyPolicyInputSchema = z.object({
417
- // verifyPolicy requires a non-null predicate; the engine refuses
418
- // `null` outright. The schema mirrors that contract.
419
- predicate: PredicateNodeSchema,
420
- permitTx: RecordedTransactionSchema,
421
- validUntilLedger: z.number().int().positive().max(U32_MAX).optional(),
422
- oraclePricesByAsset: z.record(z.string(), OraclePriceFixtureSchema).optional(),
423
- })
256
+ export const VerifyPolicyInputSchema = SimulatePolicyInputSchema
424
257
  export type VerifyPolicyInput = z.infer<typeof VerifyPolicyInputSchema>
425
258
 
426
259
  // ===== install_policy / revoke_policy / get_interpreter_info =====
@@ -435,7 +268,7 @@ export type VerifyPolicyInput = z.infer<typeof VerifyPolicyInputSchema>
435
268
  // optional `grammar_version()` RPC call to check whether the deployed contract
436
269
  // matches the pin. A mismatch is worth MORE than a fabricated audit field.
437
270
  //
438
- // All three input schemas live BELOW the MandateSpecSchemaForRule declaration
271
+ // All three input schemas live BELOW the ContextRuleDraftSchema declaration
439
272
  // so the const reference there is not in the temporal dead zone (no JS hoisting
440
273
  // for `const`).
441
274
 
@@ -448,7 +281,7 @@ export type VerifyPolicyInput = z.infer<typeof VerifyPolicyInputSchema>
448
281
  const MAX_SIGNERS_PER_RULE = 15
449
282
  const MAX_POLICIES_PER_RULE = 5
450
283
 
451
- const MandateSpecSchemaForRule = z
284
+ const ContextRuleDraftSchema = z
452
285
  .object({
453
286
  contextRuleType: z.discriminatedUnion('kind', [
454
287
  z.object({ kind: z.literal('default') }),
@@ -471,21 +304,11 @@ const MandateSpecSchemaForRule = z
471
304
  .max(MAX_SIGNERS_PER_RULE),
472
305
  policies: z
473
306
  .array(
474
- z.discriminatedUnion('kind', [
475
- z.object({
476
- kind: z.literal('interpreter'),
477
- interpreterAddress: z.string(),
478
- predicateBlobBase64: z.string().min(1),
479
- }),
480
- z.object({
481
- kind: z.literal('oz_builtin'),
482
- primitive: z.object({
483
- primitive: z.enum(['spending_limit', 'simple_threshold', 'weighted_threshold']),
484
- params: z.record(z.unknown()),
485
- }),
486
- instanceAddress: z.string(),
487
- }),
488
- ])
307
+ z.object({
308
+ kind: z.literal('interpreter'),
309
+ interpreterAddress: z.string(),
310
+ predicateBlobBase64: z.string().min(1),
311
+ })
489
312
  )
490
313
  .max(MAX_POLICIES_PER_RULE),
491
314
  })
@@ -502,23 +325,28 @@ const MandateSpecSchemaForRule = z
502
325
  /** Pinned interpreter address (testnet).
503
326
  * Single source for the MCP layer; do not embed elsewhere. */
504
327
  export const PINNED_INTERPRETER_TESTNET_ADDRESS =
505
- 'CDR4NLV22STCXFGZPNKDQTEANWLF7LZ6AJLY6B7CLJXKHDZGYJWIOKGP'
506
-
507
- /** Pinned interpreter address (mainnet). Mainnet
508
- * has now been deployed (2026-08-04); the mainnet interpreter IS the binary
509
- * that was exercised on testnet (same wasm sha256, see
510
- * PINNED_INTERPRETER_WASM_SHA256). The address differs because instance
511
- * ids are network-scoped. UNAUDITED at the time of writing. */
328
+ 'CCL336TCK2Y5OFNRCMN2M3HVPBCEX4PW5H6EQ5VW5NPMXOCP4ESB5XR4'
329
+
330
+ /** Pinned interpreter address (mainnet), redeployed 2026-08-22 from a reproducible build. The mainnet
331
+ * interpreter IS the binary exercised on testnet - both instances were created
332
+ * from the same uploaded wasm hash (see PINNED_INTERPRETER_WASM_SHA256), and
333
+ * both were read back with `grammar_version()` returning 3. The address differs
334
+ * because instance ids are network-scoped. UNAUDITED at the time of writing.
335
+ *
336
+ * These four constants move together or not at all. The grammar version and
337
+ * wasm hash are single values covering BOTH networks, so re-pinning one network
338
+ * alone would have the builder emit a version the other network refuses - with
339
+ * a green test run, since `grammar-version-parity.test.ts` would then pass. */
512
340
  export const PINNED_INTERPRETER_MAINNET_ADDRESS =
513
- 'CALZAMUPREIRY4TULBEXIK77AUTOEJG63XLCPUWEHHQDOVK6ZVVS7VQ2'
341
+ 'CBZXLSTQUITBFZHQH6XRXF3XIVRQR4RHRI64Q5WELS5KGY3ZKJPFWDPF'
514
342
 
515
343
  /** Pinned interpreter wasm sha256 (hex). */
516
344
  export const PINNED_INTERPRETER_WASM_SHA256 =
517
- '6e6c13d93e197aa380303a42cd120f5ddb080dd36ef2a343ee1dbd04ca52a443'
345
+ 'a2b36e8ac5a61caf3757af26aa79e83f2995b451099f44772383806a55fe3414'
518
346
 
519
347
  /** The grammar version the interpreter enforces (matches SELF_VERSION in
520
348
  * contracts/policy-interpreter/src/version.rs). */
521
- export const PINNED_INTERPRETER_GRAMMAR_VERSION = 1
349
+ export const PINNED_INTERPRETER_GRAMMAR_VERSION = 3
522
350
 
523
351
  /** Default Soroban RPC for the install / revoke / info tools. The recorder
524
352
  * keeps its own copy in record/rpc.ts because it hands back a fetcher rather
@@ -545,10 +373,6 @@ export const RPC_URL_BY_NETWORK: Record<Network, string> = {
545
373
  mainnet: MAINNET_RPC_URL,
546
374
  }
547
375
 
548
- /** Soroban `valid_until` window (ledgers) added to the latest ledger when
549
- * building the auth entry. ~25 minutes at 5s/ledger. */
550
- export const DEFAULT_AUTH_VALID_UNTIL_LEDGERS = 300
551
-
552
376
  /** Stellar network passphrases. Pinned here so the XDR envelope uses the
553
377
  * matching passphrase when the wallet signs (a mismatch yields invalid
554
378
  * hashes). */
@@ -587,7 +411,7 @@ export const InstallPolicyInputSchema = z
587
411
  * pin and RPC pin do not move by themselves). */
588
412
  network: NetworkSchema.optional(),
589
413
  /** The proposed rule draft. Mirrors the core `ContextRuleDraft` shape. */
590
- rule: MandateSpecSchemaForRule,
414
+ rule: ContextRuleDraftSchema,
591
415
  /** Per-rule install nonce; 1 for a fresh install. */
592
416
  installNonce: z.number().int().positive(),
593
417
  /** Optional RPC URL override. Defaults to the pinned RPC for the