@crediolabs/policy-synth 0.1.3 → 0.1.5
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.
- package/LICENSE +21 -0
- package/README.md +193 -5
- package/dist/adapters/interpreter/adapter.d.ts +38 -0
- package/dist/adapters/interpreter/adapter.js +522 -0
- package/dist/adapters/interpreter/index.d.ts +1 -0
- package/dist/adapters/interpreter/index.js +2 -0
- package/dist/adapters/oz/adapter.js +2 -0
- package/dist/codegen/compile-gate.d.ts +33 -0
- package/dist/codegen/compile-gate.js +119 -0
- package/dist/codegen/index.d.ts +2 -0
- package/dist/codegen/index.js +8 -0
- package/dist/codegen/template.d.ts +18 -0
- package/dist/codegen/template.js +131 -0
- package/dist/errors.d.ts +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/ir/types.d.ts +13 -2
- package/dist/predicate/encode.d.ts +10 -0
- package/dist/predicate/encode.js +249 -0
- package/dist/predicate/index.d.ts +1 -0
- package/dist/predicate/index.js +2 -0
- package/dist/record/decode.d.ts +6 -3
- package/dist/record/decode.js +13 -3
- package/dist/review-card/builder.d.ts +14 -0
- package/dist/review-card/builder.js +261 -0
- package/dist/review-card/conflict.d.ts +40 -0
- package/dist/review-card/conflict.js +111 -0
- package/dist/review-card/cross-check.d.ts +11 -0
- package/dist/review-card/cross-check.js +148 -0
- package/dist/review-card/index.d.ts +3 -0
- package/dist/review-card/index.js +4 -0
- package/dist/run/index.d.ts +31 -0
- package/dist/run/index.js +164 -0
- package/dist/run/schemas.d.ts +2003 -0
- package/dist/run/schemas.js +215 -0
- package/dist/synth/compose-from-recording.d.ts +35 -7
- package/dist/synth/compose-from-recording.js +225 -34
- package/dist/synth/deny-cases.d.ts +12 -0
- package/dist/synth/deny-cases.js +377 -0
- package/dist/synth/evaluate.d.ts +39 -0
- package/dist/synth/evaluate.js +425 -0
- package/dist/synth/harness.d.ts +16 -0
- package/dist/synth/harness.js +26 -0
- package/dist/synth/index.d.ts +4 -0
- package/dist/synth/index.js +4 -0
- package/dist/synth/minimize.d.ts +4 -0
- package/dist/synth/minimize.js +38 -0
- package/dist/synth/predicate-literals.d.ts +5 -0
- package/dist/synth/predicate-literals.js +25 -0
- package/dist/synth/synthesize-from-recording.d.ts +37 -5
- package/dist/synth/synthesize-from-recording.js +550 -18
- package/dist/types.d.ts +40 -1
- package/dist/types.js +17 -0
- package/dist/verify/envelope.d.ts +15 -0
- package/dist/verify/envelope.js +22 -0
- package/dist/verify/index.d.ts +3 -0
- package/dist/verify/index.js +3 -0
- package/dist/verify/simulate.d.ts +31 -0
- package/dist/verify/simulate.js +258 -0
- package/dist/verify/verify.d.ts +21 -0
- package/dist/verify/verify.js +189 -0
- package/dist-cjs/adapters/interpreter/adapter.d.ts +38 -0
- package/dist-cjs/adapters/interpreter/adapter.js +527 -0
- package/dist-cjs/adapters/interpreter/index.d.ts +1 -0
- package/dist-cjs/adapters/interpreter/index.js +8 -0
- package/dist-cjs/adapters/oz/adapter.d.ts +20 -0
- package/dist-cjs/adapters/oz/adapter.js +289 -0
- package/dist-cjs/adapters/oz/index.d.ts +1 -0
- package/dist-cjs/adapters/oz/index.js +8 -0
- package/dist-cjs/codegen/compile-gate.d.ts +33 -0
- package/dist-cjs/codegen/compile-gate.js +123 -0
- package/dist-cjs/codegen/index.d.ts +2 -0
- package/dist-cjs/codegen/index.js +14 -0
- package/dist-cjs/codegen/template.d.ts +18 -0
- package/dist-cjs/codegen/template.js +134 -0
- package/dist-cjs/errors.d.ts +37 -0
- package/dist-cjs/errors.js +3 -0
- package/dist-cjs/index.d.ts +11 -0
- package/dist-cjs/index.js +27 -0
- package/dist-cjs/ir/index.d.ts +1 -0
- package/dist-cjs/ir/index.js +3 -0
- package/dist-cjs/ir/types.d.ts +108 -0
- package/dist-cjs/ir/types.js +12 -0
- package/dist-cjs/mandate/index.d.ts +2 -0
- package/dist-cjs/mandate/index.js +6 -0
- package/dist-cjs/mandate/to-ir.d.ts +3 -0
- package/dist-cjs/mandate/to-ir.js +63 -0
- package/dist-cjs/mandate/types.d.ts +20 -0
- package/dist-cjs/mandate/types.js +9 -0
- package/dist-cjs/package.json +3 -0
- package/dist-cjs/predicate/encode.d.ts +10 -0
- package/dist-cjs/predicate/encode.js +252 -0
- package/dist-cjs/predicate/index.d.ts +1 -0
- package/dist-cjs/predicate/index.js +6 -0
- package/dist-cjs/record/decode.d.ts +79 -0
- package/dist-cjs/record/decode.js +398 -0
- package/dist-cjs/record/freshness.d.ts +17 -0
- package/dist-cjs/record/freshness.js +55 -0
- package/dist-cjs/record/index.d.ts +21 -0
- package/dist-cjs/record/index.js +166 -0
- package/dist-cjs/record/movements.d.ts +20 -0
- package/dist-cjs/record/movements.js +192 -0
- package/dist-cjs/record/rpc.d.ts +22 -0
- package/dist-cjs/record/rpc.js +74 -0
- package/dist-cjs/record/validate.d.ts +22 -0
- package/dist-cjs/record/validate.js +63 -0
- package/dist-cjs/registry/identify.d.ts +11 -0
- package/dist-cjs/registry/identify.js +87 -0
- package/dist-cjs/registry/index.d.ts +3 -0
- package/dist-cjs/registry/index.js +15 -0
- package/dist-cjs/registry/known-addresses.d.ts +16 -0
- package/dist-cjs/registry/known-addresses.js +53 -0
- package/dist-cjs/registry/protocols.d.ts +38 -0
- package/dist-cjs/registry/protocols.js +153 -0
- package/dist-cjs/review-card/builder.d.ts +14 -0
- package/dist-cjs/review-card/builder.js +264 -0
- package/dist-cjs/review-card/conflict.d.ts +40 -0
- package/dist-cjs/review-card/conflict.js +114 -0
- package/dist-cjs/review-card/cross-check.d.ts +11 -0
- package/dist-cjs/review-card/cross-check.js +151 -0
- package/dist-cjs/review-card/index.d.ts +3 -0
- package/dist-cjs/review-card/index.js +10 -0
- package/dist-cjs/run/index.d.ts +31 -0
- package/dist-cjs/run/index.js +178 -0
- package/dist-cjs/run/schemas.d.ts +2003 -0
- package/dist-cjs/run/schemas.js +218 -0
- package/dist-cjs/seams/index.d.ts +1 -0
- package/dist-cjs/seams/index.js +3 -0
- package/dist-cjs/seams/types.d.ts +66 -0
- package/dist-cjs/seams/types.js +12 -0
- package/dist-cjs/synth/compose-from-recording.d.ts +64 -0
- package/dist-cjs/synth/compose-from-recording.js +356 -0
- package/dist-cjs/synth/deny-cases.d.ts +12 -0
- package/dist-cjs/synth/deny-cases.js +380 -0
- package/dist-cjs/synth/evaluate.d.ts +39 -0
- package/dist-cjs/synth/evaluate.js +428 -0
- package/dist-cjs/synth/harness.d.ts +16 -0
- package/dist-cjs/synth/harness.js +29 -0
- package/dist-cjs/synth/index.d.ts +9 -0
- package/dist-cjs/synth/index.js +23 -0
- package/dist-cjs/synth/lower.d.ts +23 -0
- package/dist-cjs/synth/lower.js +119 -0
- package/dist-cjs/synth/minimize.d.ts +4 -0
- package/dist-cjs/synth/minimize.js +41 -0
- package/dist-cjs/synth/predicate-literals.d.ts +5 -0
- package/dist-cjs/synth/predicate-literals.js +28 -0
- package/dist-cjs/synth/scope.d.ts +26 -0
- package/dist-cjs/synth/scope.js +81 -0
- package/dist-cjs/synth/synthesize-from-mandate.d.ts +5 -0
- package/dist-cjs/synth/synthesize-from-mandate.js +37 -0
- package/dist-cjs/synth/synthesize-from-recording.d.ts +49 -0
- package/dist-cjs/synth/synthesize-from-recording.js +711 -0
- package/dist-cjs/types.d.ts +288 -0
- package/dist-cjs/types.js +55 -0
- package/dist-cjs/verify/envelope.d.ts +15 -0
- package/dist-cjs/verify/envelope.js +23 -0
- package/dist-cjs/verify/index.d.ts +3 -0
- package/dist-cjs/verify/index.js +8 -0
- package/dist-cjs/verify/simulate.d.ts +31 -0
- package/dist-cjs/verify/simulate.js +261 -0
- package/dist-cjs/verify/verify.d.ts +21 -0
- package/dist-cjs/verify/verify.js +192 -0
- package/package.json +43 -3
- package/src/adapters/interpreter/adapter.ts +642 -0
- package/src/adapters/interpreter/index.ts +8 -0
- package/src/adapters/oz/adapter.ts +2 -0
- package/src/codegen/compile-gate.ts +162 -0
- package/src/codegen/index.ts +17 -0
- package/src/codegen/template.ts +148 -0
- package/src/errors.ts +2 -0
- package/src/index.ts +2 -0
- package/src/ir/types.ts +9 -2
- package/src/predicate/encode.ts +307 -0
- package/src/predicate/index.ts +3 -0
- package/src/record/decode.ts +21 -10
- package/src/review-card/builder.ts +303 -0
- package/src/review-card/conflict.ts +143 -0
- package/src/review-card/cross-check.ts +158 -0
- package/src/review-card/index.ts +12 -0
- package/src/run/index.ts +213 -0
- package/src/run/schemas.ts +242 -0
- package/src/synth/compose-from-recording.ts +277 -43
- package/src/synth/deny-cases.ts +466 -0
- package/src/synth/evaluate.ts +493 -0
- package/src/synth/harness.ts +40 -0
- package/src/synth/index.ts +12 -0
- package/src/synth/minimize.ts +44 -0
- package/src/synth/predicate-literals.ts +27 -0
- package/src/synth/synthesize-from-recording.ts +655 -19
- package/src/types.ts +38 -2
- package/src/verify/envelope.ts +28 -0
- package/src/verify/index.ts +5 -0
- package/src/verify/simulate.ts +311 -0
- package/src/verify/verify.ts +243 -0
package/src/run/index.ts
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// packages/policy-synth/src/run/index.ts
|
|
2
|
+
//
|
|
3
|
+
// Tool-body adapters for the two core front-ends. Reachable as
|
|
4
|
+
// `@crediolabs/policy-synth/run` from both the MCP server (which re-exports
|
|
5
|
+
// for tool-registration glue) and the CLI (which calls them directly). Each
|
|
6
|
+
// body is a THIN adapter over the pure core:
|
|
7
|
+
//
|
|
8
|
+
// 1. Re-validate the parsed input via Zod. This is a defence-in-depth check -
|
|
9
|
+
// the SDK has already parsed it through the registered schema, but we never
|
|
10
|
+
// want a tool body to throw on garbage (the SDK treats uncaught throws as
|
|
11
|
+
// transport errors and the agent loses the machine-readable ToolError).
|
|
12
|
+
// 2. Dispatch to the matching core entry point with the minimum required
|
|
13
|
+
// inputs.
|
|
14
|
+
// 3. Return the core's ToolResponse<T> unchanged. The server layer maps it
|
|
15
|
+
// to the MCP envelope; nothing here knows about MCP.
|
|
16
|
+
//
|
|
17
|
+
// No business logic. No retries. No session state. The same call shape can
|
|
18
|
+
// drive the CLI (which calls into the same core directly without MCP).
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
type ErrorCode,
|
|
22
|
+
type MandateSpec,
|
|
23
|
+
type OzAdapterConfig,
|
|
24
|
+
type ProposedPolicy,
|
|
25
|
+
placeholderOzConfig,
|
|
26
|
+
type RecordedTransaction,
|
|
27
|
+
recordTransaction,
|
|
28
|
+
type SynthesizeFromRecordingOptions,
|
|
29
|
+
synthesizeFromMandate,
|
|
30
|
+
synthesizeFromRecording,
|
|
31
|
+
type ToolError,
|
|
32
|
+
type ToolResponse,
|
|
33
|
+
} from '../index.ts'
|
|
34
|
+
import {
|
|
35
|
+
type RecordTransactionInput,
|
|
36
|
+
RecordTransactionInputSchema,
|
|
37
|
+
type SynthesizePolicyInput,
|
|
38
|
+
SynthesizePolicyInputSchema,
|
|
39
|
+
} from './schemas.ts'
|
|
40
|
+
|
|
41
|
+
export type { RecordTransactionInput, SynthesizePolicyInput } from './schemas.ts'
|
|
42
|
+
// Re-export the underlying Zod schemas so the MCP package (and any other
|
|
43
|
+
// downstream consumer) can import the canonical input shapes from the same
|
|
44
|
+
// module that owns the tool-body glue. The strict schemas are the source of
|
|
45
|
+
// truth - MCP tool shapes are derived from them.
|
|
46
|
+
export {
|
|
47
|
+
ComposeUserResponsesSchema,
|
|
48
|
+
InterpreterOptionsSchema,
|
|
49
|
+
MandateSpecSchema,
|
|
50
|
+
NetworkSchema,
|
|
51
|
+
OzAdapterConfigSchema,
|
|
52
|
+
RecordedTransactionSchema,
|
|
53
|
+
RecordTransactionInputSchema,
|
|
54
|
+
SynthesizePolicyInputSchema,
|
|
55
|
+
ToolErrorSchema,
|
|
56
|
+
} from './schemas.ts'
|
|
57
|
+
|
|
58
|
+
export type RunRecordTransactionInput = RecordTransactionInput
|
|
59
|
+
|
|
60
|
+
export type RunSynthesizePolicyInput = SynthesizePolicyInput
|
|
61
|
+
|
|
62
|
+
/** `record_transaction` body - wraps `recordTransaction`. The tool input
|
|
63
|
+
* matches the core RecordInput minus the injected `fetcher` (the transport
|
|
64
|
+
* layer does not own the RPC). Returns the core ToolResponse unchanged.
|
|
65
|
+
*
|
|
66
|
+
* Item 6: a try/catch envelope wraps the core call. The core's own
|
|
67
|
+
* `recordTransaction` already returns `{ok:false, error}` for Zod + decoder
|
|
68
|
+
* failures, but a throw from the SDK (e.g. a malformed hash handed to
|
|
69
|
+
* `@stellar/stellar-sdk`'s StrKey decoder) would otherwise surface as
|
|
70
|
+
* "[object Object]" in the MCP transport. The envelope converts any
|
|
71
|
+
* non-ToolResponse throw into a structured `RECORDING_FAILED` ToolError so
|
|
72
|
+
* the agent always sees a machine-readable error code. */
|
|
73
|
+
export async function runRecordTransaction(
|
|
74
|
+
raw: unknown
|
|
75
|
+
): Promise<ToolResponse<RecordedTransaction>> {
|
|
76
|
+
const parsed = RecordTransactionInputSchema.safeParse(raw)
|
|
77
|
+
if (!parsed.success) {
|
|
78
|
+
return {
|
|
79
|
+
ok: false,
|
|
80
|
+
error: validationError('record_transaction', parsed.error.issues),
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const input: RecordTransactionInput = parsed.data
|
|
84
|
+
// Strip the wire-only `confidenceOverride` and pass the rest straight through.
|
|
85
|
+
const coreInput = {
|
|
86
|
+
network: input.network,
|
|
87
|
+
...(input.hash !== undefined ? { hash: input.hash } : {}),
|
|
88
|
+
...(input.xdr !== undefined ? { xdr: input.xdr } : {}),
|
|
89
|
+
...(input.confidenceOverride !== undefined
|
|
90
|
+
? { confidenceOverride: input.confidenceOverride }
|
|
91
|
+
: {}),
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
return await recordTransaction(coreInput)
|
|
95
|
+
} catch (e) {
|
|
96
|
+
return {
|
|
97
|
+
ok: false,
|
|
98
|
+
error: caughtError('record_transaction', 'RECORDING_FAILED', e),
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** `synthesize_policy` body - discriminated union on `source`:
|
|
104
|
+
* - `mandate` -> synthesizeFromMandate
|
|
105
|
+
* - `recording` -> synthesizeFromRecording
|
|
106
|
+
* Exposing BOTH front-ends through ONE tool keeps the MCP surface tiny while
|
|
107
|
+
* letting the agent pick the deterministic or the inferred path. The CLI
|
|
108
|
+
* routes the same way.
|
|
109
|
+
*
|
|
110
|
+
* Item 6: same try/catch envelope as `runRecordTransaction`. The core
|
|
111
|
+
* `synthesizeFromRecording` already converts ToolError-shaped throws to
|
|
112
|
+
* `{ok:false, error}`, but a raw throw from the SDK (e.g. an unexpected
|
|
113
|
+
* XDR decode error in the adapter) would otherwise surface as
|
|
114
|
+
* "[object Object]" in the MCP transport. */
|
|
115
|
+
export async function runSynthesizePolicy(raw: unknown): Promise<ToolResponse<ProposedPolicy>> {
|
|
116
|
+
const parsed = SynthesizePolicyInputSchema.safeParse(raw)
|
|
117
|
+
if (!parsed.success) {
|
|
118
|
+
return {
|
|
119
|
+
ok: false,
|
|
120
|
+
error: validationError('synthesize_policy', parsed.error.issues),
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const input = parsed.data
|
|
124
|
+
const ozConfig: OzAdapterConfig = resolveOzConfig(input)
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
if (input.source === 'mandate') {
|
|
128
|
+
// Zod's optional fields widen to `T | undefined`, which the core's
|
|
129
|
+
// exact-optional MandateSpec rejects; the schema already validated the
|
|
130
|
+
// shape, so assert it (same pattern as the recordedTx cast below).
|
|
131
|
+
return await synthesizeFromMandate(input.mandate as MandateSpec, ozConfig)
|
|
132
|
+
}
|
|
133
|
+
// recording source
|
|
134
|
+
const recorded: RecordedTransaction = input.recordedTx as RecordedTransaction
|
|
135
|
+
return await synthesizeFromRecording(
|
|
136
|
+
recorded,
|
|
137
|
+
{
|
|
138
|
+
network: input.network,
|
|
139
|
+
...(input.userResponses !== undefined ? { userResponses: input.userResponses } : {}),
|
|
140
|
+
...(input.confidenceOverride !== undefined
|
|
141
|
+
? { confidenceOverride: input.confidenceOverride }
|
|
142
|
+
: {}),
|
|
143
|
+
...(input.interpreter !== undefined ? { interpreter: input.interpreter } : {}),
|
|
144
|
+
} as SynthesizeFromRecordingOptions,
|
|
145
|
+
ozConfig
|
|
146
|
+
)
|
|
147
|
+
} catch (e) {
|
|
148
|
+
return {
|
|
149
|
+
ok: false,
|
|
150
|
+
error: caughtError('synthesize_policy', 'SYNTHESIS_ERROR', e),
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function resolveOzConfig(input: SynthesizePolicyInput): OzAdapterConfig {
|
|
156
|
+
if (input.ozConfig) return input.ozConfig
|
|
157
|
+
// The mandate path is network-agnostic; fall back to mainnet so the
|
|
158
|
+
// placeholder OZ instance addresses are deterministic.
|
|
159
|
+
return placeholderOzConfig('mainnet')
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Build a canonical ToolError for a Zod validation failure. The remediation
|
|
163
|
+
* hint points the agent back at the right tool with an empty arg bag - the
|
|
164
|
+
* tool name IS the machine-readable hint. */
|
|
165
|
+
function validationError(
|
|
166
|
+
toolName: 'record_transaction' | 'synthesize_policy',
|
|
167
|
+
issues: ReadonlyArray<{ path: ReadonlyArray<string | number>; message: string }>
|
|
168
|
+
): ToolError {
|
|
169
|
+
const code: ErrorCode = toolName === 'record_transaction' ? 'RECORDING_FAILED' : 'SYNTHESIS_ERROR'
|
|
170
|
+
return {
|
|
171
|
+
code,
|
|
172
|
+
message: `${toolName}: invalid input: ${issues
|
|
173
|
+
.map((i) => `${i.path.join('.') || '<root>'}: ${i.message}`)
|
|
174
|
+
.join('; ')}`,
|
|
175
|
+
severity: 'error',
|
|
176
|
+
retryable: false,
|
|
177
|
+
remediation: {
|
|
178
|
+
toolCall: { name: toolName, args: {} },
|
|
179
|
+
},
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Build a canonical ToolError for a thrown exception caught by the tool
|
|
184
|
+
* envelope. We refuse to surface the raw exception's text (the MCP SDK
|
|
185
|
+
* stringifies thrown objects as "[object Object]"), so we extract a
|
|
186
|
+
* string-friendly message and tag the original error in `details` for the
|
|
187
|
+
* agent to inspect. The `code` is the tool's domain code (RECORDING_FAILED
|
|
188
|
+
* for `record_transaction`, SYNTHESIS_ERROR for `synthesize_policy`) so the
|
|
189
|
+
* agent dispatches on the same code the structured ToolError would carry. */
|
|
190
|
+
function caughtError(
|
|
191
|
+
toolName: 'record_transaction' | 'synthesize_policy',
|
|
192
|
+
code: ErrorCode,
|
|
193
|
+
e: unknown
|
|
194
|
+
): ToolError {
|
|
195
|
+
let message: string
|
|
196
|
+
if (e instanceof Error) {
|
|
197
|
+
message = e.message || `${toolName}: unknown error`
|
|
198
|
+
} else if (typeof e === 'string') {
|
|
199
|
+
message = e
|
|
200
|
+
} else {
|
|
201
|
+
message = `${toolName}: caught non-Error throw of type ${typeof e}`
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
code,
|
|
205
|
+
message: `${toolName}: unhandled throw escaped core envelope: ${message}`,
|
|
206
|
+
severity: 'error',
|
|
207
|
+
retryable: false,
|
|
208
|
+
remediation: {
|
|
209
|
+
toolCall: { name: toolName, args: {} },
|
|
210
|
+
},
|
|
211
|
+
details: { thrown: String(e) },
|
|
212
|
+
}
|
|
213
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// packages/policy-synth/src/run/schemas.ts
|
|
2
|
+
//
|
|
3
|
+
// Zod schemas mirroring the policy-synth core domain types. These are the
|
|
4
|
+
// public input / output shapes exposed over MCP and the CLI. They are kept
|
|
5
|
+
// hand-written (rather than derived) because the MCP SDK needs a runtime
|
|
6
|
+
// Zod object at the transport boundary; a drift test asserts they stay in
|
|
7
|
+
// step with the TS source of truth.
|
|
8
|
+
//
|
|
9
|
+
// i128 amounts and other large integers are carried as base-10 decimal strings
|
|
10
|
+
// end-to-end (no JS number coercion). Networks are pinned to the same closed
|
|
11
|
+
// set the core defines. The discriminated union on `source` exposes BOTH
|
|
12
|
+
// synthesize_policy front-ends through a single tool input.
|
|
13
|
+
//
|
|
14
|
+
// This module is the SINGLE source of truth for these shapes. The MCP package
|
|
15
|
+
// imports them here so its tool-shape bindings stay in step; the CLI imports
|
|
16
|
+
// them here so it can build the same args envelope the MCP transport builds.
|
|
17
|
+
|
|
18
|
+
import { z } from 'zod'
|
|
19
|
+
|
|
20
|
+
/** Soroban `valid_until` is a u32 ledger sequence; a value above this cannot be
|
|
21
|
+
* installed on-chain, so reject it at the boundary (fail-closed). */
|
|
22
|
+
const U32_MAX = 4294967295
|
|
23
|
+
/** Upper bound on top-level invocations in a recorded transaction. A real
|
|
24
|
+
* Stellar tx caps operations well below this; the bound stops a hand-crafted
|
|
25
|
+
* payload from turning one request into an unbounded synthesis (DoS). */
|
|
26
|
+
const MAX_INVOCATIONS = 512
|
|
27
|
+
|
|
28
|
+
export const NetworkSchema = z.enum(['mainnet', 'testnet'])
|
|
29
|
+
export type Network = z.infer<typeof NetworkSchema>
|
|
30
|
+
|
|
31
|
+
/** ScVal subset - normalised subset the synth consumes. Mirrors
|
|
32
|
+
* `ScVal` in packages/policy-synth/src/types.ts. */
|
|
33
|
+
export const ScValSchema: z.ZodType<unknown> = z.lazy(() =>
|
|
34
|
+
z.union([
|
|
35
|
+
z.object({ type: z.literal('address'), value: z.string() }),
|
|
36
|
+
// i128 is SIGNED: real events carry negatives (e.g. a fee-adjustment/refund),
|
|
37
|
+
// so the recorder's own output must round-trip through this schema. u64/u32
|
|
38
|
+
// are unsigned and stay non-negative.
|
|
39
|
+
z.object({ type: z.literal('i128'), value: z.string().regex(/^-?[0-9]+$/) }),
|
|
40
|
+
z.object({ type: z.literal('u64'), value: z.string().regex(/^[0-9]+$/) }),
|
|
41
|
+
z.object({ type: z.literal('u32'), value: z.string().regex(/^[0-9]+$/) }),
|
|
42
|
+
z.object({ type: z.literal('symbol'), value: z.string() }),
|
|
43
|
+
z.object({ type: z.literal('vec'), value: z.array(ScValSchema) }),
|
|
44
|
+
z.object({ type: z.literal('bytes'), value: z.string() }),
|
|
45
|
+
z.object({ type: z.literal('other'), value: z.string() }),
|
|
46
|
+
])
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
/** ContractInvocation mirrors the core. Annotated with an explicit
|
|
50
|
+
* `z.ZodType<unknown>` (like ScValSchema above) so the self-referential
|
|
51
|
+
* `subInvocations` field does not trip TS's circular type inference. */
|
|
52
|
+
export const ContractInvocationSchema: z.ZodType<unknown> = z.object({
|
|
53
|
+
contract: z.string(),
|
|
54
|
+
fn: z.string(),
|
|
55
|
+
args: z.array(ScValSchema),
|
|
56
|
+
subInvocations: z.array(z.lazy(() => ContractInvocationSchema)),
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
export const TokenMovementSchema = z.object({
|
|
60
|
+
token: z.string(),
|
|
61
|
+
from: z.string(),
|
|
62
|
+
to: z.string(),
|
|
63
|
+
// The recorder reads the amount straight from the signed i128 event value
|
|
64
|
+
// (record/movements.ts readAmount), so a non-standard token that emits a
|
|
65
|
+
// negative transfer/mint/burn amount round-trips as a negative string. Mirror
|
|
66
|
+
// that here; the synth gate, not the wire schema, decides what to do with it.
|
|
67
|
+
amount: z.string().regex(/^-?[0-9]+$/),
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
export const OnChainEventSchema = z.object({
|
|
71
|
+
contract: z.string(),
|
|
72
|
+
topics: z.array(z.string()),
|
|
73
|
+
data: ScValSchema,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
export const ParseConfidenceSchema = z.object({
|
|
77
|
+
overall: z.number().min(0).max(1),
|
|
78
|
+
knownContracts: z.array(z.string()),
|
|
79
|
+
unknownContracts: z.array(
|
|
80
|
+
z.object({
|
|
81
|
+
contract: z.string(),
|
|
82
|
+
reason: z.enum(['no-abi', 'version-mismatch', 'opaque-result']),
|
|
83
|
+
})
|
|
84
|
+
),
|
|
85
|
+
opaqueScVals: z.array(z.object({ path: z.string(), type: z.string() })),
|
|
86
|
+
thresholdUsed: z.number().min(0).max(1),
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
/** RecordedTransaction mirrors the core RecordedTransaction. The output shape
|
|
90
|
+
* is referenced by name in the tool result structured content; we deliberately
|
|
91
|
+
* type it loosely (`z.unknown()`) on the success path so the core remains the
|
|
92
|
+
* single source of truth for the wire payload. */
|
|
93
|
+
export const RecordedTransactionSchema = z
|
|
94
|
+
.object({
|
|
95
|
+
network: NetworkSchema,
|
|
96
|
+
signers: z.array(z.string()),
|
|
97
|
+
invocations: z.array(ContractInvocationSchema).max(MAX_INVOCATIONS),
|
|
98
|
+
tokenMovements: z.array(TokenMovementSchema),
|
|
99
|
+
events: z.array(OnChainEventSchema),
|
|
100
|
+
authEntries: z.array(z.unknown()),
|
|
101
|
+
ledgerSequence: z.number().int().nonnegative(),
|
|
102
|
+
fetchedAt: z.number().int().nonnegative(),
|
|
103
|
+
parseConfidence: ParseConfidenceSchema,
|
|
104
|
+
sourceAccount: z.string(),
|
|
105
|
+
})
|
|
106
|
+
.passthrough()
|
|
107
|
+
|
|
108
|
+
/** MandateSpec mirrors the core MandateSpec. The deterministic Mandate
|
|
109
|
+
* front-end needs no parseConfidence; the tool adapter injects the full
|
|
110
|
+
* confidence after synthesis so the orchestrator can compare. */
|
|
111
|
+
export const MandateSpecSchema = z
|
|
112
|
+
.object({
|
|
113
|
+
chain: z.literal('stellar'),
|
|
114
|
+
contract: z.string(),
|
|
115
|
+
method: z.string().optional(),
|
|
116
|
+
spendingLimit: z
|
|
117
|
+
.object({
|
|
118
|
+
token: z.string(),
|
|
119
|
+
limit: z.string().regex(/^[0-9]+$/),
|
|
120
|
+
windowSeconds: z.number().int().positive(),
|
|
121
|
+
})
|
|
122
|
+
.optional(),
|
|
123
|
+
// A threshold of 0 means "0 approvals", which is not a real M-of-N gate.
|
|
124
|
+
approvalThreshold: z.number().int().positive().optional(),
|
|
125
|
+
recipients: z.array(z.string()).optional(),
|
|
126
|
+
expiry: z
|
|
127
|
+
.object({
|
|
128
|
+
validUntilLedger: z.number().int().positive().max(U32_MAX).optional(),
|
|
129
|
+
validUntilUnixSeconds: z.number().int().positive().optional(),
|
|
130
|
+
})
|
|
131
|
+
.optional(),
|
|
132
|
+
})
|
|
133
|
+
.passthrough()
|
|
134
|
+
|
|
135
|
+
/** ComposeUserResponses mirrors the core. */
|
|
136
|
+
export const ComposeUserResponsesSchema = z
|
|
137
|
+
.object({
|
|
138
|
+
windowSeconds: z.number().int().positive().optional(),
|
|
139
|
+
validUntilLedger: z.number().int().positive().max(U32_MAX).optional(),
|
|
140
|
+
limitAmount: z
|
|
141
|
+
.string()
|
|
142
|
+
.regex(/^[0-9]+$/)
|
|
143
|
+
.optional(),
|
|
144
|
+
invocationLimit: z.number().int().positive().optional(),
|
|
145
|
+
})
|
|
146
|
+
.passthrough()
|
|
147
|
+
|
|
148
|
+
/** OzAdapterConfig - the per-network OZ built-in instance addresses. */
|
|
149
|
+
export const OzAdapterConfigSchema = z.object({
|
|
150
|
+
network: NetworkSchema,
|
|
151
|
+
instances: z.object({
|
|
152
|
+
spending_limit: z.string(),
|
|
153
|
+
simple_threshold: z.string(),
|
|
154
|
+
weighted_threshold: z.string(),
|
|
155
|
+
}),
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
// ===== record_transaction =====
|
|
159
|
+
|
|
160
|
+
export const RecordTransactionInputSchema = z
|
|
161
|
+
.object({
|
|
162
|
+
hash: z.string().min(1).optional(),
|
|
163
|
+
xdr: z.string().min(1).optional(),
|
|
164
|
+
network: NetworkSchema,
|
|
165
|
+
confidenceOverride: z.number().min(0).max(1).optional(),
|
|
166
|
+
})
|
|
167
|
+
.refine((v) => !(v.hash && v.xdr), {
|
|
168
|
+
message: 'provide exactly one of `hash` or `xdr`, not both',
|
|
169
|
+
})
|
|
170
|
+
.refine((v) => Boolean(v.hash) || Boolean(v.xdr), {
|
|
171
|
+
message: 'one of `hash` or `xdr` is required',
|
|
172
|
+
})
|
|
173
|
+
export type RecordTransactionInput = z.infer<typeof RecordTransactionInputSchema>
|
|
174
|
+
|
|
175
|
+
// ===== synthesize_policy =====
|
|
176
|
+
//
|
|
177
|
+
// Discriminated union on `source` exposes BOTH front-ends through ONE tool.
|
|
178
|
+
// - `source: 'mandate'` -> calls synthesizeFromMandate
|
|
179
|
+
// - `source: 'recording'` -> calls synthesizeFromRecording
|
|
180
|
+
|
|
181
|
+
export const SynthesizePolicyMandateInputSchema = z.object({
|
|
182
|
+
source: z.literal('mandate'),
|
|
183
|
+
mandate: MandateSpecSchema,
|
|
184
|
+
ozConfig: OzAdapterConfigSchema.optional(),
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
/** Interpreter opt-in for the recording path. Present -> constraints OZ cannot
|
|
188
|
+
* express (per-method scoping, invocation-count windows, oracle bounds, exact
|
|
189
|
+
* hop paths) lower to a real interpreter predicate document instead of being
|
|
190
|
+
* surfaced as warnings. The core deep-validates `smartAccountAddress` (a C...
|
|
191
|
+
* contract, not the recording's G... source) and the tighten-only oracle
|
|
192
|
+
* bounds; the schema stays light so the core owns the friendly ToolErrors. */
|
|
193
|
+
export const InterpreterOptionsSchema = z.object({
|
|
194
|
+
smartAccountAddress: z.string(),
|
|
195
|
+
installNonce: z.number().int().positive().optional(),
|
|
196
|
+
oracleParams: z
|
|
197
|
+
.object({
|
|
198
|
+
maxStalenessSeconds: z.number().int().positive().optional(),
|
|
199
|
+
maxDeviationBps: z.number().int().positive().optional(),
|
|
200
|
+
})
|
|
201
|
+
.optional(),
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
export const SynthesizePolicyRecordingInputSchema = z.object({
|
|
205
|
+
source: z.literal('recording'),
|
|
206
|
+
recordedTx: RecordedTransactionSchema,
|
|
207
|
+
network: NetworkSchema,
|
|
208
|
+
userResponses: ComposeUserResponsesSchema.optional(),
|
|
209
|
+
confidenceOverride: z.object({ threshold: z.number().min(0).max(1) }).optional(),
|
|
210
|
+
interpreter: InterpreterOptionsSchema.optional(),
|
|
211
|
+
ozConfig: OzAdapterConfigSchema.optional(),
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
export const SynthesizePolicyInputSchema = z.discriminatedUnion('source', [
|
|
215
|
+
SynthesizePolicyMandateInputSchema,
|
|
216
|
+
SynthesizePolicyRecordingInputSchema,
|
|
217
|
+
])
|
|
218
|
+
export type SynthesizePolicyInput = z.infer<typeof SynthesizePolicyInputSchema>
|
|
219
|
+
|
|
220
|
+
// ===== Error envelope (canonical) =====
|
|
221
|
+
//
|
|
222
|
+
// Mirrors ToolError from packages/policy-synth/src/errors.ts. We use a
|
|
223
|
+
// `z.string()` for `code` (not an enum) because the core's ErrorCode union
|
|
224
|
+
// evolves over time; the transport contract only promises a string code the
|
|
225
|
+
// caller can dispatch on. A drift test asserts the canonical codes still
|
|
226
|
+
// pass through unchanged.
|
|
227
|
+
export const ToolErrorSchema = z
|
|
228
|
+
.object({
|
|
229
|
+
code: z.string(),
|
|
230
|
+
message: z.string(),
|
|
231
|
+
severity: z.enum(['info', 'warning', 'error', 'fatal']),
|
|
232
|
+
retryable: z.boolean(),
|
|
233
|
+
remediation: z
|
|
234
|
+
.object({
|
|
235
|
+
toolCall: z.object({ name: z.string(), args: z.record(z.unknown()) }).optional(),
|
|
236
|
+
userQuestion: z.object({ code: z.string(), question: z.string() }).optional(),
|
|
237
|
+
docsUrl: z.string().optional(),
|
|
238
|
+
})
|
|
239
|
+
.optional(),
|
|
240
|
+
details: z.unknown().optional(),
|
|
241
|
+
})
|
|
242
|
+
.passthrough()
|