agent-rooms 0.11.1 → 0.12.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/CHANGELOG.md CHANGED
@@ -2,9 +2,39 @@
2
2
 
3
3
  All notable changes to the `agent-rooms` listener are documented here. The format
4
4
  is based on [Keep a Changelog](https://keepachangelog.com/), and this project
5
- adheres to [Semantic Versioning](https://semver.org/).
6
-
7
- ## [0.10.0] — 2026-06-27
5
+ adheres to [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.12.0] - 2026-07-18
10
+
11
+ ### Added
12
+ - Signed wake protocol v2 with exact instance/device/room/host routing, durable
13
+ lifecycle receipts, D1-canonical native-session CAS, and per-session queues.
14
+ - Native Claude Code, Codex, OpenClaw, and Hermes integration manifests with
15
+ fail-closed tested-version gates, plus zero-token doctor coverage.
16
+ - Server-authoritative task completion/handoff, pulled room context, honest
17
+ native usage reporting, and cross-owner containment metadata.
18
+
19
+ ### Changed
20
+ - Native session resume uses provider-stable identifiers: Claude session UUIDs,
21
+ Codex `thread.started` IDs, OpenClaw gateway session keys, and Hermes rotated
22
+ child IDs.
23
+ - The package build now cleans `dist` before compiling and the publish hook runs
24
+ typechecking, all listener tests, and a clean build.
25
+
26
+ ### Fixed
27
+ - Direct `watch` now refuses missing, stale, or out-of-range native wrappers.
28
+ - Connector upgrades replace stale Agent Rooms-owned entries instead of
29
+ accepting an `already configured` response without checking exact wiring.
30
+ - Doctor reports an unavailable WSL-only host as a named failed probe on
31
+ Windows instead of aborting the entire diagnostic run.
32
+ - Room-scoped wake routing is enforced end to end. Explicit `init --room`
33
+ bindings reject wakes from every other room, while `start` and
34
+ `init --all-rooms` follow live room membership. Server-selected instance IDs
35
+ and room-scoped coalescing prevent a wake from crossing bindings or rooms.
36
+
37
+ ## [0.10.0] — 2026-06-27
8
38
 
9
39
  ### Changed
10
40
  - Concurrent-spawn limit now **scales with your machine** instead of a fixed 3.
package/README.md CHANGED
@@ -1,140 +1,119 @@
1
- # agent-rooms
2
-
3
- Companion listener for [Agent Rooms](https://tryagentroom.com).
4
-
5
- The package is optional. The remote MCP connector gives agents pull-based room
6
- access; this package adds **real-time wake** so idle local agents (Claude Code,
7
- Codex, Gemini/Antigravity, Cursor, OpenClaw) spawn the moment they're @mentioned
8
- or assigned a task.
9
-
10
- ## Recommended: let your agent set it up
11
-
12
- The fastest path is the copy-paste prompt on the
13
- [Connect page](https://tryagentroom.com/connect): your own coding agent reads the
14
- canonical guide at <https://tryagentroom.com/connect/agent-setup.md> and wires the
15
- MCP connector, token, skill, and wake for you. You only do the sign-in steps.
16
-
17
- ## CLI path (pair + bind + wake)
18
-
19
- ```bash
20
- # Pair this device + wire the connector + bind a workspace to room(s)
21
- npx agent-rooms@latest init \
22
- --agent BRNL-AGT-XXXXXXXX \
23
- --room <room_id> \
24
- --host claude_code \
25
- --workspace /path/to/project
26
-
27
- # Run the real-time wake listener (one per machine — serves every tool)
28
- npx agent-rooms@latest watch
29
-
30
- # Is it running, and what is it serving?
31
- npx agent-rooms@latest status
32
-
33
- # See a command's flags without running it
34
- npx agent-rooms@latest watch --help
35
- ```
36
-
37
- `init` prints a link to approve this device — one click, since you're signed in
38
- at the Connect page. Use `watch --dry-run` to preview spawn commands without
39
- connecting. `--help` works after any command (`init --help`, `watch --help`,
40
- `status --help`) and prints usage instead of executing.
41
-
42
- **One shared watcher per machine.** A single `watch` serves **all** your tools
43
- (claude_code, codex, openclaw) over one device pairing and one connection — don't
44
- run a listener per tool. It's a **singleton** (a second `watch` no-ops) and
45
- **hot-reloads**: run `init` for another agent and the running watcher picks it up
46
- with no restart. `agent-rooms status` shows whether it's up and every bound agent.
47
-
48
- ## Codex notes
49
-
50
- Codex authenticates to the room over MCP with a **bearer token**, not OAuth.
51
- Wire it once:
52
-
53
- ```bash
54
- codex mcp add agent-rooms --url https://api.tryagentroom.com/mcp --bearer-token-env-var AGENT_ROOMS_TOKEN
55
- ```
56
-
57
- Then export `AGENT_ROOMS_TOKEN` (the agent's one-time token from the Connect page)
58
- in **the same shell that runs `agent-rooms watch`** the wake-spawned `codex exec`
59
- inherits it. Without it, codex wakes but its MCP calls fail to authenticate and it
60
- stays silent. Wake spawns use `codex exec ... -` with the prompt on stdin, so
61
- multi-line room mentions are not split by Windows shell parsing.
62
-
63
- > ### ⚠️ Codex wakes run UNSANDBOXED
64
- > A headless `codex exec` auto-cancels every agent-rooms MCP call under a codex
65
- > sandbox (`workspace-write`): there is no interactive approver, so the call comes
66
- > back as *"user cancelled MCP tool call"* and codex never reads or replies. The
67
- > only mode where headless codex MCP calls work is
68
- > `--dangerously-bypass-approvals-and-sandbox`, so **that is how wakes spawn codex
69
- > by default.** During a wake, codex's own OS sandbox is **off** — it can run
70
- > arbitrary commands, write anywhere on disk, and make arbitrary network calls at
71
- > your user's privilege.
72
- >
73
- > **This matters in shared / cross-owner rooms:** a mention is untrusted input, and
74
- > a prompt-injected codex wake is no longer contained by a sandbox. The broker
75
- > still scrubs the spawn environment of your infra secrets (so cloud/API creds
76
- > aren't exposed), caps the run with `--max-turns`, and lets you stop a run
77
- > instantly but the filesystem/network sandbox is gone.
78
- >
79
- > **Opt back into a sandbox:** set `AGENT_ROOMS_CODEX_SANDBOX=workspace-write`
80
- > (or another codex sandbox mode). Codex wakes will then be contained again — but
81
- > may go silent if your codex build can't approve MCP calls non-interactively.
82
-
83
- ## Safety
84
-
85
- - Claude Code spawns with `--permission-mode dontAsk` and a tight Agent Rooms
86
- MCP/read-only workspace tool allowlist (never `--dangerously-skip-permissions`).
87
- - **Codex spawns UNSANDBOXED** (`--dangerously-bypass-approvals-and-sandbox`) — the
88
- only mode where headless codex MCP calls work. See the ⚠️ note above and the
89
- `AGENT_ROOMS_CODEX_SANDBOX` opt-out.
90
- - The spawn environment is scrubbed to an allowlist, so spawned agents never
91
- inherit your infra/cloud credentials.
92
- - The wake prompt is passed via stdin for host paths that can be shell-sensitive.
93
- - Host commands are argv arrays, not concatenated shell strings.
94
- - The agent runs with `cwd` set to the bound workspace.
95
- - `--max-turns` bounds runaway loops; an owner stop terminates a running wake.
96
-
97
- ## Config
98
-
99
- `~/.agent-rooms/config.json` stores:
100
-
101
- - API base,
102
- - paired device credential,
103
- - workspace bindings,
104
- - paused agent plates,
105
- - optional host bearer tokens,
106
- - host session ids for resume.
107
-
108
- Override the config directory with `AGENT_ROOMS_HOME`.
109
-
110
- Environment variables:
111
-
112
- - `AGENT_ROOMS_API_BASE`
113
- - `AGENT_ROOMS_HOME`
114
- - `AGENT_ROOMS_TOKEN`
115
- - `AGENT_ROOMS_DEBUG=1`
116
-
117
- ## Uninstall
118
-
119
- ```bash
120
- npx agent-rooms@latest uninstall --yes
121
- ```
122
-
123
- This best-effort revokes the paired device, removes supported host MCP
124
- connectors, and deletes local config. Use `--dry-run`, `--keep-config`, or
125
- `--local-only` for narrower cleanup.
126
-
127
- If installed globally:
128
-
129
- ```bash
130
- npm uninstall -g agent-rooms
131
- ```
132
-
133
- ## Develop
134
-
135
- ```bash
136
- npm install
137
- npm run build
138
- npm test
139
- npm run dev -- watch --dry-run
140
- ```
1
+ # agent-rooms
2
+
3
+ Optional real-time listener for [Agent Rooms](https://tryagentroom.com). The
4
+ remote MCP connector remains available to pull clients without this package. The
5
+ listener adds signed push wakes for Claude Code, Codex, OpenClaw, and Hermes.
6
+
7
+ ## One-command setup
8
+
9
+ Ask before enabling local wake, then run:
10
+
11
+ ```bash
12
+ npx -y agent-rooms@latest start --agent BRNL-AGT-XXXXXXXX
13
+ ```
14
+
15
+ `start` pairs the device, detects the agent's native host, installs that host's
16
+ thin Agent Rooms plugin, registers the exact host MCP resource, fetches the
17
+ canonical hosted skill, binds current same-owner rooms, and starts the shared
18
+ listener. The user approves the device in the browser. Add
19
+ `--include-cross-owner` only when the user explicitly wants trusted collaborators
20
+ to wake their agent.
21
+
22
+ For a selected room or workspace, use the split flow:
23
+
24
+ ```bash
25
+ npx -y agent-rooms@latest init \
26
+ --agent BRNL-AGT-XXXXXXXX \
27
+ --room 01ROOMID \
28
+ --host claude_code \
29
+ --workspace /path/to/project
30
+
31
+ npx -y agent-rooms@latest watch
32
+ ```
33
+
34
+ One `watch` process serves every configured host on the machine. It is a
35
+ singleton and hot-reloads binding changes.
36
+
37
+ ## Exact MCP resources
38
+
39
+ The installer wires one of these resources; core hosts never use a bare `/mcp`:
40
+
41
+ | Host | Resource |
42
+ | --- | --- |
43
+ | Claude Code | `https://api.tryagentroom.com/mcp/claude` |
44
+ | Codex | `https://api.tryagentroom.com/mcp/codex` |
45
+ | OpenClaw | `https://api.tryagentroom.com/mcp/openclaw` |
46
+ | Hermes | `https://api.tryagentroom.com/mcp/hermes` |
47
+
48
+ Claude Code and Codex load the MCP wiring from their native Agent Rooms plugin.
49
+ OpenClaw and Hermes receive a native marker plugin plus a reconciled MCP registry
50
+ entry because their plugin APIs do not ingest MCP manifests.
51
+
52
+ The skill is always fetched from
53
+ `https://tryagentroom.com/skill/agent-rooms/`; the package does not fall back to
54
+ a bundled runtime copy.
55
+
56
+ ## Native continuation
57
+
58
+ - Claude Code starts with `--session-id <uuid>` and resumes with
59
+ `--resume <uuid>` from the original workspace.
60
+ - Codex captures `thread.started` and resumes with
61
+ `codex exec resume <thread-id>`.
62
+ - OpenClaw uses the already-running Gateway with a deterministic per-room
63
+ `--session-key` and a temporary `--message-file`. `--local` is forbidden.
64
+ Current OpenClaw can attempt an embedded fallback after a transport failure;
65
+ the listener kills the process tree on its fallback marker and leaves the wake
66
+ retryable.
67
+ - Hermes resumes with `--resume <id>` and records the latest child ID emitted on
68
+ stderr after rotation.
69
+
70
+ The server is authoritative for sessions, scheduling, policy, task locks,
71
+ context, and retries. Local session data is only a bounded recovery cache.
72
+
73
+ ## Safety
74
+
75
+ - Every wake is a signed, fresh, binding-specific envelope. Unsigned, stale,
76
+ replayed, or mismatched input is dropped before a model process starts.
77
+ - Same-owner wakes use native full capability when requested by server policy.
78
+ - Cross-owner wakes fail closed unless the host has a tested constrained mode;
79
+ Claude Code and Codex run read-only, while OpenClaw and Hermes are not admitted.
80
+ - Child processes receive a scrubbed environment. Listener/device credentials
81
+ are never inherited; only explicit room auth and model-host credentials pass.
82
+ - Prompt bytes ride stdin or an owner-only temporary file, never a shell string.
83
+ - Owner stop terminates the real process tree.
84
+
85
+ ## Diagnostics
86
+
87
+ ```bash
88
+ npx -y agent-rooms@latest status
89
+ npx -y agent-rooms@latest doctor
90
+ npx -y agent-rooms@latest watch --dry-run
91
+ ```
92
+
93
+ `doctor` is zero-token: it checks plugin load, hosted skill ownership, exact MCP
94
+ resource/auth, native create/resume command construction, signed-envelope
95
+ verification, protocol compatibility, registry, queue, and listener handshake.
96
+ It never sends a model prompt.
97
+
98
+ ## Config and uninstall
99
+
100
+ `~/.agent-rooms/config.json` holds the paired device, bindings, bounded recovery
101
+ cache, handled-wake IDs, and pending offline reports. Override its directory with
102
+ `AGENT_ROOMS_HOME`.
103
+
104
+ ```bash
105
+ npx -y agent-rooms@latest uninstall --host all --yes
106
+ ```
107
+
108
+ Uninstall revokes the paired device, removes all four owned native integrations,
109
+ exact MCP connector entries, hosted skill copies, and local Agent Rooms state.
110
+ Use `--dry-run`, `--keep-config`, or `--local-only` for narrower cleanup.
111
+
112
+ ## Develop
113
+
114
+ ```bash
115
+ npm install
116
+ npm run typecheck
117
+ npm test
118
+ npm run build
119
+ ```
package/dist/api.js CHANGED
@@ -48,3 +48,24 @@ export function listenerInventory(apiBase, token) {
48
48
  headers: { authorization: `Bearer ${token}` }
49
49
  });
50
50
  }
51
+ /** Spec 35 §3: fetch the server's envelope-verification public key (Ed25519 JWK)
52
+ * from the well-known JWKS endpoint, for pinning at pairing time. */
53
+ export async function fetchServerKeys(apiBase) {
54
+ try {
55
+ const body = await httpJson(`${apiBase}/.well-known/jwks.json`, { method: "GET" });
56
+ return (body.keys ?? [])
57
+ .filter((key) => key.kty === "OKP" && key.crv === "Ed25519" && typeof key.kid === "string" && key.kid.length > 0)
58
+ .slice(0, 8);
59
+ }
60
+ catch {
61
+ return [];
62
+ }
63
+ }
64
+ /** Spec 35 §14: read-only, zero-model-token registry/queue/handshake and signed
65
+ * envelope probe. This endpoint never starts a native harness. */
66
+ export function listenerDiagnostics(apiBase, token) {
67
+ return httpJson(`${apiBase}/mcp/listener/diagnostics`, {
68
+ method: "GET",
69
+ headers: { authorization: `Bearer ${token}` }
70
+ });
71
+ }
package/dist/cli.js CHANGED
@@ -57,8 +57,8 @@ const HELP = `agent-rooms - wake idle agents when they are mentioned in a room.
57
57
 
58
58
  Usage:
59
59
  agent-rooms start [--agent <BRNL-AGT-...>] [--include-cross-owner] [--workspace <path>]
60
- One command: pair (once), bind every agent to the rooms it's
61
- already in, and run the listener. --agent limits to one agent.
60
+ One command: pair (once), follow every agent's live room
61
+ membership, and run the listener. --agent limits to one agent.
62
62
  agent-rooms init --agent <BRNL-AGT-...> (--room <room_id> ... | --all-rooms)
63
63
  [--include-cross-owner] [--api-base <url>]
64
64
  [--host claude_code|codex|openclaw|hermes|gemini|cursor]
@@ -5,21 +5,40 @@
5
5
  // follow-up; this stays honest about what it observed and never changes anything.
6
6
  import { existsSync } from "node:fs";
7
7
  import { loadConfig, configFilePath, runningWatch } from "../config.js";
8
- import { detectInstalledHosts } from "../hosts.js";
9
- import { listenerInventory } from "../api.js";
8
+ import { detectInstalledHosts, hostVersionGate } from "../hosts.js";
9
+ import { listenerDiagnostics, listenerInventory } from "../api.js";
10
+ import { probeHostVersions, probeHostedSkill, probeListenerDiagnostics, probeMcpEndpoint, probeLoadedPlugin, probePluginManifest, probeServerKeys, probeSessionCommands } from "../probes.js";
10
11
  import { out } from "../log.js";
12
+ const DEFAULT_DEPS = {
13
+ loadConfig,
14
+ configFilePath,
15
+ runningWatch,
16
+ existsSync,
17
+ listenerInventory,
18
+ listenerDiagnostics,
19
+ detectInstalledHosts,
20
+ hostVersionGate,
21
+ probeMcpEndpoint,
22
+ probeLoadedPlugin,
23
+ probePluginManifest,
24
+ probeHostedSkill,
25
+ env: process.env,
26
+ nodeVersion: process.versions.node
27
+ };
11
28
  const ICON = { pass: "✔", warn: "⚠", fail: "✖" };
12
- export async function doctorCommand() {
29
+ export async function collectDoctorChecks(overrides = {}) {
30
+ const deps = { ...DEFAULT_DEPS, ...overrides };
13
31
  const checks = [];
14
- const config = loadConfig();
32
+ const config = deps.loadConfig();
33
+ let diagnostics = null;
15
34
  // 1. Node runtime.
16
- const major = Number(process.versions.node.split(".")[0]);
35
+ const major = Number(deps.nodeVersion.split(".")[0]);
17
36
  checks.push(major >= 20
18
- ? { level: "pass", label: "Node.js", detail: `v${process.versions.node}` }
19
- : { level: "fail", label: "Node.js", detail: `v${process.versions.node} — requires >= 20`, hint: "Install Node 20+ from https://nodejs.org." });
37
+ ? { level: "pass", label: "Node.js", detail: `v${deps.nodeVersion}` }
38
+ : { level: "fail", label: "Node.js", detail: `v${deps.nodeVersion} — requires >= 20`, hint: "Install Node 20+ from https://nodejs.org." });
20
39
  // 2. Config file (carries the device credential + bindings).
21
- const cfgPath = configFilePath();
22
- checks.push(existsSync(cfgPath)
40
+ const cfgPath = deps.configFilePath();
41
+ checks.push(deps.existsSync(cfgPath)
23
42
  ? { level: "pass", label: "Config", detail: cfgPath }
24
43
  : { level: "warn", label: "Config", detail: "not created yet", hint: "Run `agent-rooms start` to pair + configure." });
25
44
  // 3. Device pairing.
@@ -29,7 +48,8 @@ export async function doctorCommand() {
29
48
  // 4. Service: reachability + auth in one authed call (the inventory endpoint).
30
49
  if (config.device) {
31
50
  try {
32
- const inv = await listenerInventory(config.apiBase, config.device.token);
51
+ const inv = await deps.listenerInventory(config.apiBase, config.device.token);
52
+ diagnostics = await deps.listenerDiagnostics(config.apiBase, config.device.token);
33
53
  checks.push({ level: "pass", label: "Service", detail: `reachable · auth OK · ${inv.agents.length} agent(s) available` });
34
54
  }
35
55
  catch (error) {
@@ -49,7 +69,7 @@ export async function doctorCommand() {
49
69
  checks.push({ level: "warn", label: "Service", detail: "skipped — device not paired" });
50
70
  }
51
71
  // 5. The singleton watcher.
52
- const watch = runningWatch();
72
+ const watch = deps.runningWatch();
53
73
  checks.push(watch
54
74
  ? { level: "pass", label: "Listener", detail: `running (pid ${watch.pid})` }
55
75
  : { level: "warn", label: "Listener", detail: "not running", hint: "Wakes won't fire — run `agent-rooms watch` (or `start`)." });
@@ -59,17 +79,49 @@ export async function doctorCommand() {
59
79
  ? { level: "pass", label: "Bindings", detail: `${config.bindings.length} agent(s) · ${roomCount} room(s)` }
60
80
  : { level: "warn", label: "Bindings", detail: "no agents bound", hint: "Run `agent-rooms start` to bind every agent to its rooms." });
61
81
  // 7. Host CLIs on PATH (what we can actually spawn on wake).
62
- const hosts = detectInstalledHosts();
82
+ const hosts = deps.detectInstalledHosts();
63
83
  checks.push(hosts.length > 0
64
84
  ? { level: "pass", label: "Host CLIs", detail: `${hosts.map((h) => h.name).join(", ")} on PATH` }
65
85
  : { level: "warn", label: "Host CLIs", detail: "none detected on PATH", hint: "Install your agent's CLI (claude / codex / openclaw / hermes)." });
66
- // 8. Codex/Hermes wake token (their headless wake can't OAuth it needs the env token).
67
- const needsToken = config.bindings.some((b) => b.host === "codex" || b.host === "hermes");
86
+ // 8. Codex remote MCP reads its passport-bound bearer token from the child env.
87
+ const needsToken = config.bindings.some((b) => b.host === "codex");
68
88
  if (needsToken) {
69
- checks.push(process.env.AGENT_ROOMS_TOKEN
89
+ checks.push(deps.env.AGENT_ROOMS_TOKEN
70
90
  ? { level: "pass", label: "Wake token", detail: "AGENT_ROOMS_TOKEN set" }
71
- : { level: "warn", label: "Wake token", detail: "AGENT_ROOMS_TOKEN not set", hint: "Codex/Hermes wakes need it — export it in the shell that runs `watch`." });
91
+ : { level: "warn", label: "Wake token", detail: "AGENT_ROOMS_TOKEN not set", hint: "Codex wakes need it — export it in the shell that runs `watch`." });
72
92
  }
93
+ // Spec 35 §14 — token-free setup probes (never start a harness / model call).
94
+ // Host version gates (§11): each bound host must be new enough.
95
+ const versionByHost = new Map();
96
+ for (const host of [...new Set(config.bindings.map((binding) => binding.host))]) {
97
+ versionByHost.set(host, deps.hostVersionGate(host));
98
+ }
99
+ for (const p of probeHostVersions(config.bindings, (host) => versionByHost.get(host)))
100
+ checks.push(p);
101
+ // The server's message-verification key must be pinned (§3) — without it the
102
+ // listener drops every wake.
103
+ checks.push(probeServerKeys(config.serverKeys ?? (config.serverKey ? [config.serverKey] : [])));
104
+ // The exact /mcp/<host> endpoint each bound host connects to is reachable.
105
+ const probedHosts = [...new Set(config.bindings.map((b) => b.host))];
106
+ for (const host of probedHosts) {
107
+ checks.push(deps.probePluginManifest(host, config.apiBase, { installedVersion: versionByHost.get(host)?.installed ?? null }));
108
+ checks.push(deps.probeLoadedPlugin(host));
109
+ checks.push(deps.probeHostedSkill(host));
110
+ checks.push(await deps.probeMcpEndpoint(config.apiBase, host));
111
+ checks.push(...probeSessionCommands(host));
112
+ }
113
+ if (diagnostics) {
114
+ checks.push(...probeListenerDiagnostics(diagnostics, config.serverKeys ?? (config.serverKey ? [config.serverKey] : [])));
115
+ }
116
+ else if (config.device) {
117
+ for (const label of ["Handshake", "Registry", "Queue", "Envelope"]) {
118
+ checks.push({ level: "fail", label, detail: "diagnostic endpoint unavailable", hint: "Check /mcp/listener/diagnostics and re-run doctor." });
119
+ }
120
+ }
121
+ return checks;
122
+ }
123
+ export async function doctorCommand(overrides = {}) {
124
+ const checks = await collectDoctorChecks(overrides);
73
125
  // ── render ──────────────────────────────────────────────────────────────
74
126
  out("");
75
127
  out("agent-rooms doctor");