@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/LICENSE +21 -0
- package/dist/index.d.ts +491 -0
- package/dist/index.js +868 -0
- package/dist/index.js.map +1 -0
- package/package.json +44 -0
- package/src/__fixtures__/ec2-workspace.yaml +16 -0
- package/src/__fixtures__/opencode-vertex-gemini.yaml +34 -0
- package/src/__fixtures__/triage-agent.yaml +40 -0
- package/src/__fixtures__/zooid-dev.yaml +53 -0
- package/src/acp-config.test.ts +162 -0
- package/src/acp-registry.test.ts +217 -0
- package/src/acp-registry.ts +235 -0
- package/src/acp-types.test.ts +55 -0
- package/src/acp-types.ts +48 -0
- package/src/approval-correlator.test.ts +129 -0
- package/src/approval-correlator.ts +143 -0
- package/src/config-file.test.ts +35 -0
- package/src/config.test.ts +1317 -0
- package/src/config.ts +712 -0
- package/src/env-interpolation.ts +90 -0
- package/src/example-yaml.test.ts +35 -0
- package/src/index.ts +56 -0
- package/src/transport-context.test.ts +34 -0
- package/src/transport-context.ts +91 -0
- package/src/types.ts +213 -0
- package/src/zooid-config.test.ts +164 -0
- package/src/zooid-helpers.test.ts +54 -0
- package/src/zooid-yaml-sweep.test.ts +389 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { findMatrixTransport, findTransport } from './config.js'
|
|
3
|
+
import type { ZooidConfig } from './types.js'
|
|
4
|
+
|
|
5
|
+
const cfg: ZooidConfig = {
|
|
6
|
+
runtime: 'docker',
|
|
7
|
+
transports: {
|
|
8
|
+
'matrix-local': {
|
|
9
|
+
type: 'matrix',
|
|
10
|
+
homeserver: 'http://localhost:8448',
|
|
11
|
+
as_token: 'as-x',
|
|
12
|
+
hs_token: 'hs-x',
|
|
13
|
+
sender_localpart: 'zooid',
|
|
14
|
+
user_namespace: '@.*:localhost',
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
agents: {},
|
|
18
|
+
hooks: {},
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
describe('findTransport / findMatrixTransport', () => {
|
|
22
|
+
it('findTransport returns the named transport', () => {
|
|
23
|
+
expect(findTransport(cfg, 'matrix-local')?.type).toBe('matrix')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('findTransport returns undefined for unknown names', () => {
|
|
27
|
+
expect(findTransport(cfg, 'ghost')).toBeUndefined()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('findMatrixTransport returns the (single) matrix transport', () => {
|
|
31
|
+
const t = findMatrixTransport(cfg)
|
|
32
|
+
expect(t?.transport.type).toBe('matrix')
|
|
33
|
+
expect(t?.name).toBe('matrix-local')
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('findMatrixTransport returns null when none exists', () => {
|
|
37
|
+
const httpOnly: ZooidConfig = {
|
|
38
|
+
...cfg,
|
|
39
|
+
transports: { 'http-only': { type: 'http', port: 8080 } },
|
|
40
|
+
}
|
|
41
|
+
expect(findMatrixTransport(httpOnly)).toBeNull()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('findMatrixTransport throws when more than one matrix transport exists', () => {
|
|
45
|
+
const dual: ZooidConfig = {
|
|
46
|
+
...cfg,
|
|
47
|
+
transports: {
|
|
48
|
+
a: cfg.transports['matrix-local']!,
|
|
49
|
+
b: cfg.transports['matrix-local']!,
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
expect(() => findMatrixTransport(dual)).toThrow(/multiple matrix transports/i)
|
|
53
|
+
})
|
|
54
|
+
})
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
5
|
+
import { loadZooidConfig } from './config.js'
|
|
6
|
+
|
|
7
|
+
const HERE = resolve(fileURLToPath(import.meta.url), '..')
|
|
8
|
+
|
|
9
|
+
const baseTransports = `
|
|
10
|
+
transports:
|
|
11
|
+
m1:
|
|
12
|
+
type: matrix
|
|
13
|
+
homeserver: http://localhost:8448
|
|
14
|
+
as_token: t
|
|
15
|
+
hs_token: h
|
|
16
|
+
sender_localpart: z
|
|
17
|
+
user_namespace: '@.*:localhost'
|
|
18
|
+
`
|
|
19
|
+
|
|
20
|
+
const matrixAgent = (extra = ''): string => `
|
|
21
|
+
alice:
|
|
22
|
+
workdir: ./alice
|
|
23
|
+
acp: { preset: claude }
|
|
24
|
+
matrix:
|
|
25
|
+
transport: m1
|
|
26
|
+
user_id: '@alice:localhost'
|
|
27
|
+
rooms: ['!r:localhost']${extra}
|
|
28
|
+
`
|
|
29
|
+
|
|
30
|
+
describe('container: block', () => {
|
|
31
|
+
it('accepts workforce-level container.image as the default', () => {
|
|
32
|
+
const yaml = `
|
|
33
|
+
runtime: docker
|
|
34
|
+
container:
|
|
35
|
+
image: zooid-agent-base:latest
|
|
36
|
+
${baseTransports}
|
|
37
|
+
agents:${matrixAgent()}
|
|
38
|
+
`
|
|
39
|
+
const cfg = loadZooidConfig(yaml)
|
|
40
|
+
expect(cfg.container?.image).toBe('zooid-agent-base:latest')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('accepts per-agent container.image overriding the workforce default', () => {
|
|
44
|
+
const yaml = `
|
|
45
|
+
runtime: docker
|
|
46
|
+
container:
|
|
47
|
+
image: base:1
|
|
48
|
+
${baseTransports}
|
|
49
|
+
agents:${matrixAgent(`
|
|
50
|
+
container:
|
|
51
|
+
image: alice:2`)}
|
|
52
|
+
`
|
|
53
|
+
const cfg = loadZooidConfig(yaml)
|
|
54
|
+
expect(cfg.agents.alice!.container?.image).toBe('alice:2')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('rejects container at workforce level when runtime: local', () => {
|
|
58
|
+
const yaml = `
|
|
59
|
+
runtime: local
|
|
60
|
+
container: { image: x:1 }
|
|
61
|
+
${baseTransports}
|
|
62
|
+
agents:${matrixAgent()}
|
|
63
|
+
`
|
|
64
|
+
expect(() => loadZooidConfig(yaml)).toThrow(
|
|
65
|
+
/container.*only valid when runtime is 'docker' or 'podman'/i,
|
|
66
|
+
)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('rejects per-agent container under runtime: local', () => {
|
|
70
|
+
const yaml = `
|
|
71
|
+
runtime: local
|
|
72
|
+
${baseTransports}
|
|
73
|
+
agents:${matrixAgent(`
|
|
74
|
+
container:
|
|
75
|
+
image: x:1`)}
|
|
76
|
+
`
|
|
77
|
+
expect(() => loadZooidConfig(yaml)).toThrow(
|
|
78
|
+
/alice\.container.*only valid when runtime is 'docker' or 'podman'/i,
|
|
79
|
+
)
|
|
80
|
+
})
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
describe('container.env interpolation', () => {
|
|
84
|
+
let saved: NodeJS.ProcessEnv
|
|
85
|
+
beforeEach(() => {
|
|
86
|
+
saved = { ...process.env }
|
|
87
|
+
})
|
|
88
|
+
afterEach(() => {
|
|
89
|
+
for (const k of Object.keys(process.env)) delete process.env[k]
|
|
90
|
+
Object.assign(process.env, saved)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('passes literal values through unchanged', () => {
|
|
94
|
+
const yaml = `
|
|
95
|
+
runtime: docker
|
|
96
|
+
${baseTransports}
|
|
97
|
+
agents:${matrixAgent(`
|
|
98
|
+
container:
|
|
99
|
+
env:
|
|
100
|
+
LOG_LEVEL: info`)}
|
|
101
|
+
`
|
|
102
|
+
const cfg = loadZooidConfig(yaml)
|
|
103
|
+
expect(cfg.agents.alice!.container?.env).toEqual({ LOG_LEVEL: 'info' })
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('expands ${VAR} from process.env', () => {
|
|
107
|
+
process.env.ANTHROPIC_API_KEY = 'sk-test'
|
|
108
|
+
const yaml = `
|
|
109
|
+
runtime: docker
|
|
110
|
+
${baseTransports}
|
|
111
|
+
agents:${matrixAgent(`
|
|
112
|
+
container:
|
|
113
|
+
env:
|
|
114
|
+
ANTHROPIC_API_KEY: \${ANTHROPIC_API_KEY}`)}
|
|
115
|
+
`
|
|
116
|
+
const cfg = loadZooidConfig(yaml)
|
|
117
|
+
expect(cfg.agents.alice!.container?.env?.ANTHROPIC_API_KEY).toBe('sk-test')
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('honours ${VAR:-default} when the var is unset', () => {
|
|
121
|
+
delete process.env.MODEL
|
|
122
|
+
const yaml = `
|
|
123
|
+
runtime: docker
|
|
124
|
+
${baseTransports}
|
|
125
|
+
agents:${matrixAgent(`
|
|
126
|
+
container:
|
|
127
|
+
env:
|
|
128
|
+
MODEL: \${MODEL:-claude-opus-4-7}`)}
|
|
129
|
+
`
|
|
130
|
+
const cfg = loadZooidConfig(yaml)
|
|
131
|
+
expect(cfg.agents.alice!.container?.env?.MODEL).toBe('claude-opus-4-7')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('resolves missing references to empty string (compose parity)', () => {
|
|
135
|
+
delete process.env.ABSENT
|
|
136
|
+
const yaml = `
|
|
137
|
+
runtime: docker
|
|
138
|
+
${baseTransports}
|
|
139
|
+
agents:${matrixAgent(`
|
|
140
|
+
container:
|
|
141
|
+
env:
|
|
142
|
+
K: \${ABSENT}`)}
|
|
143
|
+
`
|
|
144
|
+
const cfg = loadZooidConfig(yaml)
|
|
145
|
+
expect(cfg.agents.alice!.container?.env?.K).toBe('')
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('rejects ${ZOOID_TOKEN} reference', () => {
|
|
149
|
+
process.env.ZOOID_TOKEN = 'leak'
|
|
150
|
+
const yaml = `
|
|
151
|
+
runtime: docker
|
|
152
|
+
${baseTransports}
|
|
153
|
+
agents:${matrixAgent(`
|
|
154
|
+
container:
|
|
155
|
+
env:
|
|
156
|
+
TOKEN: \${ZOOID_TOKEN}`)}
|
|
157
|
+
`
|
|
158
|
+
expect(() => loadZooidConfig(yaml)).toThrow(/ZOOID_/i)
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('rejects ${ZOOID_X} reference inside a composed value', () => {
|
|
162
|
+
process.env.ZOOID_INTERNAL = 'leak'
|
|
163
|
+
const yaml = `
|
|
164
|
+
runtime: docker
|
|
165
|
+
${baseTransports}
|
|
166
|
+
agents:${matrixAgent(`
|
|
167
|
+
container:
|
|
168
|
+
env:
|
|
169
|
+
K: 'prefix-\${ZOOID_INTERNAL}'`)}
|
|
170
|
+
`
|
|
171
|
+
expect(() => loadZooidConfig(yaml)).toThrow(/ZOOID_/i)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('rejects a key in the ZOOID_* namespace', () => {
|
|
175
|
+
const yaml = `
|
|
176
|
+
runtime: docker
|
|
177
|
+
${baseTransports}
|
|
178
|
+
agents:${matrixAgent(`
|
|
179
|
+
container:
|
|
180
|
+
env:
|
|
181
|
+
ZOOID_FOO: literal`)}
|
|
182
|
+
`
|
|
183
|
+
expect(() => loadZooidConfig(yaml)).toThrow(/ZOOID_/i)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('rejects non-string env values (numbers, booleans)', () => {
|
|
187
|
+
const yaml = `
|
|
188
|
+
runtime: docker
|
|
189
|
+
${baseTransports}
|
|
190
|
+
agents:${matrixAgent(`
|
|
191
|
+
container:
|
|
192
|
+
env:
|
|
193
|
+
PORT: 8080`)}
|
|
194
|
+
`
|
|
195
|
+
expect(() => loadZooidConfig(yaml)).toThrow(/string/i)
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
describe('agent transport-kind block', () => {
|
|
200
|
+
it('parses matrix block with all fields', () => {
|
|
201
|
+
const yaml = `
|
|
202
|
+
runtime: docker
|
|
203
|
+
${baseTransports}
|
|
204
|
+
agents:
|
|
205
|
+
alice:
|
|
206
|
+
workdir: ./alice
|
|
207
|
+
acp: { preset: claude }
|
|
208
|
+
matrix:
|
|
209
|
+
transport: m1
|
|
210
|
+
user_id: '@alice:localhost'
|
|
211
|
+
rooms: ['!r:localhost']
|
|
212
|
+
trigger: any
|
|
213
|
+
`
|
|
214
|
+
const cfg = loadZooidConfig(yaml)
|
|
215
|
+
expect(cfg.agents.alice!.matrix).toEqual({
|
|
216
|
+
transport: 'm1',
|
|
217
|
+
user_id: '@alice:localhost',
|
|
218
|
+
rooms: ['!r:localhost'],
|
|
219
|
+
trigger: 'any',
|
|
220
|
+
})
|
|
221
|
+
expect(cfg.agents.alice!.http).toBeUndefined()
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it('defaults trigger to "mention" when omitted', () => {
|
|
225
|
+
const yaml = `
|
|
226
|
+
runtime: docker
|
|
227
|
+
${baseTransports}
|
|
228
|
+
agents:${matrixAgent()}
|
|
229
|
+
`
|
|
230
|
+
const cfg = loadZooidConfig(yaml)
|
|
231
|
+
expect(cfg.agents.alice!.matrix?.trigger).toBe('mention')
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it('rejects an agent with zero transport-kind blocks', () => {
|
|
235
|
+
const yaml = `
|
|
236
|
+
runtime: docker
|
|
237
|
+
${baseTransports}
|
|
238
|
+
agents:
|
|
239
|
+
alice:
|
|
240
|
+
workdir: ./alice
|
|
241
|
+
acp: { preset: claude }
|
|
242
|
+
`
|
|
243
|
+
expect(() => loadZooidConfig(yaml)).toThrow(/exactly one transport.*block/i)
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('rejects an agent with two transport-kind blocks', () => {
|
|
247
|
+
const yaml = `
|
|
248
|
+
runtime: docker
|
|
249
|
+
transports:
|
|
250
|
+
m1: { type: matrix, homeserver: x, as_token: t, hs_token: h, sender_localpart: z, user_namespace: '@.*:l' }
|
|
251
|
+
h1: { type: http, port: 8080 }
|
|
252
|
+
agents:
|
|
253
|
+
alice:
|
|
254
|
+
workdir: ./alice
|
|
255
|
+
acp: { preset: claude }
|
|
256
|
+
matrix:
|
|
257
|
+
transport: m1
|
|
258
|
+
user_id: '@alice:localhost'
|
|
259
|
+
rooms: ['!r:localhost']
|
|
260
|
+
http:
|
|
261
|
+
transport: h1
|
|
262
|
+
`
|
|
263
|
+
expect(() => loadZooidConfig(yaml)).toThrow(/exactly one transport.*block/i)
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it('rejects matrix block whose transport ref points at an http transport', () => {
|
|
267
|
+
const yaml = `
|
|
268
|
+
runtime: docker
|
|
269
|
+
transports:
|
|
270
|
+
h1: { type: http, port: 8080 }
|
|
271
|
+
agents:
|
|
272
|
+
alice:
|
|
273
|
+
workdir: ./alice
|
|
274
|
+
acp: { preset: claude }
|
|
275
|
+
matrix:
|
|
276
|
+
transport: h1
|
|
277
|
+
user_id: '@alice:localhost'
|
|
278
|
+
rooms: ['!r:localhost']
|
|
279
|
+
`
|
|
280
|
+
expect(() => loadZooidConfig(yaml)).toThrow(
|
|
281
|
+
/matrix.*references transport.*type: http/i,
|
|
282
|
+
)
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
it('rejects matrix block whose transport ref does not exist', () => {
|
|
286
|
+
const yaml = `
|
|
287
|
+
runtime: docker
|
|
288
|
+
${baseTransports}
|
|
289
|
+
agents:
|
|
290
|
+
alice:
|
|
291
|
+
workdir: ./alice
|
|
292
|
+
acp: { preset: claude }
|
|
293
|
+
matrix:
|
|
294
|
+
transport: nope
|
|
295
|
+
user_id: '@alice:localhost'
|
|
296
|
+
rooms: ['!r:localhost']
|
|
297
|
+
`
|
|
298
|
+
expect(() => loadZooidConfig(yaml)).toThrow(/transport "nope" is not declared/i)
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
it('parses http block', () => {
|
|
302
|
+
const yaml = `
|
|
303
|
+
runtime: docker
|
|
304
|
+
transports:
|
|
305
|
+
h1: { type: http, port: 8080 }
|
|
306
|
+
agents:
|
|
307
|
+
alice:
|
|
308
|
+
workdir: ./alice
|
|
309
|
+
acp: { preset: claude }
|
|
310
|
+
http:
|
|
311
|
+
transport: h1
|
|
312
|
+
`
|
|
313
|
+
const cfg = loadZooidConfig(yaml)
|
|
314
|
+
expect(cfg.agents.alice!.http).toEqual({ transport: 'h1' })
|
|
315
|
+
expect(cfg.agents.alice!.matrix).toBeUndefined()
|
|
316
|
+
})
|
|
317
|
+
})
|
|
318
|
+
|
|
319
|
+
describe('legacy field migration', () => {
|
|
320
|
+
it('rejects top-level docker: block with a pointer to ZOD043', () => {
|
|
321
|
+
const yaml = `
|
|
322
|
+
runtime: docker
|
|
323
|
+
docker:
|
|
324
|
+
image: x:1
|
|
325
|
+
${baseTransports}
|
|
326
|
+
agents:${matrixAgent()}
|
|
327
|
+
`
|
|
328
|
+
expect(() => loadZooidConfig(yaml)).toThrow(/Top-level 'docker'.*ZOD043/i)
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
it('rejects per-agent docker: block', () => {
|
|
332
|
+
const yaml = `
|
|
333
|
+
runtime: docker
|
|
334
|
+
${baseTransports}
|
|
335
|
+
agents:${matrixAgent(`
|
|
336
|
+
docker:
|
|
337
|
+
image: x:1`)}
|
|
338
|
+
`
|
|
339
|
+
expect(() => loadZooidConfig(yaml)).toThrow(/agents\.alice\.docker.*ZOD043/i)
|
|
340
|
+
})
|
|
341
|
+
|
|
342
|
+
it('rejects flat agent transport: string', () => {
|
|
343
|
+
const yaml = `
|
|
344
|
+
runtime: docker
|
|
345
|
+
${baseTransports}
|
|
346
|
+
agents:
|
|
347
|
+
alice:
|
|
348
|
+
workdir: ./alice
|
|
349
|
+
acp: { preset: claude }
|
|
350
|
+
transport: m1
|
|
351
|
+
matrix:
|
|
352
|
+
transport: m1
|
|
353
|
+
user_id: '@alice:localhost'
|
|
354
|
+
rooms: ['!r:localhost']
|
|
355
|
+
`
|
|
356
|
+
expect(() => loadZooidConfig(yaml)).toThrow(/transport[\s\S]*ZOD043/i)
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
it('rejects flat matrix_user_id / rooms / trigger fields', () => {
|
|
360
|
+
const yaml = `
|
|
361
|
+
runtime: docker
|
|
362
|
+
${baseTransports}
|
|
363
|
+
agents:
|
|
364
|
+
alice:
|
|
365
|
+
workdir: ./alice
|
|
366
|
+
acp: { preset: claude }
|
|
367
|
+
matrix_user_id: '@alice:localhost'
|
|
368
|
+
rooms: ['!r:localhost']
|
|
369
|
+
`
|
|
370
|
+
expect(() => loadZooidConfig(yaml)).toThrow(/matrix_user_id.*ZOD043/i)
|
|
371
|
+
})
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
describe('example fixtures parse cleanly', () => {
|
|
375
|
+
for (const rel of [
|
|
376
|
+
'./__fixtures__/zooid-dev.yaml',
|
|
377
|
+
'./__fixtures__/triage-agent.yaml',
|
|
378
|
+
'./__fixtures__/opencode-vertex-gemini.yaml',
|
|
379
|
+
'./__fixtures__/ec2-workspace.yaml',
|
|
380
|
+
]) {
|
|
381
|
+
it(`parses ${rel}`, () => {
|
|
382
|
+
const path = resolve(HERE, rel)
|
|
383
|
+
const text = readFileSync(path, 'utf8')
|
|
384
|
+
process.env.MATRIX_AS_TOKEN ??= 'fixture-as'
|
|
385
|
+
process.env.MATRIX_HS_TOKEN ??= 'fixture-hs'
|
|
386
|
+
expect(() => loadZooidConfig(text)).not.toThrow()
|
|
387
|
+
})
|
|
388
|
+
}
|
|
389
|
+
})
|