@crewhaus/spec 0.1.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.
- package/package.json +43 -0
- package/src/index.test.ts +1011 -0
- package/src/index.ts +1035 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,1035 @@
|
|
|
1
|
+
import { SpecParseError } from "@crewhaus/errors";
|
|
2
|
+
import { parse as parseYaml } from "yaml";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* v0 spec schema — a discriminated union over `target`.
|
|
7
|
+
*
|
|
8
|
+
* - `cli`: a single streaming-chat agent (Section 1–5).
|
|
9
|
+
* - `workflow`: a sequence of named steps run in order, threading the prior
|
|
10
|
+
* step's final assistant text into the next step's user message (Section 6).
|
|
11
|
+
* - `channel`: a long-running daemon that listens for inbound channel events
|
|
12
|
+
* (Slack today, more channels later) and runs one agent turn per inbound
|
|
13
|
+
* message, threaded by the routing key. Section 12.
|
|
14
|
+
*
|
|
15
|
+
* Will grow into the full catalog spec (eval, deploy) — see
|
|
16
|
+
* docs/MODULE-CATALOG.md PART A Layer F1.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// Permissions block (Section 7). SECURITY: `mode: "bypass"` is intentionally
|
|
20
|
+
// absent from the enum — bypass can only enter the system via the CLI flag.
|
|
21
|
+
// Defense in depth: parse-time and runtime checks both reject it.
|
|
22
|
+
const permissionRuleSchema = z
|
|
23
|
+
.object({
|
|
24
|
+
type: z.enum(["alwaysAllow", "alwaysDeny", "alwaysAsk"]),
|
|
25
|
+
pattern: z.string().min(1),
|
|
26
|
+
})
|
|
27
|
+
.strict();
|
|
28
|
+
|
|
29
|
+
const permissionsBlock = z
|
|
30
|
+
.object({
|
|
31
|
+
mode: z.enum(["default", "plan", "auto"]).optional(),
|
|
32
|
+
rules: z.array(permissionRuleSchema).optional(),
|
|
33
|
+
})
|
|
34
|
+
.strict()
|
|
35
|
+
.optional();
|
|
36
|
+
|
|
37
|
+
// MCP servers block (Section 9). Discriminated on `transport` so unknown
|
|
38
|
+
// configs surface as a clear "Invalid literal value" error rather than a
|
|
39
|
+
// confusing union-of-rejections.
|
|
40
|
+
const stdioMcpConfig = z
|
|
41
|
+
.object({
|
|
42
|
+
transport: z.literal("stdio"),
|
|
43
|
+
command: z.string().min(1),
|
|
44
|
+
args: z.array(z.string()).optional(),
|
|
45
|
+
env: z.record(z.string()).optional(),
|
|
46
|
+
})
|
|
47
|
+
.strict();
|
|
48
|
+
|
|
49
|
+
const sseMcpConfig = z
|
|
50
|
+
.object({
|
|
51
|
+
transport: z.literal("sse"),
|
|
52
|
+
url: z.string().url(),
|
|
53
|
+
headers: z.record(z.string()).optional(),
|
|
54
|
+
})
|
|
55
|
+
.strict();
|
|
56
|
+
|
|
57
|
+
const mcpServerConfigSchema = z.discriminatedUnion("transport", [stdioMcpConfig, sseMcpConfig]);
|
|
58
|
+
|
|
59
|
+
const mcpServersBlock = z.record(z.string().min(1), mcpServerConfigSchema).optional();
|
|
60
|
+
|
|
61
|
+
// Section 13 — sub-agent definitions. Inline on the agent block (cli +
|
|
62
|
+
// channel today; workflow has no agent block). The map's key is the
|
|
63
|
+
// `subagent_type` users pass to the Task tool. Permissions field mirrors
|
|
64
|
+
// the runtime's resolution shape.
|
|
65
|
+
const subAgentDefinitionSchema = z
|
|
66
|
+
.object({
|
|
67
|
+
description: z.string().min(1),
|
|
68
|
+
instructions: z.string().min(1),
|
|
69
|
+
tools: z.array(z.string().min(1)).optional(),
|
|
70
|
+
model: z.string().min(1).optional(),
|
|
71
|
+
permissions: z
|
|
72
|
+
.union([
|
|
73
|
+
z.enum(["inherit", "scoped"]),
|
|
74
|
+
z
|
|
75
|
+
.object({
|
|
76
|
+
allow: z.array(z.string().min(1)),
|
|
77
|
+
deny: z.array(z.string().min(1)),
|
|
78
|
+
})
|
|
79
|
+
.strict(),
|
|
80
|
+
])
|
|
81
|
+
.optional(),
|
|
82
|
+
inherit_bypass: z.boolean().optional(),
|
|
83
|
+
})
|
|
84
|
+
.strict();
|
|
85
|
+
|
|
86
|
+
const subAgentsBlock = z.record(z.string().min(1), subAgentDefinitionSchema).optional();
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Section 14 — per-tool runtime config map. Tool-specific schemas live
|
|
90
|
+
* inside each tool package; the spec layer treats every value as opaque
|
|
91
|
+
* `unknown` and forwards it verbatim to the IR. The codegen layer emits
|
|
92
|
+
* an init call (e.g. `registerFetchConfig({ ... })`) for tools whose
|
|
93
|
+
* BUILTIN_TOOL_MAP entry declares an `initSymbol`.
|
|
94
|
+
*/
|
|
95
|
+
const toolConfigBlock = z.record(z.string().min(1), z.unknown()).optional();
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Section 17 — optional override for the model used by
|
|
99
|
+
* `compaction-autocompact` when summarising long conversations. Defaults
|
|
100
|
+
* to the agent's primary model when omitted, but you can target a
|
|
101
|
+
* cheaper/faster model (or a different provider) for compaction.
|
|
102
|
+
*/
|
|
103
|
+
const compactionBlock = z
|
|
104
|
+
.object({
|
|
105
|
+
model: z.string().min(1).optional(),
|
|
106
|
+
/** Pillar 2 — opt in to the pre-compaction curator pass. Defaults
|
|
107
|
+
* to `false` when omitted; the IR carries the user's choice
|
|
108
|
+
* verbatim so target emitters can wire `@crewhaus/compaction-curator`
|
|
109
|
+
* on the runtime path. See docs/MODULE-CATALOG.md R6 + recipe 52. */
|
|
110
|
+
curate: z.boolean().optional(),
|
|
111
|
+
/** Cosine-similarity threshold above which two items are considered
|
|
112
|
+
* duplicates by the curator. Defaults to 0.92 in the curator
|
|
113
|
+
* itself; spec-level override targets per-corpus tuning. Must be
|
|
114
|
+
* in (0, 1] — values outside that range can't be cosine outputs. */
|
|
115
|
+
dedupeThreshold: z.number().gt(0).lte(1).optional(),
|
|
116
|
+
/** Max items the curator keeps after the relevance reorder. When
|
|
117
|
+
* omitted, the curator only reorders (no top-K trim). Spec-level
|
|
118
|
+
* override is the natural knob for RAG pipelines that want a hard
|
|
119
|
+
* cap on retrieved chunks per turn. */
|
|
120
|
+
relevanceTopK: z.number().int().positive().optional(),
|
|
121
|
+
})
|
|
122
|
+
.strict()
|
|
123
|
+
.optional();
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Section 55 (Track A) — named failure taxonomy. Cross-cutting block
|
|
127
|
+
* available on every target shape. Each entry names a failure class and
|
|
128
|
+
* tells the recovery engine which `RecoveryAction` to take when the
|
|
129
|
+
* pattern matches an error's `message`. Optional `hint` is surfaced to
|
|
130
|
+
* the model as a one-shot system message on `continue`/`retry` recovery.
|
|
131
|
+
*
|
|
132
|
+
* Source: Natural-Language Agent Harnesses (arxiv 2603.25723, Tsinghua,
|
|
133
|
+
* March 2026) names failure_taxonomy as one of the six components a
|
|
134
|
+
* portable harness must expose. Cited paper: NLAH (arxiv 2603.25723).
|
|
135
|
+
*/
|
|
136
|
+
const failureTaxonomyEntrySchema = z
|
|
137
|
+
.object({
|
|
138
|
+
class: z.string().min(1),
|
|
139
|
+
pattern: z.string().min(1),
|
|
140
|
+
recovery: z.enum(["retry", "compact", "continue", "tombstone", "fail"]),
|
|
141
|
+
hint: z.string().min(1).optional(),
|
|
142
|
+
})
|
|
143
|
+
.strict();
|
|
144
|
+
|
|
145
|
+
const failureTaxonomyBlock = z.array(failureTaxonomyEntrySchema).optional();
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Section 47 — blockchain subsystem blocks (cross-cutting). Any shape may
|
|
149
|
+
* declare any subset of `chains` / `wallets` / `contracts` /
|
|
150
|
+
* `transaction_policy`. Authoring rules:
|
|
151
|
+
* - `chains[]`: at least one when other blocks are present.
|
|
152
|
+
* - `wallets[]`: every entry references a declared `chains[].id`.
|
|
153
|
+
* - `contracts[]`: every entry references a declared `chains[].id`.
|
|
154
|
+
* - `transaction_policy`: enforced by §47 IR pass at compile time;
|
|
155
|
+
* entries in `allowed_contracts` must reference declared `contracts[].id`.
|
|
156
|
+
* Per-field semantics mirror the IR variants in `@crewhaus/ir`.
|
|
157
|
+
*/
|
|
158
|
+
const chainFinalitySchema = z.discriminatedUnion("kind", [
|
|
159
|
+
z
|
|
160
|
+
.object({
|
|
161
|
+
kind: z.literal("confirmations"),
|
|
162
|
+
count: z.number().int().min(0).max(256),
|
|
163
|
+
})
|
|
164
|
+
.strict(),
|
|
165
|
+
z.object({ kind: z.literal("finalized") }).strict(),
|
|
166
|
+
z.object({ kind: z.literal("safe") }).strict(),
|
|
167
|
+
]);
|
|
168
|
+
|
|
169
|
+
const chainBindingSchema = z
|
|
170
|
+
.object({
|
|
171
|
+
id: z.string().min(1),
|
|
172
|
+
kind: z.literal("evm"),
|
|
173
|
+
rpcUrls: z.array(z.string().min(1)).min(1),
|
|
174
|
+
rpcPolicy: z.enum(["single", "quorum", "fallback"]).default("single"),
|
|
175
|
+
finality: chainFinalitySchema,
|
|
176
|
+
reorgTolerant: z.boolean().default(true),
|
|
177
|
+
})
|
|
178
|
+
.strict();
|
|
179
|
+
|
|
180
|
+
const walletBindingSchema = z
|
|
181
|
+
.object({
|
|
182
|
+
id: z.string().min(1),
|
|
183
|
+
chainId: z.string().min(1),
|
|
184
|
+
custody: z.enum(["user-controlled", "kms", "hsm", "local"]),
|
|
185
|
+
signingPolicy: z
|
|
186
|
+
.enum(["explicit-user-approval", "policy-gated", "automated"])
|
|
187
|
+
.default("explicit-user-approval"),
|
|
188
|
+
keyRef: z.string().min(1).optional(),
|
|
189
|
+
})
|
|
190
|
+
.strict();
|
|
191
|
+
|
|
192
|
+
const contractBindingSchema = z
|
|
193
|
+
.object({
|
|
194
|
+
id: z.string().min(1),
|
|
195
|
+
chainId: z.string().min(1),
|
|
196
|
+
address: z.string().min(1),
|
|
197
|
+
abiRef: z.string().min(1),
|
|
198
|
+
})
|
|
199
|
+
.strict();
|
|
200
|
+
|
|
201
|
+
const transactionPolicySchema = z
|
|
202
|
+
.object({
|
|
203
|
+
defaultWriteApproval: z.enum(["required", "policy", "none"]).default("required"),
|
|
204
|
+
maxValueUsd: z.number().positive().optional(),
|
|
205
|
+
allowedContracts: z.array(z.string().min(1)).default([]),
|
|
206
|
+
simulationRequired: z.boolean().default(true),
|
|
207
|
+
})
|
|
208
|
+
.strict();
|
|
209
|
+
|
|
210
|
+
const chainsBlock = z.array(chainBindingSchema).optional();
|
|
211
|
+
const walletsBlock = z.array(walletBindingSchema).optional();
|
|
212
|
+
const contractsBlock = z.array(contractBindingSchema).optional();
|
|
213
|
+
const transactionPolicyBlock = transactionPolicySchema.optional();
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Phase 3 §3.3 — CLI banner with optional tagline rotation. When set,
|
|
217
|
+
* the compiled cli-target bundle prints this banner on cold start
|
|
218
|
+
* (suppressed under `--resume` / `--continue` so resumed sessions
|
|
219
|
+
* don't re-banner). Static mode picks the first tagline; random mode
|
|
220
|
+
* picks one uniformly per startup.
|
|
221
|
+
*/
|
|
222
|
+
const cliBannerBlock = z
|
|
223
|
+
.object({
|
|
224
|
+
taglineMode: z.enum(["static", "random"]).default("static"),
|
|
225
|
+
taglines: z.array(z.string().min(1)).min(1),
|
|
226
|
+
})
|
|
227
|
+
.strict()
|
|
228
|
+
.optional();
|
|
229
|
+
|
|
230
|
+
const cliOptionsBlock = z
|
|
231
|
+
.object({
|
|
232
|
+
banner: cliBannerBlock,
|
|
233
|
+
/**
|
|
234
|
+
* Phase 2 M2.2 — TUI polish gate. "basic" is the current readline-
|
|
235
|
+
* driven REPL; "rich" is reserved for future Ink-based output
|
|
236
|
+
* (status line, multi-line input, ESC interrupt). Today both modes
|
|
237
|
+
* compile identically; the field is forward-compatible.
|
|
238
|
+
*/
|
|
239
|
+
tui: z.enum(["basic", "rich"]).default("basic"),
|
|
240
|
+
})
|
|
241
|
+
.strict()
|
|
242
|
+
.optional();
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Phase 3 §3.1 — heartbeat scheduled wake for channel daemons. When
|
|
246
|
+
* present, target-channel-bot emits a setInterval loop that
|
|
247
|
+
* synthesises a heartbeat turn at the configured interval. The
|
|
248
|
+
* `every` field accepts a duration string (e.g. "2h", "30m", "60s").
|
|
249
|
+
* `instructions` is what the runtime sends as the synthetic user
|
|
250
|
+
* message at each tick; pair with HEARTBEAT.md in cwd for richer
|
|
251
|
+
* playbook reads.
|
|
252
|
+
*/
|
|
253
|
+
const HEARTBEAT_DURATION_REGEX = /^\d+(?:ms|s|m|h)$/;
|
|
254
|
+
|
|
255
|
+
const heartbeatBlock = z
|
|
256
|
+
.object({
|
|
257
|
+
every: z
|
|
258
|
+
.string()
|
|
259
|
+
.regex(
|
|
260
|
+
HEARTBEAT_DURATION_REGEX,
|
|
261
|
+
'heartbeat.every must be a duration like "2h", "30m", "60s", or "500ms"',
|
|
262
|
+
),
|
|
263
|
+
instructions: z.string().min(1),
|
|
264
|
+
})
|
|
265
|
+
.strict()
|
|
266
|
+
.optional();
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Phase 3 §3.4 — channel daemon control-UI gateway. When set, the
|
|
270
|
+
* compiled daemon spawns a second HTTP listener on `port` that serves
|
|
271
|
+
* a status endpoint (and, when `ui: true`, a minimal dashboard).
|
|
272
|
+
* Mirrors OpenClaw's Gateway control plane in concept; ours starts
|
|
273
|
+
* minimal and is intended to host packaged Studio UI in a follow-up.
|
|
274
|
+
*/
|
|
275
|
+
const channelGatewayBlock = z
|
|
276
|
+
.object({
|
|
277
|
+
port: z.number().int().min(1).max(65535),
|
|
278
|
+
ui: z.boolean().default(false),
|
|
279
|
+
})
|
|
280
|
+
.strict()
|
|
281
|
+
.optional();
|
|
282
|
+
|
|
283
|
+
const cliSchema = z
|
|
284
|
+
.object({
|
|
285
|
+
name: z.string().min(1),
|
|
286
|
+
target: z.literal("cli"),
|
|
287
|
+
agent: z
|
|
288
|
+
.object({
|
|
289
|
+
model: z.string().min(1),
|
|
290
|
+
instructions: z.string().min(1),
|
|
291
|
+
sub_agents: subAgentsBlock,
|
|
292
|
+
})
|
|
293
|
+
.strict(),
|
|
294
|
+
tools: z.array(z.string().min(1)).optional(),
|
|
295
|
+
tool_config: toolConfigBlock,
|
|
296
|
+
mcp_servers: mcpServersBlock,
|
|
297
|
+
permissions: permissionsBlock,
|
|
298
|
+
compaction: compactionBlock,
|
|
299
|
+
failure_taxonomy: failureTaxonomyBlock,
|
|
300
|
+
cli: cliOptionsBlock,
|
|
301
|
+
chains: chainsBlock,
|
|
302
|
+
wallets: walletsBlock,
|
|
303
|
+
contracts: contractsBlock,
|
|
304
|
+
transaction_policy: transactionPolicyBlock,
|
|
305
|
+
})
|
|
306
|
+
.strict();
|
|
307
|
+
|
|
308
|
+
const workflowStepSchema = z
|
|
309
|
+
.object({
|
|
310
|
+
name: z.string().min(1),
|
|
311
|
+
instructions: z.string().min(1),
|
|
312
|
+
model: z.string().min(1).optional(),
|
|
313
|
+
tools: z.array(z.string().min(1)).optional(),
|
|
314
|
+
tool_config: toolConfigBlock,
|
|
315
|
+
})
|
|
316
|
+
.strict();
|
|
317
|
+
|
|
318
|
+
const workflowSchema = z
|
|
319
|
+
.object({
|
|
320
|
+
name: z.string().min(1),
|
|
321
|
+
target: z.literal("workflow"),
|
|
322
|
+
model: z.string().min(1),
|
|
323
|
+
steps: z.array(workflowStepSchema).min(1),
|
|
324
|
+
mcp_servers: mcpServersBlock,
|
|
325
|
+
permissions: permissionsBlock,
|
|
326
|
+
compaction: compactionBlock,
|
|
327
|
+
failure_taxonomy: failureTaxonomyBlock,
|
|
328
|
+
chains: chainsBlock,
|
|
329
|
+
wallets: walletsBlock,
|
|
330
|
+
contracts: contractsBlock,
|
|
331
|
+
transaction_policy: transactionPolicyBlock,
|
|
332
|
+
})
|
|
333
|
+
.strict();
|
|
334
|
+
|
|
335
|
+
// Channel target (Section 12). Secret fields (botToken/signingSecret/appToken)
|
|
336
|
+
// are kept as plain strings here; the compiler's `lower()` rewrites strings
|
|
337
|
+
// matching `$VAR_NAME` into env-var references in the IR so the compiled
|
|
338
|
+
// bundle reads `process.env.VAR_NAME` at runtime instead of embedding secrets.
|
|
339
|
+
const slackChannelSchema = z
|
|
340
|
+
.object({
|
|
341
|
+
botToken: z.string().min(1),
|
|
342
|
+
signingSecret: z.string().min(1),
|
|
343
|
+
appToken: z.string().min(1).optional(),
|
|
344
|
+
})
|
|
345
|
+
.strict();
|
|
346
|
+
|
|
347
|
+
const telegramChannelSchema = z
|
|
348
|
+
.object({
|
|
349
|
+
botToken: z.string().min(1),
|
|
350
|
+
secretToken: z.string().min(1),
|
|
351
|
+
})
|
|
352
|
+
.strict();
|
|
353
|
+
|
|
354
|
+
const discordChannelSchema = z
|
|
355
|
+
.object({
|
|
356
|
+
applicationId: z.string().min(1),
|
|
357
|
+
botToken: z.string().min(1),
|
|
358
|
+
publicKeyHex: z.string().min(1),
|
|
359
|
+
})
|
|
360
|
+
.strict();
|
|
361
|
+
|
|
362
|
+
const whatsappChannelSchema = z
|
|
363
|
+
.object({
|
|
364
|
+
phoneNumberId: z.string().min(1),
|
|
365
|
+
accessToken: z.string().min(1),
|
|
366
|
+
appSecret: z.string().min(1),
|
|
367
|
+
})
|
|
368
|
+
.strict();
|
|
369
|
+
|
|
370
|
+
const imessageChannelSchema = z
|
|
371
|
+
.object({
|
|
372
|
+
chatDbPath: z.string().min(1).optional(),
|
|
373
|
+
cursorPath: z.string().min(1).optional(),
|
|
374
|
+
})
|
|
375
|
+
.strict();
|
|
376
|
+
|
|
377
|
+
const channelsBlock = z
|
|
378
|
+
.object({
|
|
379
|
+
slack: slackChannelSchema.optional(),
|
|
380
|
+
telegram: telegramChannelSchema.optional(),
|
|
381
|
+
discord: discordChannelSchema.optional(),
|
|
382
|
+
whatsapp: whatsappChannelSchema.optional(),
|
|
383
|
+
imessage: imessageChannelSchema.optional(),
|
|
384
|
+
})
|
|
385
|
+
.strict()
|
|
386
|
+
.refine(
|
|
387
|
+
(c) =>
|
|
388
|
+
c.slack !== undefined ||
|
|
389
|
+
c.telegram !== undefined ||
|
|
390
|
+
c.discord !== undefined ||
|
|
391
|
+
c.whatsapp !== undefined ||
|
|
392
|
+
c.imessage !== undefined,
|
|
393
|
+
{
|
|
394
|
+
message:
|
|
395
|
+
"channels block requires at least one channel (slack | telegram | discord | whatsapp | imessage)",
|
|
396
|
+
},
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
const routingBlock = z
|
|
400
|
+
.object({
|
|
401
|
+
sessionKey: z.enum(["thread", "user", "channel"]),
|
|
402
|
+
})
|
|
403
|
+
.strict();
|
|
404
|
+
|
|
405
|
+
const channelAgentSchema = z
|
|
406
|
+
.object({
|
|
407
|
+
model: z.string().min(1),
|
|
408
|
+
instructions: z.string().min(1),
|
|
409
|
+
tools: z.array(z.string().min(1)).optional(),
|
|
410
|
+
tool_config: toolConfigBlock,
|
|
411
|
+
sub_agents: subAgentsBlock,
|
|
412
|
+
})
|
|
413
|
+
.strict();
|
|
414
|
+
|
|
415
|
+
const channelSchema = z
|
|
416
|
+
.object({
|
|
417
|
+
name: z.string().min(1),
|
|
418
|
+
target: z.literal("channel"),
|
|
419
|
+
agent: channelAgentSchema,
|
|
420
|
+
channels: channelsBlock,
|
|
421
|
+
routing: routingBlock,
|
|
422
|
+
mcp_servers: mcpServersBlock,
|
|
423
|
+
permissions: permissionsBlock,
|
|
424
|
+
compaction: compactionBlock,
|
|
425
|
+
failure_taxonomy: failureTaxonomyBlock,
|
|
426
|
+
heartbeat: heartbeatBlock,
|
|
427
|
+
gateway: channelGatewayBlock,
|
|
428
|
+
chains: chainsBlock,
|
|
429
|
+
wallets: walletsBlock,
|
|
430
|
+
contracts: contractsBlock,
|
|
431
|
+
transaction_policy: transactionPolicyBlock,
|
|
432
|
+
})
|
|
433
|
+
.strict();
|
|
434
|
+
|
|
435
|
+
// Graph target (Section 19) — stateful DAG runtime. Nodes are LLM-backed
|
|
436
|
+
// invocations; edges link nodes; HITL pauses interrupt the run on
|
|
437
|
+
// `requestApproval()`. Each node may have its own model + tools.
|
|
438
|
+
const graphNodeSchema = z
|
|
439
|
+
.object({
|
|
440
|
+
instructions: z.string().min(1),
|
|
441
|
+
model: z.string().min(1).optional(),
|
|
442
|
+
tools: z.array(z.string().min(1)).optional(),
|
|
443
|
+
tool_config: toolConfigBlock,
|
|
444
|
+
/**
|
|
445
|
+
* When true, the node calls `ctx.requestApproval(prompt)` before
|
|
446
|
+
* returning. The engine pauses, persists a checkpoint, and waits for
|
|
447
|
+
* `resume(checkpointId, decision)` from the operator/CLI.
|
|
448
|
+
*/
|
|
449
|
+
hitl: z
|
|
450
|
+
.object({
|
|
451
|
+
prompt: z.string().min(1),
|
|
452
|
+
})
|
|
453
|
+
.strict()
|
|
454
|
+
.optional(),
|
|
455
|
+
})
|
|
456
|
+
.strict();
|
|
457
|
+
|
|
458
|
+
const graphEdgeSchema = z
|
|
459
|
+
.object({
|
|
460
|
+
from: z.string().min(1),
|
|
461
|
+
to: z.string().min(1),
|
|
462
|
+
})
|
|
463
|
+
.strict();
|
|
464
|
+
|
|
465
|
+
const graphSchema = z
|
|
466
|
+
.object({
|
|
467
|
+
name: z.string().min(1),
|
|
468
|
+
target: z.literal("graph"),
|
|
469
|
+
model: z.string().min(1),
|
|
470
|
+
entry: z.string().min(1),
|
|
471
|
+
nodes: z.record(z.string().min(1), graphNodeSchema),
|
|
472
|
+
edges: z.array(graphEdgeSchema).default([]),
|
|
473
|
+
permissions: permissionsBlock,
|
|
474
|
+
compaction: compactionBlock,
|
|
475
|
+
failure_taxonomy: failureTaxonomyBlock,
|
|
476
|
+
chains: chainsBlock,
|
|
477
|
+
wallets: walletsBlock,
|
|
478
|
+
contracts: contractsBlock,
|
|
479
|
+
transaction_policy: transactionPolicyBlock,
|
|
480
|
+
})
|
|
481
|
+
.strict();
|
|
482
|
+
|
|
483
|
+
// Managed daemon target (Section 20). Multi-tenant gateway with
|
|
484
|
+
// per-tenant budgets + policy overrides; emitted bundle is daemon.ts +
|
|
485
|
+
// agent.ts. Authentication is HS256 JWT — the signing secret enters
|
|
486
|
+
// via env at boot, not via the spec.
|
|
487
|
+
const managedTenantSchema = z
|
|
488
|
+
.object({
|
|
489
|
+
id: z.string().min(1),
|
|
490
|
+
budget: z
|
|
491
|
+
.object({
|
|
492
|
+
maxInputTokens: z.number().int().positive(),
|
|
493
|
+
maxOutputTokens: z.number().int().positive(),
|
|
494
|
+
})
|
|
495
|
+
.strict(),
|
|
496
|
+
})
|
|
497
|
+
.strict();
|
|
498
|
+
|
|
499
|
+
const managedAgentSchema = z
|
|
500
|
+
.object({
|
|
501
|
+
model: z.string().min(1),
|
|
502
|
+
instructions: z.string().min(1),
|
|
503
|
+
})
|
|
504
|
+
.strict();
|
|
505
|
+
|
|
506
|
+
const managedSchema = z
|
|
507
|
+
.object({
|
|
508
|
+
name: z.string().min(1),
|
|
509
|
+
target: z.literal("managed"),
|
|
510
|
+
agent: managedAgentSchema,
|
|
511
|
+
tenants: z.array(managedTenantSchema).min(1),
|
|
512
|
+
permissions: permissionsBlock,
|
|
513
|
+
compaction: compactionBlock,
|
|
514
|
+
failure_taxonomy: failureTaxonomyBlock,
|
|
515
|
+
})
|
|
516
|
+
.strict();
|
|
517
|
+
|
|
518
|
+
// Pipeline / RAG target (Section 21). Carries the embedder + vector-store
|
|
519
|
+
// config, an indexing pipeline, and a chat agent that uses Retrieve.
|
|
520
|
+
const pipelineDocumentSchema = z
|
|
521
|
+
.object({
|
|
522
|
+
id: z.string().min(1),
|
|
523
|
+
text: z.string().min(1),
|
|
524
|
+
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
525
|
+
})
|
|
526
|
+
.strict();
|
|
527
|
+
|
|
528
|
+
const pipelineSchema = z
|
|
529
|
+
.object({
|
|
530
|
+
name: z.string().min(1),
|
|
531
|
+
target: z.literal("pipeline"),
|
|
532
|
+
agent: z
|
|
533
|
+
.object({
|
|
534
|
+
model: z.string().min(1),
|
|
535
|
+
instructions: z.string().min(1),
|
|
536
|
+
})
|
|
537
|
+
.strict(),
|
|
538
|
+
retrieve: z
|
|
539
|
+
.object({
|
|
540
|
+
embedderModel: z.string().min(1),
|
|
541
|
+
vectorBackend: z.enum(["in-memory"]).default("in-memory"),
|
|
542
|
+
defaultK: z.number().int().positive().max(50).default(5),
|
|
543
|
+
})
|
|
544
|
+
.strict(),
|
|
545
|
+
indexing: z
|
|
546
|
+
.object({
|
|
547
|
+
chunkStrategy: z.enum(["fixed", "semantic", "markdown"]).default("fixed"),
|
|
548
|
+
chunkSize: z.number().int().positive().default(400),
|
|
549
|
+
chunkOverlap: z.number().int().nonnegative().default(0),
|
|
550
|
+
documents: z.array(pipelineDocumentSchema).min(1),
|
|
551
|
+
})
|
|
552
|
+
.strict(),
|
|
553
|
+
permissions: permissionsBlock,
|
|
554
|
+
compaction: compactionBlock,
|
|
555
|
+
failure_taxonomy: failureTaxonomyBlock,
|
|
556
|
+
})
|
|
557
|
+
.strict();
|
|
558
|
+
|
|
559
|
+
// Crew target (Section 22). Multi-role agent runtime; each role is an
|
|
560
|
+
// `agent`-shaped block (model + instructions + tools); `entry` names the
|
|
561
|
+
// first-active role; optional `routing` block carries either `match`
|
|
562
|
+
// rules or `llm` directive (lower-time placeholder for an LLM-backed
|
|
563
|
+
// router; runtime falls back to "no router" when the target shape lands
|
|
564
|
+
// on the codegen path).
|
|
565
|
+
const crewRoleSchema = z
|
|
566
|
+
.object({
|
|
567
|
+
instructions: z.string().min(1),
|
|
568
|
+
model: z.string().min(1).optional(),
|
|
569
|
+
tools: z.array(z.string().min(1)).optional(),
|
|
570
|
+
tool_config: toolConfigBlock,
|
|
571
|
+
sub_agents: subAgentsBlock,
|
|
572
|
+
})
|
|
573
|
+
.strict();
|
|
574
|
+
|
|
575
|
+
const crewRoutingMatchEntrySchema = z
|
|
576
|
+
.object({
|
|
577
|
+
contains: z.string().min(1),
|
|
578
|
+
to: z.string().min(1),
|
|
579
|
+
})
|
|
580
|
+
.strict();
|
|
581
|
+
|
|
582
|
+
const crewRoutingSchema = z
|
|
583
|
+
.object({
|
|
584
|
+
kind: z.enum(["match", "llm"]),
|
|
585
|
+
match: z.record(z.string().min(1), z.array(crewRoutingMatchEntrySchema).min(1)).optional(),
|
|
586
|
+
})
|
|
587
|
+
.strict();
|
|
588
|
+
|
|
589
|
+
const crewSchema = z
|
|
590
|
+
.object({
|
|
591
|
+
name: z.string().min(1),
|
|
592
|
+
target: z.literal("crew"),
|
|
593
|
+
/** Crew-wide model fallback used by any role that omits `role.model`. */
|
|
594
|
+
model: z.string().min(1),
|
|
595
|
+
entry: z.string().min(1),
|
|
596
|
+
roles: z.record(z.string().min(1), crewRoleSchema),
|
|
597
|
+
routing: crewRoutingSchema.optional(),
|
|
598
|
+
mcp_servers: mcpServersBlock,
|
|
599
|
+
permissions: permissionsBlock,
|
|
600
|
+
compaction: compactionBlock,
|
|
601
|
+
failure_taxonomy: failureTaxonomyBlock,
|
|
602
|
+
chains: chainsBlock,
|
|
603
|
+
wallets: walletsBlock,
|
|
604
|
+
contracts: contractsBlock,
|
|
605
|
+
transaction_policy: transactionPolicyBlock,
|
|
606
|
+
})
|
|
607
|
+
.strict();
|
|
608
|
+
// `.refine()` on a discriminatedUnion member would change the type from
|
|
609
|
+
// ZodObject to ZodEffects (incompatible with the union); the
|
|
610
|
+
// "entry-in-roles" + "non-empty roles" cross-field checks live in
|
|
611
|
+
// `parseSpec` below as a post-parse pass.
|
|
612
|
+
|
|
613
|
+
// Research target (Section 23 RES). The compiled daemon decomposes
|
|
614
|
+
// `goal` into `branchingFactor` sub-questions, runs one agent loop per
|
|
615
|
+
// branch, and writes a numbered-citation report under
|
|
616
|
+
// `.crewhaus/research/<runId>/`.
|
|
617
|
+
const researchRetrieveSchema = z
|
|
618
|
+
.object({
|
|
619
|
+
allowedOrigins: z.array(z.string().min(1)).default([]),
|
|
620
|
+
allowedFileRoots: z.array(z.string().min(1)).default([]),
|
|
621
|
+
vectorBackend: z.enum(["in-memory"]).optional(),
|
|
622
|
+
})
|
|
623
|
+
.strict();
|
|
624
|
+
|
|
625
|
+
const researchSchema = z
|
|
626
|
+
.object({
|
|
627
|
+
name: z.string().min(1),
|
|
628
|
+
target: z.literal("research"),
|
|
629
|
+
agent: z
|
|
630
|
+
.object({
|
|
631
|
+
model: z.string().min(1),
|
|
632
|
+
instructions: z.string().min(1),
|
|
633
|
+
})
|
|
634
|
+
.strict(),
|
|
635
|
+
goal: z.string().min(1),
|
|
636
|
+
branchingFactor: z.number().int().min(1).max(8).default(3),
|
|
637
|
+
maxDurationMs: z.number().int().positive().default(300_000),
|
|
638
|
+
retrieve: researchRetrieveSchema.default({}),
|
|
639
|
+
tools: z.array(z.string().min(1)).optional(),
|
|
640
|
+
tool_config: toolConfigBlock,
|
|
641
|
+
mcp_servers: mcpServersBlock,
|
|
642
|
+
permissions: permissionsBlock,
|
|
643
|
+
compaction: compactionBlock,
|
|
644
|
+
failure_taxonomy: failureTaxonomyBlock,
|
|
645
|
+
chains: chainsBlock,
|
|
646
|
+
wallets: walletsBlock,
|
|
647
|
+
contracts: contractsBlock,
|
|
648
|
+
transaction_policy: transactionPolicyBlock,
|
|
649
|
+
})
|
|
650
|
+
.strict();
|
|
651
|
+
|
|
652
|
+
// Batch target (Section 23 BATCH). Queue-worker daemon: pulls jobs
|
|
653
|
+
// from `queue`, runs the agent on each input, dedups via idempotency
|
|
654
|
+
// keys. v0 ships an in-memory adapter for tests + smoke; SQS / Redis
|
|
655
|
+
// Streams / Postgres adapters land in follow-up PRs.
|
|
656
|
+
const batchQueueSchema = z
|
|
657
|
+
.object({
|
|
658
|
+
adapter: z.enum(["in-memory", "sqs", "redis-streams", "postgres"]),
|
|
659
|
+
visibilityTimeoutMs: z.number().int().positive().default(30_000),
|
|
660
|
+
visibilityRenewIntervalMs: z.number().int().positive().optional(),
|
|
661
|
+
maxRetries: z.number().int().min(1).max(10).default(3),
|
|
662
|
+
seedJobs: z.array(z.string().min(1)).optional(),
|
|
663
|
+
})
|
|
664
|
+
.strict();
|
|
665
|
+
|
|
666
|
+
const batchSchema = z
|
|
667
|
+
.object({
|
|
668
|
+
name: z.string().min(1),
|
|
669
|
+
target: z.literal("batch"),
|
|
670
|
+
agent: z
|
|
671
|
+
.object({
|
|
672
|
+
model: z.string().min(1),
|
|
673
|
+
instructions: z.string().min(1),
|
|
674
|
+
})
|
|
675
|
+
.strict(),
|
|
676
|
+
queue: batchQueueSchema,
|
|
677
|
+
concurrency: z.number().int().min(1).max(64).default(4),
|
|
678
|
+
idempotencyWindowMs: z.number().int().positive().default(60_000),
|
|
679
|
+
tools: z.array(z.string().min(1)).optional(),
|
|
680
|
+
tool_config: toolConfigBlock,
|
|
681
|
+
mcp_servers: mcpServersBlock,
|
|
682
|
+
permissions: permissionsBlock,
|
|
683
|
+
compaction: compactionBlock,
|
|
684
|
+
failure_taxonomy: failureTaxonomyBlock,
|
|
685
|
+
chains: chainsBlock,
|
|
686
|
+
wallets: walletsBlock,
|
|
687
|
+
contracts: contractsBlock,
|
|
688
|
+
transaction_policy: transactionPolicyBlock,
|
|
689
|
+
})
|
|
690
|
+
.strict();
|
|
691
|
+
|
|
692
|
+
// Voice target (Section 24 VOICE). Realtime audio agent.
|
|
693
|
+
const voiceBlockSchema = z
|
|
694
|
+
.object({
|
|
695
|
+
provider: z.enum(["openai", "vapi"]),
|
|
696
|
+
voiceId: z.string().min(1).default("alloy"),
|
|
697
|
+
vad: z.enum(["server", "none"]).default("server"),
|
|
698
|
+
bargeInTriggerFrames: z.number().int().min(1).max(20).default(4),
|
|
699
|
+
bargeInWindowMs: z.number().int().min(60).max(2000).default(200),
|
|
700
|
+
})
|
|
701
|
+
.strict();
|
|
702
|
+
|
|
703
|
+
const voiceTelephonySchema = z
|
|
704
|
+
.object({
|
|
705
|
+
provider: z.enum(["twilio", "livekit-sip", "in-memory"]),
|
|
706
|
+
})
|
|
707
|
+
.strict();
|
|
708
|
+
|
|
709
|
+
const voiceSchema = z
|
|
710
|
+
.object({
|
|
711
|
+
name: z.string().min(1),
|
|
712
|
+
target: z.literal("voice"),
|
|
713
|
+
agent: z
|
|
714
|
+
.object({
|
|
715
|
+
model: z.string().min(1),
|
|
716
|
+
instructions: z.string().min(1),
|
|
717
|
+
})
|
|
718
|
+
.strict(),
|
|
719
|
+
voice: voiceBlockSchema,
|
|
720
|
+
telephony: voiceTelephonySchema.optional(),
|
|
721
|
+
tools: z.array(z.string().min(1)).optional(),
|
|
722
|
+
tool_config: toolConfigBlock,
|
|
723
|
+
mcp_servers: mcpServersBlock,
|
|
724
|
+
permissions: permissionsBlock,
|
|
725
|
+
compaction: compactionBlock,
|
|
726
|
+
failure_taxonomy: failureTaxonomyBlock,
|
|
727
|
+
})
|
|
728
|
+
.strict();
|
|
729
|
+
|
|
730
|
+
// Browser target (Section 25 BROW). Computer-use / browser-driver agent.
|
|
731
|
+
const browserDriverSchema = z
|
|
732
|
+
.object({
|
|
733
|
+
backend: z.enum(["host", "chromium", "remote"]).default("chromium"),
|
|
734
|
+
viewport: z
|
|
735
|
+
.object({
|
|
736
|
+
width: z.number().int().positive().default(1280),
|
|
737
|
+
height: z.number().int().positive().default(720),
|
|
738
|
+
})
|
|
739
|
+
.strict()
|
|
740
|
+
.default({ width: 1280, height: 720 }),
|
|
741
|
+
startUrl: z.string().url().optional(),
|
|
742
|
+
})
|
|
743
|
+
.strict();
|
|
744
|
+
|
|
745
|
+
const browserSchema = z
|
|
746
|
+
.object({
|
|
747
|
+
name: z.string().min(1),
|
|
748
|
+
target: z.literal("browser"),
|
|
749
|
+
agent: z
|
|
750
|
+
.object({
|
|
751
|
+
model: z.string().min(1),
|
|
752
|
+
instructions: z.string().min(1),
|
|
753
|
+
})
|
|
754
|
+
.strict(),
|
|
755
|
+
driver: browserDriverSchema.default({}),
|
|
756
|
+
/** Vision-grounding model. Defaults to the agent's primary model. */
|
|
757
|
+
groundingModel: z.string().min(1).optional(),
|
|
758
|
+
tools: z.array(z.string().min(1)).optional(),
|
|
759
|
+
tool_config: toolConfigBlock,
|
|
760
|
+
mcp_servers: mcpServersBlock,
|
|
761
|
+
permissions: permissionsBlock,
|
|
762
|
+
compaction: compactionBlock,
|
|
763
|
+
failure_taxonomy: failureTaxonomyBlock,
|
|
764
|
+
})
|
|
765
|
+
.strict();
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* Section 29 — `target: "eval"` — the EVAL target shape. A spec carries an
|
|
769
|
+
* agent definition, a dataset reference (resolved via §29 dataset-registry),
|
|
770
|
+
* a list of grader names (resolved via §29 grader-registry), concurrency
|
|
771
|
+
* and seed knobs. The compiled bundle boots dataset-registry +
|
|
772
|
+
* grader-registry + eval-runner and writes results to
|
|
773
|
+
* `.crewhaus/evals/<runId>/`.
|
|
774
|
+
*/
|
|
775
|
+
const evalSchema = z
|
|
776
|
+
.object({
|
|
777
|
+
name: z.string().min(1),
|
|
778
|
+
target: z.literal("eval"),
|
|
779
|
+
agent: z
|
|
780
|
+
.object({
|
|
781
|
+
model: z.string().min(1),
|
|
782
|
+
instructions: z.string().min(1),
|
|
783
|
+
tools: z.array(z.string().min(1)).optional(),
|
|
784
|
+
})
|
|
785
|
+
.strict(),
|
|
786
|
+
dataset: z
|
|
787
|
+
.object({
|
|
788
|
+
name: z.string().min(1),
|
|
789
|
+
version: z.string().min(1),
|
|
790
|
+
split: z.enum(["train", "dev", "test"]).default("dev"),
|
|
791
|
+
})
|
|
792
|
+
.strict(),
|
|
793
|
+
graders: z
|
|
794
|
+
.array(
|
|
795
|
+
z
|
|
796
|
+
.object({
|
|
797
|
+
name: z.string().min(1),
|
|
798
|
+
opts: z.record(z.unknown()).optional(),
|
|
799
|
+
})
|
|
800
|
+
.strict(),
|
|
801
|
+
)
|
|
802
|
+
.min(1),
|
|
803
|
+
concurrency: z.number().int().min(1).default(4),
|
|
804
|
+
seed: z.number().int().optional(),
|
|
805
|
+
failure_taxonomy: failureTaxonomyBlock,
|
|
806
|
+
})
|
|
807
|
+
.strict();
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* Section 47 — `onchain` target. Long-running event-driven daemon.
|
|
811
|
+
* Triggers fire on contract events / block scans / address watches;
|
|
812
|
+
* each trigger runs one agent turn with the decoded payload as the
|
|
813
|
+
* user message. Wallets + transaction_policy let the agent respond
|
|
814
|
+
* with signed transactions (escrow release, treasury rebalance, etc).
|
|
815
|
+
*/
|
|
816
|
+
const onchainTriggerSchema = z.discriminatedUnion("kind", [
|
|
817
|
+
z
|
|
818
|
+
.object({
|
|
819
|
+
kind: z.literal("event"),
|
|
820
|
+
chainId: z.string().min(1),
|
|
821
|
+
contract: z.string().min(1),
|
|
822
|
+
event: z.string().min(1),
|
|
823
|
+
filter: z.record(z.string(), z.unknown()).optional(),
|
|
824
|
+
})
|
|
825
|
+
.strict(),
|
|
826
|
+
z
|
|
827
|
+
.object({
|
|
828
|
+
kind: z.literal("block"),
|
|
829
|
+
chainId: z.string().min(1),
|
|
830
|
+
scanIntervalMs: z.number().int().min(1000).max(3_600_000),
|
|
831
|
+
})
|
|
832
|
+
.strict(),
|
|
833
|
+
z
|
|
834
|
+
.object({
|
|
835
|
+
kind: z.literal("address"),
|
|
836
|
+
chainId: z.string().min(1),
|
|
837
|
+
address: z.string().min(1),
|
|
838
|
+
direction: z.enum(["in", "out", "both"]).default("both"),
|
|
839
|
+
})
|
|
840
|
+
.strict(),
|
|
841
|
+
]);
|
|
842
|
+
|
|
843
|
+
const onchainSchema = z
|
|
844
|
+
.object({
|
|
845
|
+
name: z.string().min(1),
|
|
846
|
+
target: z.literal("onchain"),
|
|
847
|
+
agent: z
|
|
848
|
+
.object({
|
|
849
|
+
model: z.string().min(1),
|
|
850
|
+
instructions: z.string().min(1),
|
|
851
|
+
})
|
|
852
|
+
.strict(),
|
|
853
|
+
chains: z.array(chainBindingSchema).min(1),
|
|
854
|
+
wallets: z.array(walletBindingSchema).default([]),
|
|
855
|
+
contracts: z.array(contractBindingSchema).default([]),
|
|
856
|
+
transaction_policy: transactionPolicySchema.default({
|
|
857
|
+
defaultWriteApproval: "required",
|
|
858
|
+
allowedContracts: [],
|
|
859
|
+
simulationRequired: true,
|
|
860
|
+
}),
|
|
861
|
+
triggers: z.array(onchainTriggerSchema).min(1),
|
|
862
|
+
idempotencyWindowMs: z.number().int().positive().default(60_000),
|
|
863
|
+
tools: z.array(z.string().min(1)).optional(),
|
|
864
|
+
tool_config: toolConfigBlock,
|
|
865
|
+
mcp_servers: mcpServersBlock,
|
|
866
|
+
permissions: permissionsBlock,
|
|
867
|
+
compaction: compactionBlock,
|
|
868
|
+
failure_taxonomy: failureTaxonomyBlock,
|
|
869
|
+
})
|
|
870
|
+
.strict();
|
|
871
|
+
|
|
872
|
+
/**
|
|
873
|
+
* Section 47 — `onchain-game` target. Perceive-act-perceive loop
|
|
874
|
+
* against a game contract: read state via `stateReader`, ask the model
|
|
875
|
+
* for a move, broadcast it as a transaction, await confirmation,
|
|
876
|
+
* re-read state. Single chain, single wallet.
|
|
877
|
+
*/
|
|
878
|
+
const onchainGameSchema = z
|
|
879
|
+
.object({
|
|
880
|
+
name: z.string().min(1),
|
|
881
|
+
target: z.literal("onchain-game"),
|
|
882
|
+
agent: z
|
|
883
|
+
.object({
|
|
884
|
+
model: z.string().min(1),
|
|
885
|
+
instructions: z.string().min(1),
|
|
886
|
+
})
|
|
887
|
+
.strict(),
|
|
888
|
+
chain: chainBindingSchema,
|
|
889
|
+
wallet: walletBindingSchema,
|
|
890
|
+
game: z
|
|
891
|
+
.object({
|
|
892
|
+
contract: contractBindingSchema,
|
|
893
|
+
stateReader: z.string().min(1),
|
|
894
|
+
actionsContract: z.string().min(1).optional(),
|
|
895
|
+
turnSemantics: z.enum(["turn-based", "real-time", "async"]).default("turn-based"),
|
|
896
|
+
moveTimeoutMs: z.number().int().positive().optional(),
|
|
897
|
+
objective: z.string().min(1).optional(),
|
|
898
|
+
})
|
|
899
|
+
.strict(),
|
|
900
|
+
transaction_policy: transactionPolicySchema.default({
|
|
901
|
+
defaultWriteApproval: "required",
|
|
902
|
+
allowedContracts: [],
|
|
903
|
+
simulationRequired: true,
|
|
904
|
+
}),
|
|
905
|
+
tools: z.array(z.string().min(1)).optional(),
|
|
906
|
+
tool_config: toolConfigBlock,
|
|
907
|
+
mcp_servers: mcpServersBlock,
|
|
908
|
+
permissions: permissionsBlock,
|
|
909
|
+
compaction: compactionBlock,
|
|
910
|
+
failure_taxonomy: failureTaxonomyBlock,
|
|
911
|
+
})
|
|
912
|
+
.strict();
|
|
913
|
+
|
|
914
|
+
export const Spec = z.discriminatedUnion("target", [
|
|
915
|
+
cliSchema,
|
|
916
|
+
workflowSchema,
|
|
917
|
+
channelSchema,
|
|
918
|
+
graphSchema,
|
|
919
|
+
managedSchema,
|
|
920
|
+
pipelineSchema,
|
|
921
|
+
crewSchema,
|
|
922
|
+
researchSchema,
|
|
923
|
+
batchSchema,
|
|
924
|
+
voiceSchema,
|
|
925
|
+
browserSchema,
|
|
926
|
+
evalSchema,
|
|
927
|
+
onchainSchema,
|
|
928
|
+
onchainGameSchema,
|
|
929
|
+
]);
|
|
930
|
+
|
|
931
|
+
export type Spec = z.infer<typeof Spec>;
|
|
932
|
+
export type SpecCli = z.infer<typeof cliSchema>;
|
|
933
|
+
export type SpecWorkflow = z.infer<typeof workflowSchema>;
|
|
934
|
+
export type SpecWorkflowStep = z.infer<typeof workflowStepSchema>;
|
|
935
|
+
export type SpecChannel = z.infer<typeof channelSchema>;
|
|
936
|
+
export type SpecChannelAgent = z.infer<typeof channelAgentSchema>;
|
|
937
|
+
export type SpecSlackChannel = z.infer<typeof slackChannelSchema>;
|
|
938
|
+
export type SpecTelegramChannel = z.infer<typeof telegramChannelSchema>;
|
|
939
|
+
export type SpecDiscordChannel = z.infer<typeof discordChannelSchema>;
|
|
940
|
+
export type SpecWhatsAppChannel = z.infer<typeof whatsappChannelSchema>;
|
|
941
|
+
export type SpecIMessageChannel = z.infer<typeof imessageChannelSchema>;
|
|
942
|
+
export type SpecGraph = z.infer<typeof graphSchema>;
|
|
943
|
+
export type SpecGraphNode = z.infer<typeof graphNodeSchema>;
|
|
944
|
+
export type SpecGraphEdge = z.infer<typeof graphEdgeSchema>;
|
|
945
|
+
export type SpecManaged = z.infer<typeof managedSchema>;
|
|
946
|
+
export type SpecManagedTenant = z.infer<typeof managedTenantSchema>;
|
|
947
|
+
export type SpecPipeline = z.infer<typeof pipelineSchema>;
|
|
948
|
+
export type SpecPipelineDocument = z.infer<typeof pipelineDocumentSchema>;
|
|
949
|
+
export type SpecCrew = z.infer<typeof crewSchema>;
|
|
950
|
+
export type SpecCrewRole = z.infer<typeof crewRoleSchema>;
|
|
951
|
+
export type SpecCrewRouting = z.infer<typeof crewRoutingSchema>;
|
|
952
|
+
export type SpecResearch = z.infer<typeof researchSchema>;
|
|
953
|
+
export type SpecResearchRetrieve = z.infer<typeof researchRetrieveSchema>;
|
|
954
|
+
export type SpecBatch = z.infer<typeof batchSchema>;
|
|
955
|
+
export type SpecBatchQueue = z.infer<typeof batchQueueSchema>;
|
|
956
|
+
export type SpecOnchain = z.infer<typeof onchainSchema>;
|
|
957
|
+
export type SpecOnchainGame = z.infer<typeof onchainGameSchema>;
|
|
958
|
+
export type SpecChainTrigger = z.infer<typeof onchainTriggerSchema>;
|
|
959
|
+
export type SpecVoice = z.infer<typeof voiceSchema>;
|
|
960
|
+
export type SpecVoiceBlock = z.infer<typeof voiceBlockSchema>;
|
|
961
|
+
export type SpecVoiceTelephony = z.infer<typeof voiceTelephonySchema>;
|
|
962
|
+
export type SpecBrowser = z.infer<typeof browserSchema>;
|
|
963
|
+
export type SpecBrowserDriver = z.infer<typeof browserDriverSchema>;
|
|
964
|
+
export type SpecEval = z.infer<typeof evalSchema>;
|
|
965
|
+
export type SpecMcpServerConfig = z.infer<typeof mcpServerConfigSchema>;
|
|
966
|
+
export type SpecSubAgentDefinition = z.infer<typeof subAgentDefinitionSchema>;
|
|
967
|
+
export type SpecCompactionBlock = z.infer<typeof compactionBlock>;
|
|
968
|
+
export type SpecFailureTaxonomyEntry = z.infer<typeof failureTaxonomyEntrySchema>;
|
|
969
|
+
export type SpecFailureTaxonomy = z.infer<typeof failureTaxonomyBlock>;
|
|
970
|
+
|
|
971
|
+
export { SpecParseError };
|
|
972
|
+
|
|
973
|
+
export function parseSpec(yamlText: string): Spec {
|
|
974
|
+
let raw: unknown;
|
|
975
|
+
try {
|
|
976
|
+
raw = parseYaml(yamlText);
|
|
977
|
+
} catch (err) {
|
|
978
|
+
throw new SpecParseError("invalid YAML", err);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// Friendly early-rejection for `permissions.mode: bypass` so the error
|
|
982
|
+
// message names the actual security policy rather than a Zod enum mismatch.
|
|
983
|
+
// The Zod schema also excludes "bypass" from its enum (defense in depth).
|
|
984
|
+
if (typeof raw === "object" && raw !== null && "permissions" in raw) {
|
|
985
|
+
const perms = (raw as { permissions?: unknown }).permissions;
|
|
986
|
+
if (typeof perms === "object" && perms !== null && "mode" in perms) {
|
|
987
|
+
const mode = (perms as { mode?: unknown }).mode;
|
|
988
|
+
if (mode === "bypass") {
|
|
989
|
+
throw new SpecParseError(
|
|
990
|
+
"permissions.mode: bypass is rejected — bypass mode is only available via the --permission-mode CLI flag, never from a spec file",
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
const result = Spec.safeParse(raw);
|
|
997
|
+
if (!result.success) {
|
|
998
|
+
throw new SpecParseError(
|
|
999
|
+
`spec validation failed:\n${result.error.issues
|
|
1000
|
+
.map((i) => ` ${i.path.join(".") || "<root>"}: ${i.message}`)
|
|
1001
|
+
.join("\n")}`,
|
|
1002
|
+
result.error,
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
// Section 22 — crew cross-field invariants. Kept here rather than as
|
|
1006
|
+
// `.refine()`s on the schema so the discriminated-union member stays
|
|
1007
|
+
// a plain ZodObject (Zod's discriminatedUnion rejects ZodEffects).
|
|
1008
|
+
const data = result.data;
|
|
1009
|
+
if (data.target === "crew") {
|
|
1010
|
+
const roleNames = Object.keys(data.roles);
|
|
1011
|
+
if (roleNames.length === 0) {
|
|
1012
|
+
throw new SpecParseError("crew target requires at least one role");
|
|
1013
|
+
}
|
|
1014
|
+
if (!roleNames.includes(data.entry)) {
|
|
1015
|
+
throw new SpecParseError(
|
|
1016
|
+
`crew.entry "${data.entry}" must name one of crew.roles (got: ${roleNames.join(", ")})`,
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
if (data.routing !== undefined && data.routing.kind === "match" && data.routing.match) {
|
|
1020
|
+
for (const [from, rules] of Object.entries(data.routing.match)) {
|
|
1021
|
+
if (!roleNames.includes(from)) {
|
|
1022
|
+
throw new SpecParseError(`crew.routing.match["${from}"]: source role not in crew.roles`);
|
|
1023
|
+
}
|
|
1024
|
+
for (const rule of rules) {
|
|
1025
|
+
if (!roleNames.includes(rule.to)) {
|
|
1026
|
+
throw new SpecParseError(
|
|
1027
|
+
`crew.routing.match["${from}"].to = "${rule.to}" — target role not in crew.roles`,
|
|
1028
|
+
);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
return data;
|
|
1035
|
+
}
|