@zooid/core 0.7.0 → 0.7.1

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.d.ts CHANGED
@@ -80,9 +80,25 @@ interface Transport {
80
80
  listen(channel: string, onMessage: (msg: InboundMessage) => void): void;
81
81
  reply(thread: ThreadRef, message: string): Promise<void> | void;
82
82
  }
83
+ /**
84
+ * One bind mount. `id` is the handle used by `disable_mounts` to subtract
85
+ * a zooid- or preset-declared entry; user-declared entries that omit `id`
86
+ * are auto-assigned `user-N`. The reserved id `workspace` is rejected on
87
+ * user entries.
88
+ */
89
+ interface MountConfig {
90
+ id?: string;
91
+ host: string;
92
+ target: string;
93
+ mode: 'ro' | 'rw';
94
+ /** mkdir -p the host path before bind-mounting. Default false. */
95
+ create?: boolean;
96
+ }
83
97
  /**
84
98
  * Per-agent container configuration. Holds runtime-neutral container
85
- * concerns — image, env. Rejected at parse time when `runtime: local`.
99
+ * concerns — image, env, mounts. `image` / `env` are rejected at parse time
100
+ * when `runtime: local`; `mounts` / `disable_mounts` are accepted under
101
+ * `runtime: local` but ignored at compose time.
86
102
  */
87
103
  interface ContainerConfig {
88
104
  image?: string;
@@ -93,6 +109,13 @@ interface ContainerConfig {
93
109
  * `ZOOID_*` references and `ZOOID_*` keys are rejected.
94
110
  */
95
111
  env?: Record<string, string>;
112
+ /** User-declared mounts, layered on top of workspace + preset. */
113
+ mounts?: MountConfig[];
114
+ /**
115
+ * Subtractive override by mount id. Built-in ids: `workspace` plus
116
+ * whatever each preset declares (canonical: `memory`, `history`, `config`).
117
+ */
118
+ disable_mounts?: string[];
96
119
  }
97
120
  /**
98
121
  * Workforce-level container defaults. Currently `image` only — see
@@ -254,7 +277,16 @@ interface CliFlags {
254
277
  image?: string;
255
278
  }
256
279
 
257
- declare function loadZooidConfig(yamlText: string): ZooidConfig;
280
+ interface LoadZooidConfigOptions {
281
+ /**
282
+ * Directory containing zooid.yaml. Required when any agent uses a
283
+ * relative `container.mounts[].host` path; resolution happens at parse
284
+ * time so the resulting `MountConfig` always carries an absolute host
285
+ * path.
286
+ */
287
+ configDir?: string;
288
+ }
289
+ declare function loadZooidConfig(yamlText: string, opts?: LoadZooidConfigOptions): ZooidConfig;
258
290
  declare function findTransport(cfg: ZooidConfig, name: string): TransportConfig | undefined;
