@zooid/core 0.7.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/dist/index.js ADDED
@@ -0,0 +1,868 @@
1
+ // src/config.ts
2
+ import { existsSync } from "fs";
3
+ import { join } from "path";
4
+ import { parse } from "yaml";
5
+ import { isPreset } from "@zooid/acp-client";
6
+
7
+ // src/env-interpolation.ts
8
+ import dotenvExpand from "dotenv-expand";
9
+ var REF_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)(?::?[-+][^}]*)?\}|\$([A-Za-z_][A-Za-z0-9_]*)/g;
10
+ var EnvInterpolationError = class extends Error {
11
+ };
12
+ var isDenied = (name) => name === "ZOOID_TOKEN" || name.startsWith("ZOOID_");
13
+ function interpolateEnv(parsed, processEnv, scope) {
14
+ for (const [key, val] of Object.entries(parsed)) {
15
+ if (isDenied(key)) {
16
+ throw new EnvInterpolationError(
17
+ `${scope}.${key}: keys in the ZOOID_* namespace are not allowed`
18
+ );
19
+ }
20
+ if (typeof val !== "string") {
21
+ throw new EnvInterpolationError(
22
+ `${scope}.${key}: env values must be strings (got ${typeof val})`
23
+ );
24
+ }
25
+ REF_RE.lastIndex = 0;
26
+ let m;
27
+ while ((m = REF_RE.exec(val)) !== null) {
28
+ const ref = m[1] ?? m[2];
29
+ if (isDenied(ref)) {
30
+ throw new EnvInterpolationError(
31
+ `${scope}.${key}: references to ZOOID_* vars are not allowed (saw \${${ref}})`
32
+ );
33
+ }
34
+ }
35
+ }
36
+ const out = {};
37
+ for (const [key, val] of Object.entries(parsed)) {
38
+ out[key] = interpolateString(val, processEnv);
39
+ }
40
+ return out;
41
+ }
42
+ function interpolateString(value, processEnv) {
43
+ if (!value.includes("$")) return value;
44
+ const sentinel = "__zooid_interp_v1__";
45
+ const env = {};
46
+ for (const [k, v] of Object.entries(processEnv)) {
47
+ if (typeof v === "string") env[k] = v;
48
+ }
49
+ delete env[sentinel];
50
+ const result = dotenvExpand.expand({
51
+ parsed: { [sentinel]: value },
52
+ processEnv: env
53
+ });
54
+ return result.parsed?.[sentinel] ?? "";
55
+ }
56
+
57
+ // src/config.ts
58
+ var AGENT_NAME_RE = /^[a-z][a-z0-9-]{0,31}$/;
59
+ var MATRIX_USER_ID_RE = /^@[A-Za-z0-9._\-=/+]+:[A-Za-z0-9.\-]+$/;
60
+ var MATRIX_USER_LOCALPART_RE = /^@[a-z0-9._=/+\-]+$/;
61
+ var MATRIX_ROOM_IDENT_RE = /^[#!]/;
62
+ function deriveServerName(userNamespace) {
63
+ const tail = userNamespace.split(":").slice(1).join(":").replace(/\\?\)?$/, "");
64
+ if (!tail) throw new Error(`user_namespace missing server_name: ${userNamespace}`);
65
+ return tail;
66
+ }
67
+ var TRANSPORT_KINDS = ["matrix", "http"];
68
+ function parseAcpBlock(name, raw) {
69
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
70
+ throw new Error(`agents.${name}.acp: must be a mapping with either preset or command`);
71
+ }
72
+ const a = raw;
73
+ const hasPreset = a.preset !== void 0;
74
+ const hasCommand = a.command !== void 0;
75
+ if (hasPreset && hasCommand) {
76
+ throw new Error(
77
+ `agents.${name}.acp: specify either preset or command, not both`
78
+ );
79
+ }
80
+ if (!hasPreset && !hasCommand) {
81
+ throw new Error(
82
+ `agents.${name}.acp: must specify either preset or command`
83
+ );
84
+ }
85
+ if (hasPreset) {
86
+ if (typeof a.preset !== "string" || a.preset.length === 0) {
87
+ throw new Error(`agents.${name}.acp.preset: must be a non-empty string`);
88
+ }
89
+ if (!isPreset(a.preset)) {
90
+ throw new Error(
91
+ `agents.${name}.acp.preset: unknown preset "${a.preset}"`
92
+ );
93
+ }
94
+ const out = { preset: a.preset };
95
+ if (a.model !== void 0) {
96
+ if (typeof a.model !== "string" || a.model.trim().length === 0) {
97
+ throw new Error(`agents.${name}.acp.model: must be a non-empty string`);
98
+ }
99
+ out.model = a.model.trim();
100
+ }
101
+ return out;
102
+ }
103
+ if (typeof a.command !== "string" || a.command.length === 0) {
104
+ throw new Error(`agents.${name}.acp.command: must be a non-empty string`);
105
+ }
106
+ const args = [];
107
+ if (a.args !== void 0) {
108
+ if (!Array.isArray(a.args)) {
109
+ throw new Error(`agents.${name}.acp.args: must be an array of strings`);
110
+ }
111
+ for (const v of a.args) {
112
+ if (typeof v !== "string") {
113
+ throw new Error(`agents.${name}.acp.args[]: must be a string`);
114
+ }
115
+ args.push(v);
116
+ }
117
+ }
118
+ return { command: a.command, args };
119
+ }
120
+ function parseApprovalTimeout(name, raw) {
121
+ if (raw === void 0) return 0;
122
+ if (raw === 0 || raw === "0") return 0;
123
+ if (typeof raw !== "string") {
124
+ throw new Error(
125
+ `agents.${name}.approval_timeout: must be a duration like "1h", "15m", "30s", or 0 to disable (got ${JSON.stringify(raw)})`
126
+ );
127
+ }
128
+ const m = /^(\d+)(s|m|h)$/.exec(raw);
129
+ if (!m) {
130
+ throw new Error(
131
+ `agents.${name}.approval_timeout: "${raw}" is not a valid duration (use "<n>s", "<n>m", or "<n>h")`
132
+ );
133
+ }
134
+ const n = Number(m[1]);
135
+ switch (m[2]) {
136
+ case "s":
137
+ return n * 1e3;
138
+ case "m":
139
+ return n * 6e4;
140
+ case "h":
141
+ return n * 60 * 6e4;
142
+ }
143
+ throw new Error("unreachable");
144
+ }
145
+ function parseAgentContainer(name, raw, processEnv) {
146
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
147
+ throw new Error(`agents.${name}.container must be a mapping`);
148
+ }
149
+ const r = raw;
150
+ const out = {};
151
+ if (r.image !== void 0) {
152
+ if (typeof r.image !== "string" || r.image.length === 0) {
153
+ throw new Error(`agents.${name}.container.image must be a non-empty string`);
154
+ }
155
+ out.image = r.image;
156
+ }
157
+ if (r.env !== void 0 && r.env !== null) {
158
+ if (typeof r.env !== "object" || Array.isArray(r.env)) {
159
+ throw new Error(`agents.${name}.container.env must be a mapping`);
160
+ }
161
+ const rawEnv = r.env;
162
+ const stringEnv = {};
163
+ for (const [k, v] of Object.entries(rawEnv)) {
164
+ if (typeof v !== "string") {
165
+ throw new Error(
166
+ `agents.${name}.container.env.${k}: must be a string (got ${typeof v})`
167
+ );
168
+ }
169
+ stringEnv[k] = v;
170
+ }
171
+ out.env = interpolateEnv(stringEnv, processEnv, `agents.${name}.container.env`);
172
+ }
173
+ return out;
174
+ }
175
+ function parseZooidContainer(raw) {
176
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
177
+ throw new Error("container must be a mapping");
178
+ }
179
+ const r = raw;
180
+ const out = {};
181
+ if (r.env !== void 0) {
182
+ throw new Error(
183
+ "Top-level 'container.env' is not supported (workforce-level env defaults are out of scope; see [ZOD043]). Move env entries to per-agent container.env."
184
+ );
185
+ }
186
+ if (r.image !== void 0) {
187
+ if (typeof r.image !== "string" || r.image.length === 0) {
188
+ throw new Error("container.image must be a non-empty string");
189
+ }
190
+ out.image = r.image;
191
+ }
192
+ return out;
193
+ }
194
+ function parseTransports(raw, processEnv) {
195
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
196
+ throw new Error("transports: must be a mapping with at least one entry");
197
+ }
198
+ const r = raw;
199
+ const names = Object.keys(r);
200
+ if (names.length === 0) {
201
+ throw new Error("transports: at least one transport must be declared");
202
+ }
203
+ const out = {};
204
+ for (const name of names) {
205
+ out[name] = parseTransport(name, r[name], processEnv);
206
+ }
207
+ const matrixEntries = Object.entries(out).filter(
208
+ (e) => e[1].type === "matrix"
209
+ );
210
+ if (matrixEntries.length === 1) {
211
+ const [, mt] = matrixEntries[0];
212
+ if (mt.as_token === "__INFER__") {
213
+ const v = interpolateString("${MATRIX_AS_TOKEN}", processEnv);
214
+ if (v.length === 0) {
215
+ throw new Error(
216
+ "transports.matrix.as_token: env var MATRIX_AS_TOKEN is not set (set it in your shell or .env, or declare as_token explicitly in zooid.yaml)"
217
+ );
218
+ }
219
+ mt.as_token = v;
220
+ }
221
+ if (mt.hs_token === "__INFER__") {
222
+ const v = interpolateString("${MATRIX_HS_TOKEN}", processEnv);
223
+ if (v.length === 0) {
224
+ throw new Error(
225
+ "transports.matrix.hs_token: env var MATRIX_HS_TOKEN is not set (set it in your shell or .env, or declare hs_token explicitly in zooid.yaml)"
226
+ );
227
+ }
228
+ mt.hs_token = v;
229
+ }
230
+ } else if (matrixEntries.length > 1) {
231
+ for (const [tname, mt] of matrixEntries) {
232
+ if (mt.as_token === "__INFER__" || mt.hs_token === "__INFER__") {
233
+ throw new Error(
234
+ `transports.${tname}: as_token / hs_token must be set explicitly when more than one matrix transport is declared (no sensible default env var across multiple transports)`
235
+ );
236
+ }
237
+ }
238
+ }
239
+ return out;
240
+ }
241
+ function parseTransport(name, raw, processEnv) {
242
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
243
+ throw new Error(`transports.${name}: must be a mapping`);
244
+ }
245
+ const r = raw;
246
+ const inferredType = r.type ?? (name === "matrix" || name === "http" ? name : void 0);
247
+ if (inferredType !== "matrix" && inferredType !== "http") {
248
+ throw new Error(
249
+ `transports.${name}.type must be "matrix" or "http" (got ${JSON.stringify(r.type)})`
250
+ );
251
+ }
252
+ if (inferredType === "matrix") {
253
+ if (r.sender_localpart === void 0) r.sender_localpart = "zooid";
254
+ if (r.user_namespace === void 0 && typeof r.homeserver === "string") {
255
+ try {
256
+ const host = new URL(
257
+ interpolateString(r.homeserver, processEnv)
258
+ ).hostname;
259
+ if (host) r.user_namespace = `@.*:${host}`;
260
+ } catch {
261
+ }
262
+ }
263
+ if (r.as_token === void 0) r.as_token = "__INFER__";
264
+ if (r.hs_token === void 0) r.hs_token = "__INFER__";
265
+ const fields = [
266
+ "homeserver",
267
+ "as_token",
268
+ "hs_token",
269
+ "sender_localpart",
270
+ "user_namespace"
271
+ ];
272
+ for (const f of fields) {
273
+ if (typeof r[f] !== "string" || r[f].length === 0) {
274
+ throw new Error(`transports.${name}.${f} must be a non-empty string`);
275
+ }
276
+ }
277
+ const out = {
278
+ type: "matrix",
279
+ homeserver: interpolateString(r.homeserver, processEnv),
280
+ as_token: interpolateString(r.as_token, processEnv),
281
+ hs_token: interpolateString(r.hs_token, processEnv),
282
+ sender_localpart: r.sender_localpart,
283
+ user_namespace: r.user_namespace
284
+ };
285
+ if (r.port !== void 0) {
286
+ if (!Number.isInteger(r.port)) {
287
+ throw new Error(
288
+ `transports.${name}.port must be an integer (got ${JSON.stringify(r.port)})`
289
+ );
290
+ }
291
+ out.port = r.port;
292
+ }
293
+ if (r.space !== void 0) {
294
+ if (typeof r.space !== "string" || r.space.length === 0) {
295
+ throw new Error(
296
+ `transports.${name}.space must be a non-empty string (got ${JSON.stringify(r.space)})`
297
+ );
298
+ }
299
+ out.space = r.space;
300
+ }
301
+ return out;
302
+ }
303
+ const port = r.port ?? 8080;
304
+ if (!Number.isInteger(port)) {
305
+ throw new Error(`transports.${name}.port must be an integer (got ${JSON.stringify(port)})`);
306
+ }
307
+ return { type: "http", port };
308
+ }
309
+ function parseTransportBinding(name, entry, transports) {
310
+ const present = TRANSPORT_KINDS.filter(
311
+ (k) => entry[k] !== void 0 && entry[k] !== null
312
+ );
313
+ if (present.length === 0) {
314
+ throw new Error(
315
+ `agents.${name}: must declare exactly one transport-kind block (e.g. 'matrix:' or 'http:'). Saw none.`
316
+ );
317
+ }
318
+ if (present.length > 1) {
319
+ throw new Error(
320
+ `agents.${name}: must declare exactly one transport-kind block. Saw: ${present.join(", ")}. To run "the same agent" on two transports, declare two agents (e.g. ${name}-matrix and ${name}-http).`
321
+ );
322
+ }
323
+ const kind = present[0];
324
+ const blockRaw = entry[kind];
325
+ if (typeof blockRaw !== "object" || blockRaw === null || Array.isArray(blockRaw)) {
326
+ throw new Error(`agents.${name}.${kind}: must be a mapping`);
327
+ }
328
+ const block = blockRaw;
329
+ let refName;
330
+ if (typeof block.transport === "string" && block.transport.length > 0) {
331
+ refName = block.transport;
332
+ } else {
333
+ const matches = Object.entries(transports).filter(
334
+ ([, t]) => t.type === kind
335
+ );
336
+ if (matches.length === 0) {
337
+ throw new Error(
338
+ `agents.${name}.${kind}: no transport of type ${kind} declared (add one under transports:)`
339
+ );
340
+ }
341
+ if (matches.length > 1) {
342
+ throw new Error(
343
+ `agents.${name}.${kind}.transport is required when more than one ${kind} transport is declared (saw: ${matches.map(([n]) => n).join(", ")})`
344
+ );
345
+ }
346
+ refName = matches[0][0];
347
+ }
348
+ const refTransport = transports[refName];
349
+ if (!refTransport) {
350
+ throw new Error(
351
+ `agents.${name}.${kind}.transport "${refName}" is not declared in transports`
352
+ );
353
+ }
354
+ if (refTransport.type !== kind) {
355
+ throw new Error(
356
+ `agents.${name}.${kind} references transport "${refName}" of type: ${refTransport.type}. Block name and referenced transport's type must match.`
357
+ );
358
+ }
359
+ if (kind === "matrix") {
360
+ if (refTransport.type !== "matrix") {
361
+ throw new Error(`agents.${name}.matrix: transport must be matrix`);
362
+ }
363
+ const serverName = deriveServerName(refTransport.user_namespace);
364
+ const rawUserId = typeof block.user_id === "string" && block.user_id.length > 0 ? block.user_id : `@${name}`;
365
+ let userId = rawUserId;
366
+ if (!userId.includes(":") && MATRIX_USER_LOCALPART_RE.test(userId)) {
367
+ userId = `${userId}:${serverName}`;
368
+ }
369
+ if (!MATRIX_USER_ID_RE.test(userId)) {
370
+ throw new Error(
371
+ `agents.${name}.matrix.user_id must look like @localpart:server (got ${JSON.stringify(block.user_id)})`
372
+ );
373
+ }
374
+ if (!Array.isArray(block.rooms) || block.rooms.length === 0) {
375
+ throw new Error(`agents.${name}.matrix.rooms is required and must be a non-empty array`);
376
+ }
377
+ const rooms = [];
378
+ for (const r of block.rooms) {
379
+ if (typeof r !== "string" || r.length === 0) {
380
+ throw new Error(`agents.${name}.matrix.rooms[] must be a non-empty string`);
381
+ }
382
+ if (!MATRIX_ROOM_IDENT_RE.test(r)) {
383
+ throw new Error(
384
+ `agents.${name}.matrix.rooms[] must start with '#' or '!' (got ${JSON.stringify(r)})`
385
+ );
386
+ }
387
+ rooms.push(r.includes(":") ? r : `${r}:${serverName}`);
388
+ }
389
+ let displayName;
390
+ if (block.display_name !== void 0) {
391
+ if (typeof block.display_name !== "string") {
392
+ throw new Error(
393
+ `agents.${name}.matrix.display_name must be a string (got ${JSON.stringify(block.display_name)})`
394
+ );
395
+ }
396
+ const trimmed = block.display_name.trim();
397
+ if (trimmed.length === 0) {
398
+ throw new Error(`agents.${name}.matrix.display_name must be non-empty after trim`);
399
+ }
400
+ if (trimmed.length > 256) {
401
+ throw new Error(`agents.${name}.matrix.display_name must be 256 characters or fewer`);
402
+ }
403
+ displayName = trimmed;
404
+ }
405
+ const tr = block.trigger ?? "mention";
406
+ if (tr !== "mention" && tr !== "any") {
407
+ throw new Error(
408
+ `agents.${name}.matrix.trigger must be "mention" or "any" (got ${JSON.stringify(tr)})`
409
+ );
410
+ }
411
+ const matrix = {
412
+ transport: refName,
413
+ user_id: userId,
414
+ rooms,
415
+ trigger: tr
416
+ };
417
+ if (displayName !== void 0) matrix.display_name = displayName;
418
+ return { matrix };
419
+ }
420
+ return { http: { transport: refName } };
421
+ }
422
+ function parseAgents(raw, runtime, transports, daemonHooks, processEnv) {
423
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
424
+ throw new Error("agents: must be a mapping");
425
+ }
426
+ const entries = Object.entries(raw);
427
+ if (entries.length === 0) {
428
+ throw new Error("agents: must have at least one entry");
429
+ }
430
+ const result = {};
431
+ for (const [name, val] of entries) {
432
+ if (!AGENT_NAME_RE.test(name)) {
433
+ throw new Error(`agents.${name}: name must match /^[a-z][a-z0-9-]{0,31}$/`);
434
+ }
435
+ if (!val || typeof val !== "object" || Array.isArray(val)) {
436
+ throw new Error(`agents.${name} must be a mapping`);
437
+ }
438
+ const entry = val;
439
+ let workdir;
440
+ if (entry.workdir === void 0) {
441
+ workdir = `./agents/${name}`;
442
+ } else if (typeof entry.workdir !== "string" || entry.workdir.length === 0) {
443
+ throw new Error(`agents.${name}.workdir must be a non-empty string`);
444
+ } else {
445
+ workdir = entry.workdir;
446
+ }
447
+ if (entry.adapter !== void 0) {
448
+ throw new Error(
449
+ `agents.${name}: "adapter" is no longer supported; use "acp" \u2014 see epics/003-ZOD025-acp-migration/SPEC.md`
450
+ );
451
+ }
452
+ if (entry.acp === void 0) {
453
+ throw new Error(`agents.${name}: missing required "acp" block`);
454
+ }
455
+ const acp = parseAcpBlock(name, entry.acp);
456
+ const approval_timeout_ms = parseApprovalTimeout(name, entry.approval_timeout);
457
+ if (entry.docker !== void 0) {
458
+ throw new Error(
459
+ `agents.${name}.docker is no longer supported. Move 'image' to agents.${name}.container.image, and 'forward_env' entries to agents.${name}.container.env with \${VAR} interpolation. See [ZOD043].`
460
+ );
461
+ }
462
+ if (typeof entry.transport === "string") {
463
+ throw new Error(
464
+ `agents.${name}.transport (string) is no longer supported at the agent level. Move it inside a transport-kind block, e.g.:
465
+ matrix:
466
+ transport: <name>
467
+ user_id: "@..."
468
+ rooms: [...]
469
+ See [ZOD043].`
470
+ );
471
+ }
472
+ for (const k of ["matrix_user_id", "rooms", "trigger"]) {
473
+ if (entry[k] !== void 0) {
474
+ throw new Error(
475
+ `agents.${name}.${k} is no longer supported as a flat field. Move it inside a 'matrix:' block on the agent. See [ZOD043].`
476
+ );
477
+ }
478
+ }
479
+ const agentHooks = {};
480
+ if (daemonHooks.pre_turn !== void 0) agentHooks.pre_turn = daemonHooks.pre_turn;
481
+ if (daemonHooks.post_turn !== void 0) agentHooks.post_turn = daemonHooks.post_turn;
482
+ if (entry.hooks !== void 0 && entry.hooks !== null) {
483
+ if (typeof entry.hooks !== "object" || Array.isArray(entry.hooks)) {
484
+ throw new Error(`agents.${name}.hooks must be a mapping`);
485
+ }
486
+ const h = entry.hooks;
487
+ if (Object.prototype.hasOwnProperty.call(h, "pre_turn")) {
488
+ if (typeof h.pre_turn === "string") agentHooks.pre_turn = h.pre_turn;
489
+ else delete agentHooks.pre_turn;
490
+ }
491
+ if (Object.prototype.hasOwnProperty.call(h, "post_turn")) {
492
+ if (typeof h.post_turn === "string") agentHooks.post_turn = h.post_turn;
493
+ else delete agentHooks.post_turn;
494
+ }
495
+ }
496
+ let containerBlock;
497
+ if (entry.container !== void 0 && entry.container !== null) {
498
+ if (runtime === "local") {
499
+ throw new Error(
500
+ `agents.${name}.container is only valid when runtime is 'docker' or 'podman'. runtime: local spawns agents as host child processes \u2014 there is no container, so 'image' is inert and 'env' would silently lie (the agent inherits the daemon's full process.env regardless).`
501
+ );
502
+ }
503
+ containerBlock = parseAgentContainer(name, entry.container, processEnv);
504
+ }
505
+ const binding = parseTransportBinding(name, entry, transports);
506
+ const agentCfg = {
507
+ name,
508
+ workdir,
509
+ hooks: agentHooks,
510
+ acp,
511
+ approval_timeout_ms
512
+ };
513
+ if (containerBlock) agentCfg.container = containerBlock;
514
+ if (binding.matrix) agentCfg.matrix = binding.matrix;
515
+ if (binding.http) agentCfg.http = binding.http;
516
+ result[name] = agentCfg;
517
+ }
518
+ return result;
519
+ }
520
+ function parseRuntime(raw) {
521
+ const runtime = raw ?? "docker";
522
+ if (runtime !== "local" && runtime !== "docker" && runtime !== "podman") {
523
+ throw new Error(`runtime must be "local", "docker", or "podman" (got "${runtime}")`);
524
+ }
525
+ return runtime;
526
+ }
527
+ function zooidHooks(raw) {
528
+ const out = {};
529
+ if (raw.hooks && typeof raw.hooks === "object") {
530
+ const h = raw.hooks;
531
+ if (typeof h.pre_turn === "string") out.pre_turn = h.pre_turn;
532
+ if (typeof h.post_turn === "string") out.post_turn = h.post_turn;
533
+ }
534
+ return out;
535
+ }
536
+ function loadZooidConfig(yamlText) {
537
+ const raw = parse(yamlText) ?? {};
538
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
539
+ throw new Error("zooid.yaml must be a YAML object");
540
+ }
541
+ const r = raw;
542
+ if (r.transport !== void 0) {
543
+ throw new Error(
544
+ 'zooid.yaml: top-level "transport:" is no longer supported; declare entries under "transports:" instead'
545
+ );
546
+ }
547
+ if (r.matrix !== void 0) {
548
+ throw new Error(
549
+ 'zooid.yaml: top-level "matrix:" is no longer supported; move it under "transports.<name>: { type: matrix, ... }"'
550
+ );
551
+ }
552
+ if (r.workdir !== void 0) {
553
+ throw new Error(
554
+ "top-level workdir is not supported; define agents: { <name>: { workdir: ... } } instead"
555
+ );
556
+ }
557
+ if (r.docker !== void 0) {
558
+ throw new Error(
559
+ "Top-level 'docker' block is no longer supported. Move 'image' to top-level 'container.image'. See [ZOD043]."
560
+ );
561
+ }
562
+ if (r.agents === void 0) {
563
+ throw new Error("agents: is required \u2014 zooid.yaml must define at least one agent");
564
+ }
565
+ const runtime = parseRuntime(r.runtime);
566
+ const processEnv = process.env;
567
+ const transports = parseTransports(r.transports, processEnv);
568
+ const hooks = zooidHooks(r);
569
+ const agents = parseAgents(r.agents, runtime, transports, hooks, processEnv);
570
+ const cfg = {
571
+ runtime,
572
+ transports,
573
+ agents,
574
+ hooks
575
+ };
576
+ if (r.container !== void 0 && r.container !== null) {
577
+ if (runtime === "local") {
578
+ throw new Error(
579
+ "container is only valid when runtime is 'docker' or 'podman'. runtime: local does not run agents in containers; image is ignored. See [ZOD043]."
580
+ );
581
+ }
582
+ cfg.container = parseZooidContainer(r.container);
583
+ }
584
+ return cfg;
585
+ }
586
+ function findTransport(cfg, name) {
587
+ return cfg.transports[name];
588
+ }
589
+ function findMatrixTransport(cfg) {
590
+ const matrices = Object.entries(cfg.transports).filter(
591
+ (e) => e[1].type === "matrix"
592
+ );
593
+ if (matrices.length === 0) return null;
594
+ if (matrices.length > 1) {
595
+ throw new Error(
596
+ `findMatrixTransport: multiple matrix transports declared (${matrices.map((m) => m[0]).join(", ")}). Per-agent matrix routing is not supported yet.`
597
+ );
598
+ }
599
+ const [name, transport] = matrices[0];
600
+ return { name, transport };
601
+ }
602
+ function findHttpTransport(cfg) {
603
+ const https = Object.entries(cfg.transports).filter(
604
+ (e) => e[1].type === "http"
605
+ );
606
+ if (https.length === 0) return null;
607
+ if (https.length > 1) {
608
+ throw new Error(
609
+ `findHttpTransport: multiple http transports declared (${https.map((h) => h[0]).join(", ")}). Per-agent http routing is not supported yet.`
610
+ );
611
+ }
612
+ const [name, transport] = https[0];
613
+ return { name, transport };
614
+ }
615
+ function findConfigFile(cwd) {
616
+ const z = join(cwd, "zooid.yaml");
617
+ if (existsSync(z)) return { path: z };
618
+ const legacy = join(cwd, "workforce.yaml");
619
+ if (existsSync(legacy)) {
620
+ throw new Error(
621
+ `workforce.yaml is no longer supported. Rename it to zooid.yaml. See [ZOD045].`
622
+ );
623
+ }
624
+ return null;
625
+ }
626
+ function mergeCliFlags(base, flags) {
627
+ const runtimeFlag = flags.runtime;
628
+ if (runtimeFlag !== void 0 && runtimeFlag !== "local" && runtimeFlag !== "docker" && runtimeFlag !== "podman") {
629
+ throw new Error(`runtime must be "local", "docker", or "podman" (got "${flags.runtime}")`);
630
+ }
631
+ const runtime = runtimeFlag ?? base.runtime;
632
+ const merged = {
633
+ runtime,
634
+ transports: base.transports,
635
+ agents: base.agents,
636
+ hooks: { ...base.hooks }
637
+ };
638
+ if (runtime === "docker" || runtime === "podman") {
639
+ const image = flags.image ?? base.container?.image;
640
+ if (image !== void 0) {
641
+ merged.container = { image };
642
+ } else if (base.container !== void 0) {
643
+ merged.container = { ...base.container };
644
+ }
645
+ }
646
+ return merged;
647
+ }
648
+
649
+ // src/acp-registry.ts
650
+ import { join as join2 } from "path";
651
+ import {
652
+ AcpClient,
653
+ resolvePreset
654
+ } from "@zooid/acp-client";
655
+ var AcpAgentRegistry = class {
656
+ opts;
657
+ clients = /* @__PURE__ */ new Map();
658
+ onEvent;
659
+ onApprovalRequest;
660
+ constructor(opts) {
661
+ this.opts = opts;
662
+ this.onEvent = opts.onEvent ?? (() => {
663
+ });
664
+ if (opts.onApprovalRequest) {
665
+ this.onApprovalRequest = opts.onApprovalRequest;
666
+ } else if (opts.approvals) {
667
+ const correlator = opts.approvals;
668
+ this.onApprovalRequest = async (name, req) => {
669
+ const cfg = this.opts.agents[name];
670
+ const handle = correlator.register(name, req.sessionId, req, {
671
+ timeoutMs: cfg?.approval_timeout_ms ?? 0
672
+ });
673
+ this.opts.onApprovalRegistered?.(handle);
674
+ return handle.decisionPromise;
675
+ };
676
+ } else {
677
+ this.onApprovalRequest = async () => ({ decision: "cancel" });
678
+ }
679
+ }
680
+ hasAgent(name) {
681
+ return Object.prototype.hasOwnProperty.call(this.opts.agents, name);
682
+ }
683
+ hasContextSpawn(name) {
684
+ return Boolean(this.opts.contextSpawns?.[name]);
685
+ }
686
+ resolveSpawnEnv(name) {
687
+ return this.opts.env?.[name] ?? {};
688
+ }
689
+ resolveSpawnImage(name) {
690
+ return this.opts.image?.[name];
691
+ }
692
+ getApprovalTimeoutMs(name) {
693
+ return this.opts.agents[name]?.approval_timeout_ms ?? 0;
694
+ }
695
+ async ensureSession(name, threadId, channelId) {
696
+ if (!this.hasAgent(name)) throw new Error(`unknown agent: ${name}`);
697
+ const client = await this.ensureClient(name);
698
+ return client.ensureSession(threadId, channelId);
699
+ }
700
+ endSession(name, threadId) {
701
+ if (!this.hasAgent(name)) return;
702
+ const client = this.clients.get(name);
703
+ client?.endSession(threadId);
704
+ }
705
+ async cancelSession(name, sessionId) {
706
+ if (!this.hasAgent(name)) return;
707
+ const client = this.clients.get(name);
708
+ this.opts.approvals?.cancelSession(sessionId);
709
+ if (!client) return;
710
+ try {
711
+ await client.cancel(sessionId);
712
+ } catch (err) {
713
+ console.warn(`[acp:${name}] cancel(${sessionId}) failed:`, err);
714
+ }
715
+ }
716
+ async prompt(name, input) {
717
+ if (!this.hasAgent(name)) throw new Error(`unknown agent: ${name}`);
718
+ const client = await this.ensureClient(name);
719
+ return client.prompt(input);
720
+ }
721
+ async stopAll() {
722
+ await Promise.allSettled(
723
+ [...this.clients.values()].map((c) => c.stop())
724
+ );
725
+ this.clients.clear();
726
+ }
727
+ async ensureClient(name) {
728
+ const existing = this.clients.get(name);
729
+ if (existing) return existing;
730
+ const cfg = this.opts.agents[name];
731
+ if (!cfg.acp) throw new Error(`agents.${name}: missing acp block`);
732
+ const spawn = resolveAcpAgentSpec(cfg.acp);
733
+ const client = new AcpClient({
734
+ agent: {
735
+ id: name,
736
+ command: spawn.command,
737
+ args: spawn.args,
738
+ env: this.opts.env?.[name],
739
+ cwd: cfg.workdir,
740
+ image: this.opts.image?.[name]
741
+ },
742
+ agentDataDir: this.opts.agentsDir ? join2(this.opts.agentsDir, name) : void 0,
743
+ runtime: this.opts.runtime,
744
+ onEvent: (e) => this.onEvent(name, e),
745
+ onApprovalRequest: (req) => this.onApprovalRequest(name, req),
746
+ onTap: this.opts.onTap ? (e) => this.opts.onTap(name, e) : void 0,
747
+ contextSpawn: this.opts.contextSpawns?.[name]
748
+ });
749
+ await client.start();
750
+ this.clients.set(name, client);
751
+ return client;
752
+ }
753
+ };
754
+ function resolveAcpAgentSpec(spec) {
755
+ if ("preset" in spec && spec.preset) {
756
+ return resolvePreset(spec.preset, { model: spec.model });
757
+ }
758
+ if ("command" in spec && spec.command) {
759
+ return { command: spec.command, args: spec.args ?? [] };
760
+ }
761
+ throw new Error("AcpAgentSpec: must specify either preset or command");
762
+ }
763
+
764
+ // src/approval-correlator.ts
765
+ import { EventEmitter } from "events";
766
+ import { randomUUID } from "crypto";
767
+ var ApprovalCorrelator = class extends EventEmitter {
768
+ pending = /* @__PURE__ */ new Map();
769
+ bySession = /* @__PURE__ */ new Map();
770
+ register(agentName, sessionId, req, opts = {}) {
771
+ const approvalId = randomUUID();
772
+ let resolve;
773
+ const decisionPromise = new Promise((r) => {
774
+ resolve = r;
775
+ });
776
+ const entry = {
777
+ approvalId,
778
+ agentName,
779
+ sessionId,
780
+ toolCallId: req.toolCallId,
781
+ toolKind: req.toolKind,
782
+ toolTitle: req.toolTitle,
783
+ toolInput: req.toolInput,
784
+ options: req.options,
785
+ decisionPromise,
786
+ resolve
787
+ };
788
+ if (opts.timeoutMs && opts.timeoutMs > 0) {
789
+ entry.timer = setTimeout(() => {
790
+ if (this.pending.get(approvalId) !== entry) return;
791
+ entry.resolve({ decision: "cancel" });
792
+ this.pending.delete(approvalId);
793
+ this.bySession.get(sessionId)?.delete(approvalId);
794
+ this.emit("timeout", { approvalId, sessionId, agentName });
795
+ }, opts.timeoutMs);
796
+ entry.timer.unref?.();
797
+ }
798
+ this.pending.set(approvalId, entry);
799
+ let set = this.bySession.get(sessionId);
800
+ if (!set) {
801
+ set = /* @__PURE__ */ new Set();
802
+ this.bySession.set(sessionId, set);
803
+ }
804
+ set.add(approvalId);
805
+ this.emit("registered", this.toPublic(entry));
806
+ return this.toPublic(entry);
807
+ }
808
+ resolve(sessionId, approvalId, decision) {
809
+ const entry = this.pending.get(approvalId);
810
+ if (!entry || entry.sessionId !== sessionId) return false;
811
+ if (entry.timer) clearTimeout(entry.timer);
812
+ entry.resolve(decision);
813
+ this.pending.delete(approvalId);
814
+ this.bySession.get(sessionId)?.delete(approvalId);
815
+ return true;
816
+ }
817
+ cancelSession(sessionId) {
818
+ const ids = this.bySession.get(sessionId);
819
+ if (!ids) return;
820
+ for (const id of [...ids]) {
821
+ const entry = this.pending.get(id);
822
+ if (entry) {
823
+ if (entry.timer) clearTimeout(entry.timer);
824
+ entry.resolve({ decision: "cancel" });
825
+ this.pending.delete(id);
826
+ }
827
+ }
828
+ this.bySession.delete(sessionId);
829
+ }
830
+ listPending(sessionId) {
831
+ const ids = this.bySession.get(sessionId);
832
+ if (!ids) return [];
833
+ const out = [];
834
+ for (const id of ids) {
835
+ const entry = this.pending.get(id);
836
+ if (entry) out.push(this.toPublic(entry));
837
+ }
838
+ return out;
839
+ }
840
+ size() {
841
+ return this.pending.size;
842
+ }
843
+ toPublic(entry) {
844
+ return {
845
+ approvalId: entry.approvalId,
846
+ agentName: entry.agentName,
847
+ sessionId: entry.sessionId,
848
+ toolCallId: entry.toolCallId,
849
+ toolKind: entry.toolKind,
850
+ toolTitle: entry.toolTitle,
851
+ toolInput: entry.toolInput,
852
+ options: entry.options,
853
+ decisionPromise: entry.decisionPromise
854
+ };
855
+ }
856
+ };
857
+ export {
858
+ AcpAgentRegistry,
859
+ ApprovalCorrelator,
860
+ findConfigFile,
861
+ findHttpTransport,
862
+ findMatrixTransport,
863
+ findTransport,
864
+ loadZooidConfig,
865
+ mergeCliFlags,
866
+ resolveAcpAgentSpec
867
+ };
868
+ //# sourceMappingURL=index.js.map