zooid 0.9.1 → 0.11.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/README.md CHANGED
@@ -6,10 +6,12 @@ Zooid is an open-source, self-hostable chat app for collaborating with AI agents
6
6
 
7
7
  **Full docs: [zooid.dev/docs](https://zooid.dev/docs)** · **Join the community server: [community.zoon.eco](https://community.zoon.eco)**
8
8
 
9
- - **Protocol-first.** Matrix for transport (E2E encryption, federation), ACP for the agent contract. Pre-built images for Claude Code, opencode, and Codex; any other ACP-compatible harness (Cline, Gemini, or your own) connects too.
10
- - **Containerized runtime.** Podman or Docker. Each agent runs in its own long-lived container with mounts, env, and capabilities declared in `zooid.yaml`.
11
- - **Workforce as code.** Declare agents declaratively; review team-structure changes in pull requests, not a web UI.
9
+ - **Protocol-first.** Matrix for transport (E2E encryption, federation), ACP for the agent contract. Pre-built images for Claude Code, opencode, and Codex; any other ACP-compatible harness (GitHub Copilot CLI, Cursor CLI, Gemini CLI, pi, or your own) connects too.
10
+ - **Run it anywhere.** Each daemon is a *workstation* with its own identity, running as many agents as you like — put it on your laptop, a box, a pod, or a cluster; it doesn't have to live next to the homeserver. Several workstations can share one homeserver.
11
+ - **Containerized runtime.** Docker or Podman. Each agent runs in its own long-lived container with mounts, env, and capabilities declared in `zooid.yaml`.
12
12
  - **Multi-agent collaboration.** Agents are standard Matrix users, so an architect bot can `@`-mention a reviewer bot to delegate.
13
+ - **Workforce as code.** Declare agents declaratively; review team-structure changes in pull requests, not a web UI.
14
+ - **Push or pull.** Pick how the daemon and server connect. *Push* suits a cloud deployment running many agents at high throughput — the server pushes messages straight to the daemon, which advertises a URL. *Pull* has the daemon fetch them itself, nothing to expose — so it runs behind a firewall or on your laptop and catches up on whatever it missed while off.
13
15
 
14
16
  ## Quickstart
15
17
 
@@ -34,22 +36,46 @@ Boot the local stack:
34
36
  zooid dev
35
37
  ```
36
38
 
39
+ > **Prerequisite:** `zooid dev` runs Tuwunel inside a container, so you need a container engine installed and running first — either **Docker** (with the daemon started) or **Podman** aliased as `docker`.
40
+
37
41
  `zooid dev` starts a Tuwunel Matrix homeserver in a container, generates the Application Service registration, registers an `admin:admin` user, runs the daemon, and serves the [Zooid web client](https://github.com/zooid-ai/clients).
38
42
 
39
43
  Open `http://localhost:5173`, log in as `admin` / `admin`, join `#welcome`, and `@`-mention your agent.
40
44
 
41
45
  For deployment recipes, the `zooid.yaml` reference, and a deeper tour of how the runtime works, see **[zooid.dev/docs](https://zooid.dev/docs)**.
42
46
 
47
+ ## Workstations & transport modes
48
+
49
+ A Zooid daemon is a **workstation** — set a top-level `workstation:` in `zooid.yaml` and its agents get an exclusive `@{workstation}.{agent}:server` namespace. Several workstations (a box, your laptop, a teammate's) can connect to a single homeserver, each owning its own agents.
50
+
51
+ How a workstation connects is set per-transport with `mode:` — the two choices (push/pull above), named by the value you put in the YAML:
52
+
53
+ - **`appservice` (push)** — the homeserver delivers events to the daemon, so the daemon advertises an address the homeserver can reach: `port` (default `9099`) when they share a host or network, or an `advertise_url` when the daemon lives on a separate machine, pod, or cluster. Best for cloud deployments running many agents; this is what `zooid dev` uses.
54
+ - **`client` (pull)** — the daemon fetches events itself, one outbound sync per agent, with nothing to expose. Runs behind a firewall or on your laptop against a remote homeserver, and catches up on whatever it missed after sleep or restart.
55
+
56
+ ```yaml
57
+ workstation: my-laptop
58
+ runtime: local
59
+ transports:
60
+ matrix:
61
+ homeserver: https://your-homeserver.example
62
+ mode: client
63
+ ```
64
+
65
+ Acting (sending, joining) is identical in both modes — only how the daemon ingests events differs.
66
+
43
67
  ## The stack
44
68
 
45
69
  Every layer is open and replaceable.
46
70
 
47
- | Layer | Project | License | Backing |
48
- | -------- | --------- | ------------- | --------------------------------------------- |
49
- | Protocol | Matrix | Open standard | Adopted by Germany, France, Switzerland, NATO |
50
- | Server | Tuwunel | Apache-2.0 | Swiss government in production |
51
- | Client | Zooid web | Apache-2.0 | Built on `matrix-js-sdk` |
52
- | Runtime | **Zooid** | MIT | This project |
71
+ | Layer | Project | License | Backing |
72
+ | -------------- | ---------------- | ------------- | ------------------------------------------------- |
73
+ | Agent protocol | **ACP** | Open standard | Backed by Zed and JetBrains |
74
+ | Bridge | **Zooid daemon** | MIT | This project the ACP–Matrix bridge |
75
+ | Server | **Matrix** | Open standard | Any homeserver — adopted by Germany, France, NATO |
76
+ | Client | **Zooid web** | MIT | Built on `matrix-js-sdk` (Apache-2.0) |
77
+
78
+ `zooid dev` runs [Tuwunel](https://github.com/matrix-construct/tuwunel) (Apache-2.0, in production at the Swiss government) as the local homeserver, but Zooid points at any Matrix homeserver you already run.
53
79
 
54
80
  ## Agent images
55
81
 
package/dist/bin.js CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  renderRegistration,
17
17
  startDaemonSocketServer,
18
18
  startWorkforcePublisher
19
- } from "./chunk-PDSJJWQW.js";
19
+ } from "./chunk-OCRQ3LWL.js";
20
20
 
21
21
  // src/bin.ts
22
22
  import { resolve as resolve6 } from "path";
@@ -25632,7 +25632,9 @@ var MatrixClient = class {
25632
25632
  ];
25633
25633
  }
25634
25634
  if (opts.userPowerLevels && Object.keys(opts.userPowerLevels).length > 0) {
25635
- body.power_level_content_override = { users: opts.userPowerLevels };
25635
+ const users = { ...opts.userPowerLevels };
25636
+ if (users[opts.senderUserId] === void 0) users[opts.senderUserId] = 100;
25637
+ body.power_level_content_override = { users };
25636
25638
  }
25637
25639
  const r = await this.fetch(
25638
25640
  `${this.homeserver}/_matrix/client/v3/createRoom?user_id=${encodeURIComponent(opts.senderUserId)}`,
@@ -26088,11 +26090,16 @@ function route(event, agents, threadStates) {
26088
26090
  continue;
26089
26091
  }
26090
26092
  if (threadState) {
26091
- const lastPoster = threadState.participants.at(-1);
26092
- if (lastPoster) {
26093
- if (lastPoster === a.name) matches.push(a);
26094
- } else if (threadState.rootMentions.includes(a.name)) {
26095
- matches.push(a);
26093
+ const senderAgent = agents.find((x) => x.userId === event.sender);
26094
+ if (senderAgent) {
26095
+ if (threadState.callers[senderAgent.name] === a.name) matches.push(a);
26096
+ } else {
26097
+ const lastPoster = threadState.participants.at(-1);
26098
+ if (lastPoster) {
26099
+ if (lastPoster === a.name) matches.push(a);
26100
+ } else if (threadState.rootMentions.includes(a.name)) {
26101
+ matches.push(a);
26102
+ }
26096
26103
  }
26097
26104
  }
26098
26105
  }
@@ -26220,14 +26227,20 @@ var BotPool = class {
26220
26227
  this.agents,
26221
26228
  room
26222
26229
  );
26223
- resolved = await this.client.createRoom({
26224
- roomAliasName: aliasLocalpart,
26225
- invite: opts.adminUserId ? [opts.adminUserId] : [],
26226
- senderUserId: sender,
26227
- name: aliasLocalpart,
26228
- ...opts.spaceRoomId ? { restrictedToSpaceId: opts.spaceRoomId } : {},
26229
- ...userPowerLevels ? { userPowerLevels } : {}
26230
- });
26230
+ try {
26231
+ resolved = await this.client.createRoom({
26232
+ roomAliasName: aliasLocalpart,
26233
+ invite: opts.adminUserId ? [opts.adminUserId] : [],
26234
+ senderUserId: sender,
26235
+ name: aliasLocalpart,
26236
+ ...opts.spaceRoomId ? { restrictedToSpaceId: opts.spaceRoomId } : {},
26237
+ ...userPowerLevels ? { userPowerLevels } : {}
26238
+ });
26239
+ } catch (err) {
26240
+ const raced = await this.client.resolveAlias(room);
26241
+ if (!raced) throw err;
26242
+ resolved = raced;
26243
+ }
26231
26244
  }
26232
26245
  aliasToId.set(room, resolved);
26233
26246
  }
