@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/src/config.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { existsSync } from 'node:fs'
2
- import { join } from 'node:path'
2
+ import { isAbsolute, join, resolve as pathResolve } from 'node:path'
3
3
  import { parse } from 'yaml'
4
4
  import type { AcpAgentSpec } from './acp-types.js'
5
5
  import { isPreset } from '@zooid/acp-client'
@@ -12,11 +12,23 @@ import type {
12
12
  HttpTransportConfig,
13
13
  MatrixBinding,
14
14
  MatrixTransportConfig,
15
+ MountConfig,
16
+ RoomBinding,
15
17
  TransportConfig,
16
18
  ZooidConfig,
17
19
  ZooidContainerConfig,
18
20
  } from './types.js'
19
21
 
22
+ export interface LoadZooidConfigOptions {
23
+ /**
24
+ * Directory containing zooid.yaml. Required when any agent uses a
25
+ * relative `container.mounts[].host` path; resolution happens at parse
26
+ * time so the resulting `MountConfig` always carries an absolute host
27
+ * path.
28
+ */
29
+ configDir?: string
30
+ }
31
+
20
32
  const AGENT_NAME_RE = /^[a-z][a-z0-9-]{0,31}$/
21
33
  const MATRIX_USER_ID_RE = /^@[A-Za-z0-9._\-=/+]+:[A-Za-z0-9.\-]+$/
22
34
  const MATRIX_USER_LOCALPART_RE = /^@[a-z0-9._=/+\-]+$/
@@ -117,6 +129,7 @@ function parseAgentContainer(
117
129
  name: string,
118
130
  raw: unknown,
119
131
  processEnv: NodeJS.ProcessEnv,
132
+ configDir: string | undefined,
120
133
  ): ContainerConfig {
121
134
  if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
122
135
  throw new Error(`agents.${name}.container must be a mapping`)
@@ -145,6 +158,145 @@ function parseAgentContainer(
145
158
  }
146
159
  out.env = interpolateEnv(stringEnv, processEnv, `agents.${name}.container.env`)
147
160
  }
