@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 +53 -3
- package/dist/index.js +158 -11
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/acp-registry.ts +38 -2
- package/src/config.ts +177 -10
- package/src/container-mounts.test.ts +292 -0
- package/src/index.ts +2 -0
- package/src/types.ts +25 -1
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.
|
|
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
|
-
|
|
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
|
-
|
|
500
|
-
`agents.${name}.container
|
|
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:
|
|
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,
|