@@ -26942,14 +26955,15 @@ function createMatrixTransport(opts) {
26942
26955
  if (matches.length > 0 && promotedRoot) {
26943
26956
  let st = threadStates.get(promotedRoot);
26944
26957
  if (!st) {
26945
- st = { participants: [], rootMentions: [] };
26958
+ st = { participants: [], rootMentions: [], callers: {} };
26946
26959
  threadStates.set(promotedRoot, st);
26947
26960
  }
26948
26961
  const msgMentions = new Set(extractMentions(evt));
26962
+ const senderAgent = bindings.find((b) => b.userId === evt.sender);
26949
26963
  for (const a of bindings) {
26950
- if (msgMentions.has(a.userId) && !st.rootMentions.includes(a.name)) {
26951
- st.rootMentions.push(a.name);
26952
- }
26964
+ if (!msgMentions.has(a.userId)) continue;
26965
+ if (!st.rootMentions.includes(a.name)) st.rootMentions.push(a.name);
26966
+ if (senderAgent && a.name !== senderAgent.name) st.callers[a.name] = senderAgent.name;
26953
26967
  }
26954
26968
  }
26955
26969
  for (const a of matches) {
@@ -26958,7 +26972,7 @@ function createMatrixTransport(opts) {
26958
26972
  if (!promotedRoot) return;
26959
26973
  let st = threadStates.get(promotedRoot);
26960
26974
  if (!st) {
26961
- st = { participants: [], rootMentions: [] };
26975
+ st = { participants: [], rootMentions: [], callers: {} };
26962
26976
  threadStates.set(promotedRoot, st);
26963
26977
  }
26964
26978
  if (st.participants.at(-1) !== a.name) st.participants.push(a.name);
@@ -27134,16 +27148,18 @@ function createMatrixTransport(opts) {
27134
27148
  };
27135
27149
  }
27136
27150
  async function rebuildThreadState(client, roomId, rootEventId, bindings) {
27137
- const state = { participants: [], rootMentions: [] };
27151
+ const state = { participants: [], rootMentions: [], callers: {} };
27138
27152
  const asUser = (bindings.find((b) => b.rooms.some((r) => r.alias === roomId)) ?? bindings[0])?.userId;
27139
27153
  if (!asUser) return state;
27140
27154
  const root = await client.fetchEvent(roomId, rootEventId, asUser);
27141
27155
  if (root) {
27142
27156
  const rootMentions = new Set(extractMentions(root));
27157
+ const rootSender = root.sender;
27158
+ const rootSenderAgent = rootSender ? bindings.find((b) => b.userId === rootSender) : void 0;
27143
27159
  for (const a of bindings) {
27144
- if (rootMentions.has(a.userId) && !state.rootMentions.includes(a.name)) {
27145
- state.rootMentions.push(a.name);
27146
- }
27160
+ if (!rootMentions.has(a.userId)) continue;
27161
+ if (!state.rootMentions.includes(a.name)) state.rootMentions.push(a.name);
27162
+ if (rootSenderAgent && a.name !== rootSenderAgent.name) state.callers[a.name] = rootSenderAgent.name;
27147
27163
  }
27148
27164
  }
27149
27165
  const { chunk: thread } = await client.fetchThreadRelations({
@@ -27153,15 +27169,16 @@ async function rebuildThreadState(client, roomId, rootEventId, bindings) {
27153
27169
  });
27154
27170
  for (const ev of thread) {
27155
27171
  const mentions = new Set(extractMentions(ev));
27172
+ const evSender = ev.sender;
27173
+ const evSenderAgent = evSender ? bindings.find((b) => b.userId === evSender) : void 0;
27156
27174
  for (const a of bindings) {
27157
- if (mentions.has(a.userId) && !state.rootMentions.includes(a.name)) {
27158
- state.rootMentions.push(a.name);
27159
- }
27175
+ if (!mentions.has(a.userId)) continue;
27176
+ if (!state.rootMentions.includes(a.name)) state.rootMentions.push(a.name);
27177
+ if (evSenderAgent && a.name !== evSenderAgent.name) state.callers[a.name] = evSenderAgent.name;
27160
27178
  }
27161
- const sender = ev.sender;
27162
27179
  const type = ev.type;
27163
- if (type === "m.room.message" && sender) {
27164
- const a = bindings.find((b) => b.userId === sender);
27180
+ if (type === "m.room.message" && evSender) {
27181
+ const a = bindings.find((b) => b.userId === evSender);
27165
27182
  if (a && state.participants.at(-1) !== a.name) state.participants.push(a.name);
27166
27183
  }
27167
27184
  }
@@ -32554,6 +32571,7 @@ var McpZodTypeKind;
32554
32571
 
32555
32572
  // ../context-mcp/src/factory.ts
32556
32573
  import { createRequire } from "module";
32574
+ import { dirname } from "path";
32557
32575
  function resolveDefaultBin() {
32558
32576
  const req = createRequire(import.meta.url);
32559
32577
  return req.resolve("@zooid/context-mcp/bin");
@@ -32563,7 +32581,25 @@ function getDefaultBin() {
32563
32581
  if (!cachedDefaultBin) cachedDefaultBin = resolveDefaultBin();
32564
32582
  return cachedDefaultBin;
32565
32583
  }
32584
+ var CONTEXT_CONTAINER_BIN_DIR = "/zooid/context-mcp";
32585
+ var CONTEXT_CONTAINER_BIN = `${CONTEXT_CONTAINER_BIN_DIR}/bin.js`;
32586
+ var CONTEXT_CONTAINER_SOCK = "/zooid/context.sock";
32587
+ function contextContainerMounts(opts) {
32588
+ const binFile = opts.binPath ?? getDefaultBin();
32589
+ return [
32590
+ { path: dirname(binFile), target: CONTEXT_CONTAINER_BIN_DIR, mode: "ro" },
32591
+ { path: opts.sockPath, target: CONTEXT_CONTAINER_SOCK, mode: "rw" }
32592
+ ];
32593
+ }
32566
32594
  function buildContextServerSpec(opts) {
32595
+ if (opts.containerize) {
32596
+ return {
32597
+ name: "zooid-context",
32598
+ command: "node",
32599
+ args: [CONTEXT_CONTAINER_BIN, "--spawn-id", opts.spawnId],
32600
+ env: [{ name: "ZOOID_DAEMON_SOCK", value: CONTEXT_CONTAINER_SOCK }]
32601
+ };
32602
+ }
32567
32603
  return {
32568
32604
  name: "zooid-context",
32569
32605
  command: process.execPath,
@@ -32713,6 +32749,15 @@ Set agents.<name>.container.image, top-level container.image, or use a preset th
32713
32749
  }
32714
32750
  }
32715
32751
  const contextSpawns = buildContextSpawns(cfg, opts);
32752
+ if (cfg.runtime !== "local" && opts.daemonSockPath && contextSpawns) {
32753
+ for (const name of Object.keys(cfg.agents)) {
32754
+ if (!contextSpawns[name]) continue;
32755
+ mountsByAgent[name] = [
32756
+ ...mountsByAgent[name] ?? [],
32757
+ ...contextContainerMounts({ sockPath: opts.daemonSockPath })
32758
+ ];
32759
+ }
32760
+ }
32716
32761
  return new AcpAgentRegistry({
32717
32762
  runtime,
32718
32763
  agents: cfg.agents,
@@ -32758,7 +32803,11 @@ function buildContextSpawns(cfg, opts) {
32758
32803
  threadRef: { channelId: channelId ?? threadId, threadId },
32759
32804
  provider
32760
32805
  });
32761
- return buildContextServerSpec({ spawnId, sockPath });
32806
+ return buildContextServerSpec({
32807
+ spawnId,
32808
+ sockPath,
32809
+ containerize: cfg.runtime !== "local"
32810
+ });
32762
32811
  };
32763
32812
  } else {
32764
32813
  result[name] = void 0;
@@ -32795,4 +32844,4 @@ export {
32795
32844
  startDaemonSocketServer,
32796
32845
  buildAcpRegistry
32797
32846
  };
32798
- //# sourceMappingURL=chunk-PDSJJWQW.js.map
32847
+ //# sourceMappingURL=chunk-OCRQ3LWL.js.map