@zooid/core 0.7.0 → 0.7.2

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
@@ -101,6 +124,27 @@ interface ContainerConfig {
101
124
  interface ZooidContainerConfig {
102
125
  image?: string;
103
126
  }
127
+ /**
128
+ * A room binding for an agent. Either a bare alias (default PL) or
129
+ * an alias with a declared power level applied at room creation.
130
+ *
131
+ * `alias` may be an alias (`#room:server`) or a room ID (`!id:server`).
132
+ * Resolved to a canonical room ID by `bot-pool` at bootstrap. The
133
+ * declared `powerLevel` (when set) seeds the agent's entry in
134
+ * `m.room.power_levels.users` at room creation; the daemon never reads
135
+ * or modifies power levels after that — promote/demote in the UI is
136
+ * canonical.
137
+ */
138
+ interface RoomBinding {
139
+ /** Room alias (typical) or room ID. */
140
+ alias: string;
141
+ /**
142
+ * Power level the agent should hold in this room at the moment it is
143
+ * created. Omitted = `users_default` (effectively 0). Not reconciled
144
+ * after creation.
145
+ */
146
+ powerLevel?: number;
147
+ }
104
148
  /**
105
149
  * Matrix transport binding. Lives under `agents.<name>.matrix:` in
106
150
  * zooid.yaml. The block name (`matrix`) is the transport-kind
@@ -123,8 +167,8 @@ interface MatrixBinding {
123
167
  * profile on bootstrap. Falls back to the user_id localpart when absent.
124
168
  */
125
169
  display_name?: string;
126
- /** Room IDs / aliases this agent watches. */
127
- rooms: string[];
170
+ /** Rooms this agent watches. Each entry carries the alias/ID and an optional declared PL. */
171
+ rooms: RoomBinding[];
128
172
  /**
129
173
  * Routing rule. `mention` requires the bot to be tagged; `any` triggers
130
174
  * on every message.
@@ -181,7 +225,9 @@ interface AgentConfig {
181
225
  }
182
226
  /**
183
227
  * Matrix application-service transport. The CLI binds the AS HTTP listener
184
- * to `port` (defaults to 8080).
228
+ * to `port` (defaults to 9000 — the most common Matrix AS convention; see
229
+ * Synapse / mautrix / matrix-appservice-* projects). Must match the port in
230
+ * the registration YAML's `url` (read by the homeserver, not by Zooid).
185
231
  */
186
232
  interface MatrixTransportConfig {
187
233
  type: 'matrix';
@@ -207,8 +253,11 @@ interface MatrixTransportConfig {
207
253
  */
208
254
  user_namespace: string;
209
255
  /**
210
- * AS HTTP listener port.
211
- * @default 8080
256
+ * AS HTTP listener port. Must match the registration YAML's `url` port —
257
+ * Zooid never reads the registration, so a mismatch silently sinks every
258
+ * transaction (the homeserver gets connection refused; you see no error
259
+ * in Zooid's logs).
260
+ * @default 9000
212
261
  */
213
262
  port?: number;
214
263
  /**
@@ -254,7 +303,16 @@ interface CliFlags {
254
303
  image?: string;
255
304
  }
256
305
 
257
- declare function loadZooidConfig(yamlText: string): ZooidConfig;
306
+ interface LoadZooidConfigOptions {
307
+ /**
308
+ * Directory containing zooid.yaml. Required when any agent uses a
309
+ * relative `container.mounts[].host` path; resolution happens at parse
310
+ * time so the resulting `MountConfig` always carries an absolute host
311
+ * path.
312
+ */
313
+ configDir?: string;
314
+ }
315
+ declare function loadZooidConfig(yamlText: string, opts?: LoadZooidConfigOptions): ZooidConfig;
258
316
  declare function findTransport(cfg: ZooidConfig, name: string): TransportConfig | undefined;
259
317
  declare function findMatrixTransport(cfg: ZooidConfig): {
260
318
  name: string;
@@ -374,6 +432,21 @@ interface AcpAgentRegistryOptions {
374
432
  * transports without a context provider (e.g. HTTP) have no entry here.
375
433
  */
376
434
  contextSpawns?: Record<string, ContextSpawnFactory | undefined>;
435
+ /**
436
+ * Per-agent resolved bind-mount list. Threaded into the AcpClient's spawn
437
+ * spec; honoured by the docker runtime, ignored by the local runtime.
438
+ */
439
+ mounts?: Record<string, AcpMount[]>;
440
+ /**
441
+ * Per-agent list of host directories to `mkdir -p` before the first
442
+ * `runtime.spawn` for that agent. Subset of mount entries with `create: true`.
443
+ */
444
+ mkdirOnSpawn?: Record<string, string[]>;
445
+ /**
446
+ * Per-agent override for the spawn-spec `cwd`. Set to e.g. `/workspace`
447
+ * when the workspace mount is active; falls back to `agent.workdir`.
448
+ */
449
+ cwd?: Record<string, string>;
377
450
  }
378
451
  type ContextSpawnFactory = (threadId: string, channelId?: string) => Promise<{
379
452
  name: 'zooid-context';
@@ -394,6 +467,9 @@ declare class AcpAgentRegistry implements AcpRegistry {
394
467
  hasContextSpawn(name: string): boolean;
395
468
  resolveSpawnEnv(name: string): Record<string, string>;
396
469
  resolveSpawnImage(name: string): string | undefined;
470
+ resolveSpawnMounts(name: string): AcpMount[];
471
+ resolveSpawnCwd(name: string): string;
472
+ agentNames(): string[];
397
473
  getApprovalTimeoutMs(name: string): number;
398
474
  ensureSession(name: string, threadId: string, channelId?: string): Promise<string>;
399
475
  endSession(name: string, threadId: string): void;
@@ -488,4 +564,4 @@ interface TransportContextProvider {
488
564
  getChannelInfo(channelId: string): Promise<ChannelInfo>;
489
565
  }
490
566
 
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 };
567
+ 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 RoomBinding, 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) {
@@ -306,6 +432,39 @@ function parseTransport(name, raw, processEnv) {
306
432
  }
307
433
  return { type: "http", port };
308
434
  }
435
+ function parseRoomBinding(path, raw, serverName) {
436
+ function normalizeAlias(alias) {
437
+ if (alias.length === 0) {
438
+ throw new Error(`${path}: must be a non-empty alias`);
439
+ }
440
+ if (!MATRIX_ROOM_IDENT_RE.test(alias)) {
441
+ throw new Error(
442
+ `${path}: must start with '#' or '!' (got ${JSON.stringify(alias)})`
443
+ );
444
+ }
445
+ return alias.includes(":") ? alias : `${alias}:${serverName}`;
446
+ }
447
+ if (typeof raw === "string") {
448
+ return { alias: normalizeAlias(raw) };
449
+ }
450
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
451
+ throw new Error(`${path}: must be a string or { alias, power_level } object`);
452
+ }
453
+ const r = raw;
454
+ if (typeof r.alias !== "string" || r.alias.length === 0) {
455
+ throw new Error(`${path}.alias: must be a non-empty string`);
456
+ }
457
+ const out = { alias: normalizeAlias(r.alias) };
458
+ if (r.power_level !== void 0) {
459
+ if (typeof r.power_level !== "number" || !Number.isInteger(r.power_level)) {
460
+ throw new Error(
461
+ `${path}.power_level: must be an integer (got ${JSON.stringify(r.power_level)})`
462
+ );
463
+ }
464
+ out.powerLevel = r.power_level;
465
+ }
466
+ return out;
467
+ }
309
468
  function parseTransportBinding(name, entry, transports) {
310
469
  const present = TRANSPORT_KINDS.filter(
311
470
  (k) => entry[k] !== void 0 && entry[k] !== null
@@ -375,16 +534,8 @@ function parseTransportBinding(name, entry, transports) {
375
534
  throw new Error(`agents.${name}.matrix.rooms is required and must be a non-empty array`);
376
535
  }
377
536
  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}`);
537
+ for (let i = 0; i < block.rooms.length; i++) {
538
+ rooms.push(parseRoomBinding(`agents.${name}.matrix.rooms[${i}]`, block.rooms[i], serverName));
388
539
  }
389
540
  let displayName;
390
541
  if (block.display_name !== void 0) {
@@ -419,7 +570,7 @@ function parseTransportBinding(name, entry, transports) {
419
570
  }
420
571
  return { http: { transport: refName } };
421
572
  }
422
- function parseAgents(raw, runtime, transports, daemonHooks, processEnv) {
573
+ function parseAgents(raw, runtime, transports, daemonHooks, processEnv, configDir) {
423
574
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
424
575
  throw new Error("agents: must be a mapping");
425
576
  }
@@ -496,11 +647,18 @@ See [ZOD043].`
496
647
  let containerBlock;
497
648
  if (entry.container !== void 0 && entry.container !== null) {
498
649
  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
- );
650
+ if (typeof entry.container !== "object" || entry.container === null || Array.isArray(entry.container)) {
651
+ throw new Error(`agents.${name}.container must be a mapping`);
652
+ }
653
+ const c = entry.container;
654
+ const disallowed = Object.keys(c).filter((k) => k !== "mounts" && k !== "disable_mounts");
655
+ if (disallowed.length > 0) {
656
+ throw new Error(
657
+ `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.`
658
+ );
659
+ }
502
660
  }
503
- containerBlock = parseAgentContainer(name, entry.container, processEnv);
661
+ containerBlock = parseAgentContainer(name, entry.container, processEnv, configDir);
504
662
  }
505
663
  const binding = parseTransportBinding(name, entry, transports);
506
664
  const agentCfg = {
@@ -533,7 +691,7 @@ function zooidHooks(raw) {
533
691
  }
534
692
  return out;
535
693
  }
536
- function loadZooidConfig(yamlText) {
694
+ function loadZooidConfig(yamlText, opts = {}) {
537
695
  const raw = parse(yamlText) ?? {};
538
696
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
539
697
  throw new Error("zooid.yaml must be a YAML object");
@@ -566,7 +724,7 @@ function loadZooidConfig(yamlText) {
566
724
  const processEnv = process.env;
567
725
  const transports = parseTransports(r.transports, processEnv);
568
726
  const hooks = zooidHooks(r);
569
- const agents = parseAgents(r.agents, runtime, transports, hooks, processEnv);
727
+ const agents = parseAgents(r.agents, runtime, transports, hooks, processEnv, opts.configDir);
570
728
  const cfg = {
571
729
  runtime,
572
730
  transports,
@@ -647,6 +805,7 @@ function mergeCliFlags(base, flags) {
647
805
  }
648
806
 
649
807
  // src/acp-registry.ts
808
+ import { mkdirSync } from "fs";
650
809
  import { join as join2 } from "path";
651
810
  import {
652
811
  AcpClient,
@@ -689,6 +848,15 @@ var AcpAgentRegistry = class {
689
848
  resolveSpawnImage(name) {
690
849
  return this.opts.image?.[name];
691
850
  }
851
+ resolveSpawnMounts(name) {
852
+ return this.opts.mounts?.[name] ?? [];
853
+ }
854
+ resolveSpawnCwd(name) {
855
+ return this.opts.cwd?.[name] ?? this.opts.agents[name]?.workdir ?? process.cwd();
856
+ }
857
+ agentNames() {
858
+ return Object.keys(this.opts.agents);
859
+ }
692
860
  getApprovalTimeoutMs(name) {
693
861
  return this.opts.agents[name]?.approval_timeout_ms ?? 0;
694
862
  }
@@ -730,14 +898,18 @@ var AcpAgentRegistry = class {
730
898
  const cfg = this.opts.agents[name];
731
899
  if (!cfg.acp) throw new Error(`agents.${name}: missing acp block`);
732
900
  const spawn = resolveAcpAgentSpec(cfg.acp);
901
+ for (const dir of this.opts.mkdirOnSpawn?.[name] ?? []) {
902
+ mkdirSync(dir, { recursive: true });
903
+ }
733
904
  const client = new AcpClient({
734
905
  agent: {
735
906
  id: name,
736
907
  command: spawn.command,
737
908
  args: spawn.args,
738
909
  env: this.opts.env?.[name],
739
- cwd: cfg.workdir,
740
- image: this.opts.image?.[name]
910
+ cwd: this.resolveSpawnCwd(name),
911
+ image: this.opts.image?.[name],
912
+ mounts: this.resolveSpawnMounts(name)
741
913
  },
742
914
  agentDataDir: this.opts.agentsDir ? join2(this.opts.agentsDir, name) : void 0,
743
915
  runtime: this.opts.runtime,