161
+ if (r.mounts !== undefined) {
162
+ out.mounts = parseMountList(name, r.mounts, processEnv, configDir)
163
+ }
164
+ if (r.disable_mounts !== undefined) {
165
+ out.disable_mounts = parseDisableMounts(name, r.disable_mounts)
166
+ }
167
+ return out
168
+ }
169
+
170
+ function parseMountList(
171
+ agentName: string,
172
+ raw: unknown,
173
+ processEnv: NodeJS.ProcessEnv,
174
+ configDir: string | undefined,
175
+ ): MountConfig[] {
176
+ if (!Array.isArray(raw)) {
177
+ throw new Error(`agents.${agentName}.container.mounts must be an array`)
178
+ }
179
+ const out: MountConfig[] = []
180
+ const seenIds = new Set<string>()
181
+ for (let i = 0; i < raw.length; i++) {
182
+ const entry = raw[i]
183
+ if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
184
+ throw new Error(`agents.${agentName}.container.mounts[${i}] must be a mapping`)
185
+ }
186
+ const e = entry as Record<string, unknown>
187
+ if (e.host === undefined) {
188
+ throw new Error(`agents.${agentName}.container.mounts[${i}].host is required`)
189
+ }
190
+ if (e.target === undefined) {
191
+ throw new Error(`agents.${agentName}.container.mounts[${i}].target is required`)
192
+ }
193
+ if (typeof e.host !== 'string' || e.host.length === 0) {
194
+ throw new Error(
195
+ `agents.${agentName}.container.mounts[${i}].host must be a non-empty string`,
196
+ )
197
+ }
198
+ if (typeof e.target !== 'string' || e.target.length === 0) {
199
+ throw new Error(
200
+ `agents.${agentName}.container.mounts[${i}].target must be a non-empty string`,
201
+ )
202
+ }
203
+ const mode = e.mode ?? 'rw'
204
+ if (mode !== 'ro' && mode !== 'rw') {
205
+ throw new Error(
206
+ `agents.${agentName}.container.mounts[${i}].mode must be "ro" or "rw" (got ${JSON.stringify(e.mode)})`,
207
+ )
208
+ }
209
+ let id: string | undefined
210
+ if (e.id !== undefined) {
211
+ if (typeof e.id !== 'string' || e.id.length === 0) {
212
+ throw new Error(
213
+ `agents.${agentName}.container.mounts[${i}].id must be a non-empty string`,
214
+ )
215
+ }
216
+ if (e.id === 'workspace') {
217
+ throw new Error(
218
+ `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.`,
219
+ )
220
+ }
221
+ if (seenIds.has(e.id)) {
222
+ throw new Error(
223
+ `agents.${agentName}.container.mounts: duplicate id "${e.id}"`,
224
+ )
225
+ }
226
+ seenIds.add(e.id)
227
+ id = e.id
228
+ }
229
+ let create: boolean | undefined
230
+ if (e.create !== undefined) {
231
+ if (typeof e.create !== 'boolean') {
232
+ throw new Error(
233
+ `agents.${agentName}.container.mounts[${i}].create must be a boolean`,
234
+ )
235
+ }
236
+ create = e.create
237
+ }
238
+ const host = resolveHostPath(
239
+ agentName,
240
+ i,
241
+ interpolateString(e.host, processEnv),
242
+ configDir,
243
+ )
244
+ const target = interpolateString(e.target, processEnv)
245
+ const m: MountConfig = { host, target, mode }
246
+ if (id !== undefined) m.id = id
247
+ if (create !== undefined) m.create = create
248
+ out.push(m)
249
+ }
250
+ return out
251
+ }
252
+
253
+ function resolveHostPath(
254
+ agentName: string,
255
+ index: number,
256
+ host: string,
257
+ configDir: string | undefined,
258
+ ): string {
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 ~ — $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 ~ — $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) — pass it via loadZooidConfig(yaml, { configDir })`,
281
+ )
282
+ }
283
+ return pathResolve(configDir, host)
284
+ }
285
+
286
+ function parseDisableMounts(agentName: string, raw: unknown): string[] {
287
+ if (!Array.isArray(raw)) {
288
+ throw new Error(`agents.${agentName}.container.disable_mounts must be an array of strings`)
289
+ }
290
+ const out: string[] = []
291
+ for (let i = 0; i < raw.length; i++) {
292
+ const v = raw[i]
293
+ if (typeof v !== 'string' || v.length === 0) {
294
+ throw new Error(
295
+ `agents.${agentName}.container.disable_mounts[${i}] must be a non-empty string`,
296
+ )
297
+ }
298
+ out.push(v)
299
+ }
148
300
  return out
149
301
  }
150
302
 
@@ -301,6 +453,40 @@ function parseTransport(
301
453
  return { type: 'http', port }
302
454
  }
303
455
 
456
+ function parseRoomBinding(path: string, raw: unknown, serverName: string): RoomBinding {
457
+ function normalizeAlias(alias: string): string {
458
+ if (alias.length === 0) {
459
+ throw new Error(`${path}: must be a non-empty alias`)
460
+ }
461
+ if (!MATRIX_ROOM_IDENT_RE.test(alias)) {
462
+ throw new Error(
463
+ `${path}: must start with '#' or '!' (got ${JSON.stringify(alias)})`,
464
+ )
465
+ }
466
+ return alias.includes(':') ? alias : `${alias}:${serverName}`
467
+ }
468
+ if (typeof raw === 'string') {
469
+ return { alias: normalizeAlias(raw) }
470
+ }
471
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
472
+ throw new Error(`${path}: must be a string or { alias, power_level } object`)
473
+ }
474
+ const r = raw as Record<string, unknown>
475
+ if (typeof r.alias !== 'string' || r.alias.length === 0) {
476
+ throw new Error(`${path}.alias: must be a non-empty string`)
477
+ }
478
+ const out: RoomBinding = { alias: normalizeAlias(r.alias) }
479
+ if (r.power_level !== undefined) {
480
+ if (typeof r.power_level !== 'number' || !Number.isInteger(r.power_level)) {
481
+ throw new Error(
482
+ `${path}.power_level: must be an integer (got ${JSON.stringify(r.power_level)})`,
483
+ )
484
+ }
485
+ out.powerLevel = r.power_level
486
+ }
487
+ return out
488
+ }
489
+
304
490
  function parseTransportBinding(
305
491
  name: string,
306
492
  entry: Record<string, unknown>,
@@ -384,17 +570,9 @@ function parseTransportBinding(
384
570
  if (!Array.isArray(block.rooms) || block.rooms.length === 0) {
385
571
  throw new Error(`agents.${name}.matrix.rooms is required and must be a non-empty array`)
386
572
  }
387
- const rooms: string[] = []
388
- for (const r of block.rooms) {
389
- if (typeof r !== 'string' || r.length === 0) {
390
- throw new Error(`agents.${name}.matrix.rooms[] must be a non-empty string`)
391
- }
392
- if (!MATRIX_ROOM_IDENT_RE.test(r)) {
393
- throw new Error(
394
- `agents.${name}.matrix.rooms[] must start with '#' or '!' (got ${JSON.stringify(r)})`,
395
- )
396
- }
397
- rooms.push(r.includes(':') ? r : `${r}:${serverName}`)
573
+ const rooms: RoomBinding[] = []
574
+ for (let i = 0; i < block.rooms.length; i++) {
575
+ rooms.push(parseRoomBinding(`agents.${name}.matrix.rooms[${i}]`, block.rooms[i], serverName))
398
576
  }
399
577
 
400
578
  let displayName: string | undefined
@@ -439,6 +617,7 @@ function parseAgents(
439
617
  transports: Record<string, TransportConfig>,
440
618
  daemonHooks: { pre_turn?: string; post_turn?: string },
441
619
  processEnv: NodeJS.ProcessEnv,
620
+ configDir: string | undefined,
442
621
  ): Record<string, AgentConfig> {
443
622
  if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
444
623
  throw new Error('agents: must be a mapping')
@@ -523,14 +702,26 @@ function parseAgents(
523
702
  let containerBlock: ContainerConfig | undefined
524
703
  if (entry.container !== undefined && entry.container !== null) {
525
704
  if (runtime === 'local') {
526
- throw new Error(
527
- `agents.${name}.container is only valid when runtime is 'docker' or 'podman'. ` +
528
- `runtime: local spawns agents as host child processes — there is no container, ` +
529
- `so 'image' is inert and 'env' would silently lie (the agent inherits the daemon's ` +
530
- `full process.env regardless).`,
531
- )
705
+ // Under runtime: local, the parser only accepts mounts/disable_mounts
706
+ // (which the compose layer ignores). image/env stay rejected because
707
+ // they would silently lie: there's no container and the host inherits
708
+ // the daemon's full process.env regardless.
709
+ if (typeof entry.container !== 'object' || entry.container === null || Array.isArray(entry.container)) {
710
+ throw new Error(`agents.${name}.container must be a mapping`)
711
+ }
712
+ const c = entry.container as Record<string, unknown>
713
+ const disallowed = Object.keys(c).filter((k) => k !== 'mounts' && k !== 'disable_mounts')
714
+ if (disallowed.length > 0) {
715
+ throw new Error(
716
+ `agents.${name}.container.${disallowed[0]} is only valid when runtime is 'docker' or 'podman'. ` +
717
+ `runtime: local spawns agents as host child processes — there is no container, ` +
718
+ `so 'image' is inert and 'env' would silently lie (the agent inherits the daemon's ` +
719
+ `full process.env regardless). 'mounts' and 'disable_mounts' are accepted under ` +
720
+ `runtime: local but ignored at compose time.`,
721
+ )
722
+ }
532
723
  }