259
291
  declare function findMatrixTransport(cfg: ZooidConfig): {
260
292
  name: string;
@@ -374,6 +406,21 @@ interface AcpAgentRegistryOptions {
374
406
  * transports without a context provider (e.g. HTTP) have no entry here.
375
407
  */
376
408
  contextSpawns?: Record<string, ContextSpawnFactory | undefined>;
409
+ /**
410
+ * Per-agent resolved bind-mount list. Threaded into the AcpClient's spawn
411
+ * spec; honoured by the docker runtime, ignored by the local runtime.
412
+ */
413
+ mounts?: Record<string, AcpMount[]>;
414
+ /**
415
+ * Per-agent list of host directories to `mkdir -p` before the first
416
+ * `runtime.spawn` for that agent. Subset of mount entries with `create: true`.
417
+ */
418
+ mkdirOnSpawn?: Record<string, string[]>;
419
+ /**
420
+ * Per-agent override for the spawn-spec `cwd`. Set to e.g. `/workspace`
421
+ * when the workspace mount is active; falls back to `agent.workdir`.
422
+ */
423
+ cwd?: Record<string, string>;
377
424
  }
378
425
  type ContextSpawnFactory = (threadId: string, channelId?: string) => Promise<{
379
426
  name: 'zooid-context';
@@ -394,6 +441,9 @@ declare class AcpAgentRegistry implements AcpRegistry {
394
441
  hasContextSpawn(name: string): boolean;
395
442
  resolveSpawnEnv(name: string): Record<string, string>;
396
443
  resolveSpawnImage(name: string): string | undefined;
444
+ resolveSpawnMounts(name: string): AcpMount[];
445
+ resolveSpawnCwd(name: string): string;
446
+ agentNames(): string[];
397
447
  getApprovalTimeoutMs(name: string): number;
398
448
  ensureSession(name: string, threadId: string, channelId?: string): Promise<string>;
399
449
  endSession(name: string, threadId: string): void;
@@ -488,4 +538,4 @@ interface TransportContextProvider {
488
538
  getChannelInfo(channelId: string): Promise<ChannelInfo>;
489
539
  }
490
540
 
491
- export { AcpAgentRegistry, type AcpAgentRegistryOptions, type AcpAgentSpec, type AcpMount, type AcpRegistry, type AcpRegistryApprovalHandler, type AcpRegistryEventHandler, type AcpRuntime, type AcpSpawnSpec, type AgentConfig, ApprovalCorrelator, type ChannelInfo, type CliFlags, type ContainerConfig, type ContextSpawnFactory, type HistoryOptions, type HistoryPage, type HttpBinding, type HttpTransportConfig, type InboundMessage, type MatrixBinding, type MatrixTransportConfig, type Member, type Message, type RegisterOptions, type RegisteredApproval, type ThreadOverview, type ThreadOverviewPage, type ThreadRef, type Transport, type TransportConfig, type TransportContextProvider, type ZooidConfig, type ZooidContainerConfig, findConfigFile, findHttpTransport, findMatrixTransport, findTransport, loadZooidConfig, mergeCliFlags, resolveAcpAgentSpec };
541
+ export { AcpAgentRegistry, type AcpAgentRegistryOptions, type AcpAgentSpec, type AcpMount, type AcpRegistry, type AcpRegistryApprovalHandler, type AcpRegistryEventHandler, type AcpRuntime, type AcpSpawnSpec, type AgentConfig, ApprovalCorrelator, type ChannelInfo, type CliFlags, type ContainerConfig, type ContextSpawnFactory, type HistoryOptions, type HistoryPage, type HttpBinding, type HttpTransportConfig, type InboundMessage, type LoadZooidConfigOptions, type MatrixBinding, type MatrixTransportConfig, type Member, type Message, type MountConfig, type RegisterOptions, type RegisteredApproval, type ThreadOverview, type ThreadOverviewPage, type ThreadRef, type Transport, type TransportConfig, type TransportContextProvider, type ZooidConfig, type ZooidContainerConfig, findConfigFile, findHttpTransport, findMatrixTransport, findTransport, loadZooidConfig, mergeCliFlags, resolveAcpAgentSpec };
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // src/config.ts
2
2
  import { existsSync } from "fs";
3
- import { join } from "path";
3
+ import { isAbsolute, join, resolve as pathResolve } from "path";
4
4
  import { parse } from "yaml";
5
5
  import { isPreset } from "@zooid/acp-client";
6
6
 
@@ -142,7 +142,7 @@ function parseApprovalTimeout(name, raw) {
142
142
  }
143
143
  throw new Error("unreachable");
144
144
  }
145
- function parseAgentContainer(name, raw, processEnv) {
145
+ function parseAgentContainer(name, raw, processEnv, configDir) {
146
146
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
147
147
  throw new Error(`agents.${name}.container must be a mapping`);
148
148
  }
@@ -170,6 +170,132 @@ function parseAgentContainer(name, raw, processEnv) {
170
170
  }
171
171
  out.env = interpolateEnv(stringEnv, processEnv, `agents.${name}.container.env`);
172
172
  }
173
+ if (r.mounts !== void 0) {
174
+ out.mounts = parseMountList(name, r.mounts, processEnv, configDir);
175
+ }
176
+ if (r.disable_mounts !== void 0) {
177
+ out.disable_mounts = parseDisableMounts(name, r.disable_mounts);
178
+ }
179
+ return out;
180
+ }
181
+ function parseMountList(agentName, raw, processEnv, configDir) {
182
+ if (!Array.isArray(raw)) {
183
+ throw new Error(`agents.${agentName}.container.mounts must be an array`);
184
+ }
185
+ const out = [];
186
+ const seenIds = /* @__PURE__ */ new Set();
187
+ for (let i = 0; i < raw.length; i++) {
188
+ const entry = raw[i];
189
+ if (typeof entry !== "object" || entry === null || Array.isArray(entry)) {
190
+ throw new Error(`agents.${agentName}.container.mounts[${i}] must be a mapping`);
191
+ }
192
+ const e = entry;
193
+ if (e.host === void 0) {
194
+ throw new Error(`agents.${agentName}.container.mounts[${i}].host is required`);
195
+ }
196
+ if (e.target === void 0) {
197
+ throw new Error(`agents.${agentName}.container.mounts[${i}].target is required`);
198
+ }
199
+ if (typeof e.host !== "string" || e.host.length === 0) {
200
+ throw new Error(
201
+ `agents.${agentName}.container.mounts[${i}].host must be a non-empty string`
202
+ );
203
+ }
204
+ if (typeof e.target !== "string" || e.target.length === 0) {
205
+ throw new Error(
206
+ `agents.${agentName}.container.mounts[${i}].target must be a non-empty string`
207
+ );
208
+ }
209
+ const mode = e.mode ?? "rw";
210
+ if (mode !== "ro" && mode !== "rw") {
211
+ throw new Error(
212
+ `agents.${agentName}.container.mounts[${i}].mode must be "ro" or "rw" (got ${JSON.stringify(e.mode)})`
213
+ );
214
+ }
215
+ let id;
216
+ if (e.id !== void 0) {
217
+ if (typeof e.id !== "string" || e.id.length === 0) {
218
+ throw new Error(
219
+ `agents.${agentName}.container.mounts[${i}].id must be a non-empty string`
220
+ );
221
+ }
222
+ if (e.id === "workspace") {
223
+ throw new Error(
224
+ `agents.${agentName}.container.mounts[${i}].id: "workspace" is a reserved id (set by the workspace auto-mount). Use a different id or rely on disable_mounts to subtract.`
225
+ );
226
+ }
227
+ if (seenIds.has(e.id)) {
228
+ throw new Error(
229
+ `agents.${agentName}.container.mounts: duplicate id "${e.id}"`
230
+ );
231
+ }
232
+ seenIds.add(e.id);
233
+ id = e.id;
234
+ }
235
+ let create;
236
+ if (e.create !== void 0) {
237
+ if (typeof e.create !== "boolean") {
238
+ throw new Error(
239
+ `agents.${agentName}.container.mounts[${i}].create must be a boolean`
240
+ );
241
+ }
242
+ create = e.create;
243
+ }
244
+ const host = resolveHostPath(
245
+ agentName,
246
+ i,
247
+ interpolateString(e.host, processEnv),
248
+ configDir
249
+ );
250
+ const target = interpolateString(e.target, processEnv);
251
+ const m = { host, target, mode };
252
+ if (id !== void 0) m.id = id;
253
+ if (create !== void 0) m.create = create;
254
+ out.push(m);
255
+ }
256
+ return out;
257
+ }
258
+ function resolveHostPath(agentName, index, host, configDir) {
259
+ if (host.startsWith("~/")) {
260
+ const home = process.env.HOME;
261
+ if (!home) {
262
+ throw new Error(
263
+ `agents.${agentName}.container.mounts[${index}].host: cannot expand ~ \u2014 $HOME is not set`
264
+ );
265
+ }
266
+ return `${home}/${host.slice(2)}`;
267
+ }
268
+ if (host === "~") {
269
+ const home = process.env.HOME;
270
+ if (!home) {
271
+ throw new Error(
272
+ `agents.${agentName}.container.mounts[${index}].host: cannot expand ~ \u2014 $HOME is not set`
273
+ );
274
+ }
275
+ return home;
276
+ }
277
+ if (isAbsolute(host)) return host;
278
+ if (!configDir) {
279
+ throw new Error(
280
+ `agents.${agentName}.container.mounts[${index}]: relative host path "${host}" requires configDir (zooid.yaml directory) \u2014 pass it via loadZooidConfig(yaml, { configDir })`
281
+ );
282
+ }
283
+ return pathResolve(configDir, host);
284
+ }
285
+ function parseDisableMounts(agentName, raw) {
286
+ if (!Array.isArray(raw)) {
287
+ throw new Error(`agents.${agentName}.container.disable_mounts must be an array of strings`);
288
+ }
289
+ const out = [];
290
+ for (let i = 0; i < raw.length; i++) {
291
+ const v = raw[i];
292
+ if (typeof v !== "string" || v.length === 0) {
293
+ throw new Error(
294
+ `agents.${agentName}.container.disable_mounts[${i}] must be a non-empty string`
295
+ );
296
+ }
297
+ out.push(v);
298
+ }
173
299
  return out;
174
300
  }
175
301
  function parseZooidContainer(raw) {
@@ -419,7 +545,7 @@ function parseTransportBinding(name, entry, transports) {
419
545
  }
420
546
  return { http: { transport: refName } };
421
547
  }
422
- function parseAgents(raw, runtime, transports, daemonHooks, processEnv) {
548
+ function parseAgents(raw, runtime, transports, daemonHooks, processEnv, configDir) {
423
549
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
424
550
  throw new Error("agents: must be a mapping");
425
551
  }
@@ -496,11 +622,18 @@ See [ZOD043].`
496
622
  let containerBlock;
497
623
  if (entry.container !== void 0 && entry.container !== null) {
498
624
  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
- );
625
+ if (typeof entry.container !== "object" || entry.container === null || Array.isArray(entry.container)) {
626
+ throw new Error(`agents.${name}.container must be a mapping`);
627
+ }
628
+ const c = entry.container;
629
+ const disallowed = Object.keys(c).filter((k) => k !== "mounts" && k !== "disable_mounts");
630
+ if (disallowed.length > 0) {
631
+ throw new Error(
632
+ `agents.${name}.container.${disallowed[0]} 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). 'mounts' and 'disable_mounts' are accepted under runtime: local but ignored at compose time.`
633
+ );
634
+ }
502
635
  }
503
- containerBlock = parseAgentContainer(name, entry.container, processEnv);
636
+ containerBlock = parseAgentContainer(name, entry.container, processEnv, configDir);
504
637
  }
505
638
  const binding = parseTransportBinding(name, entry, transports);
506
639
  const agentCfg = {
@@ -533,7 +666,7 @@ function zooidHooks(raw) {
533
666
  }
534
667
  return out;
535
668
  }
536
- function loadZooidConfig(yamlText) {
669
+ function loadZooidConfig(yamlText, opts = {}) {
537
670
  const raw = parse(yamlText) ?? {};
538
671
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
539
672
  throw new Error("zooid.yaml must be a YAML object");
@@ -566,7 +699,7 @@ function loadZooidConfig(yamlText) {
566
699
  const processEnv = process.env;
567
700
  const transports = parseTransports(r.transports, processEnv);
568
701
  const hooks = zooidHooks(r);
569
- const agents = parseAgents(r.agents, runtime, transports, hooks, processEnv);
702
+ const agents = parseAgents(r.agents, runtime, transports, hooks, processEnv, opts.configDir);
570
703
  const cfg = {
571
704
  runtime,
572
705
  transports,
@@ -647,6 +780,7 @@ function mergeCliFlags(base, flags) {
647
780
  }
648
781
 
649
782
  // src/acp-registry.ts
783
+ import { mkdirSync } from "fs";
650
784
  import { join as join2 } from "path";
651
785
  import {
652
786
  AcpClient,
@@ -689,6 +823,15 @@ var AcpAgentRegistry = class {
689
823
  resolveSpawnImage(name) {
690
824
  return this.opts.image?.[name];
691
825
  }
826
+ resolveSpawnMounts(name) {
827
+ return this.opts.mounts?.[name] ?? [];
828
+ }
829
+ resolveSpawnCwd(name) {
830
+ return this.opts.cwd?.[name] ?? this.opts.agents[name]?.workdir ?? process.cwd();
831
+ }
832
+ agentNames() {
833
+ return Object.keys(this.opts.agents);
834
+ }
692
835
  getApprovalTimeoutMs(name) {
693
836
  return this.opts.agents[name]?.approval_timeout_ms ?? 0;
694
837
  }
@@ -730,14 +873,18 @@ var AcpAgentRegistry = class {
730
873
  const cfg = this.opts.agents[name];
731
874
  if (!cfg.acp) throw new Error(`agents.${name}: missing acp block`);
732
875
  const spawn = resolveAcpAgentSpec(cfg.acp);
876
+ for (const dir of this.opts.mkdirOnSpawn?.[name] ?? []) {
877
+ mkdirSync(dir, { recursive: true });
878
+ }
733
879
  const client = new AcpClient({
734
880
  agent: {
735
881
  id: name,
736
882
  command: spawn.command,
737
883
  args: spawn.args,
738
884
  env: this.opts.env?.[name],
739
- cwd: cfg.workdir,
740
- image: this.opts.image?.[name]
885
+ cwd: this.resolveSpawnCwd(name),
886
+ image: this.opts.image?.[name],
887
+ mounts: this.resolveSpawnMounts(name)
741
888
  },
742
889
  agentDataDir: this.opts.agentsDir ? join2(this.opts.agentsDir, name) : void 0,
743
890
  runtime: this.opts.runtime,