533
- containerBlock = parseAgentContainer(name, entry.container, processEnv)
724
+ containerBlock = parseAgentContainer(name, entry.container, processEnv, configDir)
534
725
  }
535
726
 
536
727
  const binding = parseTransportBinding(name, entry, transports)
@@ -568,7 +759,10 @@ function zooidHooks(raw: Record<string, unknown>): { pre_turn?: string; post_tur
568
759
  return out
569
760
  }
570
761
 
571
- export function loadZooidConfig(yamlText: string): ZooidConfig {
762
+ export function loadZooidConfig(
763
+ yamlText: string,
764
+ opts: LoadZooidConfigOptions = {},
765
+ ): ZooidConfig {
572
766
  const raw = parse(yamlText) ?? {}
573
767
  if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
574
768
  throw new Error('zooid.yaml must be a YAML object')
@@ -604,7 +798,7 @@ export function loadZooidConfig(yamlText: string): ZooidConfig {
604
798
  const processEnv = process.env
605
799
  const transports = parseTransports(r.transports, processEnv)
606
800
  const hooks = zooidHooks(r)
607
- const agents = parseAgents(r.agents, runtime, transports, hooks, processEnv)
801
+ const agents = parseAgents(r.agents, runtime, transports, hooks, processEnv, opts.configDir)
608
802
 
609
803
  const cfg: ZooidConfig = {
610
804
  runtime,
@@ -0,0 +1,292 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest'
2
+ import { loadZooidConfig } from './config.js'
3
+
4
+ const baseTransports = `
5
+ transports:
6
+ m1:
7
+ type: matrix
8
+ homeserver: http://localhost:8448
9
+ as_token: t
10
+ hs_token: h
11
+ sender_localpart: z
12
+ user_namespace: '@.*:localhost'
13
+ `
14
+
15
+ const matrixAgent = (extra = ''): string => `
16
+ alice:
17
+ workdir: ./agents/alice
18
+ acp: { preset: claude }
19
+ matrix:
20
+ transport: m1
21
+ user_id: '@alice:localhost'
22
+ rooms: ['!r:localhost']${extra}
23
+ `
24
+
25
+ const wrap = (extra: string): string => `
26
+ runtime: docker
27
+ ${baseTransports}
28
+ agents:${matrixAgent(extra)}
29
+ `
30
+
31
+ describe('container.mounts — shape', () => {
32
+ it('parses a minimal mount with default mode rw', () => {
33
+ const cfg = loadZooidConfig(
34
+ wrap(`
35
+ container:
36
+ mounts:
37
+ - host: /var/run/docker.sock
38
+ target: /var/run/docker.sock`),
39
+ { configDir: '/tmp' },
40
+ )
41
+ expect(cfg.agents.alice!.container?.mounts).toEqual([
42
+ { host: '/var/run/docker.sock', target: '/var/run/docker.sock', mode: 'rw' },
43
+ ])
44
+ })
45
+
46
+ it('honours explicit mode: ro', () => {
47
+ const cfg = loadZooidConfig(
48
+ wrap(`
49
+ container:
50
+ mounts:
51
+ - host: /etc/hosts
52
+ target: /etc/hosts
53
+ mode: ro`),
54
+ { configDir: '/tmp' },
55
+ )
56
+ expect(cfg.agents.alice!.container?.mounts?.[0]?.mode).toBe('ro')
57
+ })
58
+
59
+ it('rejects mode other than ro/rw', () => {
60
+ expect(() =>
61
+ loadZooidConfig(
62
+ wrap(`
63
+ container:
64
+ mounts:
65
+ - host: /a
66
+ target: /a
67
+ mode: rwx`),
68
+ { configDir: '/tmp' },
69
+ ),
70
+ ).toThrow(/mode must be "ro" or "rw"/i)
71
+ })
72
+
73
+ it('rejects entries missing host or target', () => {
74
+ expect(() =>
75
+ loadZooidConfig(
76
+ wrap(`
77
+ container:
78
+ mounts:
79
+ - target: /a`),
80
+ { configDir: '/tmp' },
81
+ ),
82
+ ).toThrow(/host.*required/i)
83
+ expect(() =>
84
+ loadZooidConfig(
85
+ wrap(`
86
+ container:
87
+ mounts:
88
+ - host: /a`),
89
+ { configDir: '/tmp' },
90
+ ),
91
+ ).toThrow(/target.*required/i)
92
+ })
93
+
94
+ it('rejects duplicate user-declared ids', () => {
95
+ expect(() =>
96
+ loadZooidConfig(
97
+ wrap(`
98
+ container:
99
+ mounts:
100
+ - id: x
101
+ host: /a
102
+ target: /a
103
+ - id: x
104
+ host: /b
105
+ target: /b`),
106
+ { configDir: '/tmp' },
107
+ ),
108
+ ).toThrow(/duplicate.*id.*"x"/i)
109
+ })
110
+
111
+ it('rejects the reserved id "workspace" on user entries', () => {
112
+ expect(() =>
113
+ loadZooidConfig(
114
+ wrap(`
115
+ container:
116
+ mounts:
117
+ - id: workspace
118
+ host: /a
119
+ target: /a`),
120
+ { configDir: '/tmp' },
121
+ ),
122
+ ).toThrow(/reserved id.*workspace/i)
123
+ })
124
+ })
125
+
126
+ describe('container.mounts — host path resolution', () => {
127
+ it('keeps absolute paths verbatim', () => {
128
+ const cfg = loadZooidConfig(
129
+ wrap(`
130
+ container:
131
+ mounts:
132
+ - host: /var/lib/foo
133
+ target: /opt/foo`),
134
+ { configDir: '/some/where' },
135
+ )
136
+ expect(cfg.agents.alice!.container?.mounts?.[0]?.host).toBe('/var/lib/foo')
137
+ })
138
+
139
+ it('expands ~/... against $HOME', () => {
140
+ const home = process.env.HOME!
141
+ const cfg = loadZooidConfig(
142
+ wrap(`
143
+ container:
144
+ mounts:
145
+ - host: ~/.cache/zooid
146
+ target: /cache`),
147
+ { configDir: '/tmp' },
148
+ )
149
+ expect(cfg.agents.alice!.container?.mounts?.[0]?.host).toBe(`${home}/.cache/zooid`)
150
+ })
151
+
152
+ it('resolves relative paths against configDir', () => {
153
+ const cfg = loadZooidConfig(
154
+ wrap(`
155
+ container:
156
+ mounts:
157
+ - host: ./shared
158
+ target: /shared`),
159
+ { configDir: '/example/path' },
160
+ )
161
+ expect(cfg.agents.alice!.container?.mounts?.[0]?.host).toBe('/example/path/shared')
162
+ })
163
+
164
+ it('throws on relative host path when configDir is omitted', () => {
165
+ expect(() =>
166
+ loadZooidConfig(
167
+ wrap(`
168
+ container:
169
+ mounts:
170
+ - host: ./shared
171
+ target: /shared`),
172
+ // no configDir
173
+ ),
174
+ ).toThrow(/relative.*host.*configDir/i)
175
+ })
176
+ })
177
+
178
+ describe('container.mounts — ${VAR} interpolation', () => {
179
+ let saved: NodeJS.ProcessEnv
180
+ beforeEach(() => {
181
+ saved = { ...process.env }
182
+ })
183
+ afterEach(() => {
184
+ for (const k of Object.keys(process.env)) delete process.env[k]
185
+ Object.assign(process.env, saved)
186
+ })
187
+
188
+ it('expands ${VAR} in host and target', () => {
189
+ process.env.SHARED = '/srv/shared'
190
+ process.env.MOUNTPT = '/mnt/shared'
191
+ const cfg = loadZooidConfig(
192
+ wrap(`
193
+ container:
194
+ mounts:
195
+ - host: \${SHARED}
196
+ target: \${MOUNTPT}`),
197
+ { configDir: '/tmp' },
198
+ )
199
+ expect(cfg.agents.alice!.container?.mounts?.[0]).toMatchObject({
200
+ host: '/srv/shared',
201
+ target: '/mnt/shared',
202
+ })
203
+ })
204
+ })
205
+
206
+ describe('container.disable_mounts', () => {
207
+ it('parses a list of strings', () => {
208
+ const cfg = loadZooidConfig(
209
+ wrap(`
210
+ container:
211
+ disable_mounts: [history, workspace]`),
212
+ { configDir: '/tmp' },
213
+ )
214
+ expect(cfg.agents.alice!.container?.disable_mounts).toEqual(['history', 'workspace'])
215
+ })
216
+
217
+ it('rejects empty strings', () => {
218
+ expect(() =>
219
+ loadZooidConfig(
220
+ wrap(`
221
+ container:
222
+ disable_mounts: ['', 'history']`),
223
+ { configDir: '/tmp' },
224
+ ),
225
+ ).toThrow(/disable_mounts.*non-empty string/i)
226
+ })
227
+ })
228
+
229
+ describe('runtime: local + container.mounts', () => {
230
+ it('accepts mounts under runtime: local without error (silently ignored at compose time)', () => {
231
+ const cfg = loadZooidConfig(
232
+ `runtime: local
233
+ ${baseTransports}
234
+ agents:
235
+ alice:
236
+ workdir: ./agents/alice
237
+ acp: { preset: claude }
238
+ matrix:
239
+ transport: m1
240
+ user_id: '@alice:localhost'
241
+ rooms: ['!r:localhost']
242
+ container:
243
+ mounts:
244
+ - host: /a
245
+ target: /a
246
+ disable_mounts: [memory]
247
+ `,
248
+ { configDir: '/tmp' },
249
+ )
250
+ expect(cfg.agents.alice!.container?.mounts).toHaveLength(1)
251
+ expect(cfg.agents.alice!.container?.disable_mounts).toEqual(['memory'])
252
+ })
253
+ })
254
+
255
+ describe('post-migration shape — relative workdir + image + env interpolation', () => {
256
+ it('parses an opencode reviewer agent with relative workdir and ${VAR} env', () => {
257
+ const yaml = `
258
+ runtime: docker
259
+
260
+ transports:
261
+ matrix:
262
+ type: matrix
263
+ homeserver: http://localhost:8448
264
+ as_token: \${MATRIX_AS_TOKEN}
265
+ hs_token: \${MATRIX_HS_TOKEN}
266
+ sender_localpart: zooid
267
+ user_namespace: '@.*:localhost'
268
+ port: 9099
269
+
270
+ agents:
271
+ reviewer:
272
+ workdir: ./agents/reviewer
273
+ acp:
274
+ preset: opencode
275
+ container:
276
+ image: zooid-opencode-vertex:smoke
277
+ env:
278
+ GOOGLE_VERTEX_API_KEY: \${GOOGLE_VERTEX_API_KEY}
279
+ matrix:
280
+ transport: matrix
281
+ user_id: '@reviewer:localhost'
282
+ rooms:
283
+ - '#review:localhost'
284
+ trigger: mention
285
+ `
286
+ process.env.MATRIX_AS_TOKEN ??= 'fixture-as'
287
+ process.env.MATRIX_HS_TOKEN ??= 'fixture-hs'
288
+ const cfg = loadZooidConfig(yaml, { configDir: '/example/path' })
289
+ expect(cfg.agents.reviewer!.workdir).toBe('./agents/reviewer')
290
+ expect(cfg.agents.reviewer!.container?.image).toBe('zooid-opencode-vertex:smoke')
291
+ })
292
+ })
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@ export {
6
6
  findHttpTransport,
7
7
  findConfigFile,
8
8
  } from './config.js'
9
+ export type { LoadZooidConfigOptions } from './config.js'
9
10
  export {
10
11
  AcpAgentRegistry,
11
12
  resolveAcpAgentSpec,
@@ -32,8 +33,10 @@ export type {
32
33
  export type {
33
34
  AgentConfig,
34
35
  ContainerConfig,
36
+ MountConfig,
35
37
  ZooidContainerConfig,
36
38
  MatrixBinding,
39
+ RoomBinding,
37
40
  HttpBinding,
38
41
  ZooidConfig,
39
42
  TransportConfig,