agent-peers 0.1.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.
@@ -0,0 +1,293 @@
1
+ """Bounded subprocess transport and read-only runtime dependency checks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from concurrent.futures import Future, TimeoutError
6
+ from contextvars import copy_context
7
+ import json
8
+ import logging
9
+ import math
10
+ import os
11
+ from pathlib import Path
12
+ from queue import Empty, Full, Queue
13
+ import re
14
+ import select
15
+ import shlex
16
+ import shutil
17
+ import subprocess
18
+ import threading
19
+ import time
20
+
21
+ logger = logging.getLogger(__name__)
22
+ MAX_LINE = 1_000_000
23
+ QUEUE_MAX = 50
24
+ PENDING_MAX = 64
25
+ ROOT = Path(__file__).resolve().parent.parent
26
+ _EXITED = "agent-peers bridge exited; delivery is unknown and was not retried"
27
+ _TIMED_OUT = "agent-peers request timed out; delivery is unknown and was not retried"
28
+
29
+
30
+ def context_thread(target, *, name, args=()):
31
+ """Carry public contextvars (including the host profile) into daemon workers."""
32
+ context = copy_context()
33
+ return threading.Thread(target=context.run, args=(target, *args), name=name, daemon=True)
34
+
35
+
36
+ def bridge_environment():
37
+ # Discovery paths only; provider credentials and Node preload hooks stay out.
38
+ return {key: os.environ[key] for key in (
39
+ "PATH", "HOME", "XDG_RUNTIME_DIR", "CLAUDE_CONFIG_DIR", "CODEX_HOME",
40
+ "AGENT_PEERS_HOME", "AGENT_PEERS_CODEX_APP_SERVER",
41
+ ) if key in os.environ}
42
+
43
+
44
+ def default_command(*, root=ROOT, hermes_home=None):
45
+ """Resolve a usable system or Hermes-managed Node without changing either."""
46
+ home = Path(hermes_home or os.environ.get("HERMES_HOME") or Path.home() / ".hermes")
47
+ homes = [home]
48
+ if home.parent.name == "profiles":
49
+ homes.append(home.parent.parent)
50
+ homes.append(Path.home() / ".hermes")
51
+ candidates = [str(path / suffix) for path in homes for suffix in ("node/bin/node", "node/node")]
52
+ if system := shutil.which("node"):
53
+ candidates.append(system)
54
+ env = bridge_environment()
55
+ deadline = time.monotonic() + 5
56
+ node = None
57
+ for candidate in dict.fromkeys(candidates):
58
+ if not os.access(candidate, os.X_OK) or not Path(candidate).is_file():
59
+ continue
60
+ remaining = deadline - time.monotonic()
61
+ if remaining <= 0:
62
+ break
63
+ try:
64
+ probe = subprocess.run([candidate, "--version"], stdout=subprocess.PIPE,
65
+ stderr=subprocess.DEVNULL, env=env, timeout=min(1, remaining), check=False)
66
+ except (OSError, subprocess.TimeoutExpired):
67
+ continue
68
+ match = re.fullmatch(rb"v(\d+)\.\d+\.\d+\s*", probe.stdout)
69
+ if probe.returncode == 0 and match and int(match[1]) >= 22:
70
+ node = str(Path(candidate).absolute())
71
+ break
72
+ if node is None:
73
+ raise RuntimeError("agent-peers requires Node.js 22 or newer on PATH or in the Hermes-managed node directory")
74
+ bridge = root / "bin" / "agent-peers.mjs"
75
+ if not bridge.is_file():
76
+ raise RuntimeError(f"agent-peers bridge is missing at {bridge}; install the whole repository as a Hermes plugin")
77
+ # Import the actual bridge module, without starting its stdin server. This
78
+ # catches missing and incompatible dependencies before the first request.
79
+ try:
80
+ probe = subprocess.run([node, "--input-type=module", "-e", "await import(process.argv[1])",
81
+ (root / "src" / "hermes-bridge.mjs").as_uri()],
82
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
83
+ env=env, timeout=5, check=False)
84
+ except (OSError, subprocess.TimeoutExpired):
85
+ raise RuntimeError("agent-peers bridge dependency check failed or timed out") from None
86
+ if probe.returncode:
87
+ command = shlex.join(["npm", "--prefix", str(root), "ci", "--omit=dev", "--ignore-scripts"])
88
+ raise RuntimeError(f"agent-peers bridge dependencies are unavailable; run: {command}")
89
+ return [node, str(bridge), "hermes-bridge"]
90
+
91
+
92
+ class BridgeClient:
93
+ """One subprocess per plugin/profile; writes and responses share a deadline."""
94
+
95
+ def __init__(self, command: list[str], receive):
96
+ if not isinstance(command, list) or not command or not all(isinstance(x, str) and x for x in command):
97
+ raise ValueError("agent-peers bridge_command must be a nonempty executable/argument list")
98
+ self.command = list(command)
99
+ self.receive = receive
100
+ self._environment = bridge_environment()
101
+ self._lock = threading.RLock()
102
+ self._write_lock = threading.Lock()
103
+ self._process = None
104
+ self._reader = None
105
+ self._pending = {}
106
+ self._next = 0
107
+ self._stopped = False
108
+ self._messages = Queue(maxsize=QUEUE_MAX)
109
+ self._delivery = context_thread(self._deliver, name="agent-peers-delivery")
110
+ self._delivery.start()
111
+
112
+ def _start(self):
113
+ # Called under both locks. Only this path creates processes or reuses FDs.
114
+ if self._stopped:
115
+ raise RuntimeError("agent-peers plugin has been unloaded")
116
+ if self._process is not None:
117
+ if self._process.poll() is None:
118
+ return self._process
119
+ self._process.stdin.close()
120
+ process = subprocess.Popen(self.command, stdin=subprocess.PIPE, stdout=subprocess.PIPE,
121
+ stderr=subprocess.DEVNULL, env=self._environment)
122
+ os.set_blocking(process.stdin.fileno(), False)
123
+ self._process = process
124
+ self._reader = context_thread(self._read, name="agent-peers-reader", args=(process,))
125
+ self._reader.start()
126
+ return process
127
+
128
+ def _fail_pending(self, process, message):
129
+ with self._lock:
130
+ for owner, future in list(self._pending.values()):
131
+ if (process is None or owner is process) and not future.done():
132
+ future.set_exception(RuntimeError(message))
133
+
134
+ def _write(self, process, data, deadline):
135
+ view = memoryview(data)
136
+ while view:
137
+ remaining = deadline - time.monotonic()
138
+ if remaining <= 0:
139
+ raise TimeoutError()
140
+ if self._stopped or process.poll() is not None:
141
+ raise BrokenPipeError()
142
+ fd = process.stdin.fileno()
143
+ if not select.select([], [fd], [], min(remaining, 0.1))[1]:
144
+ continue
145
+ try:
146
+ written = os.write(fd, view)
147
+ except BlockingIOError:
148
+ continue
149
+ if written <= 0:
150
+ raise BrokenPipeError()
151
+ view = view[written:]
152
+
153
+ def call(self, method: str, params: dict | None = None, *, timeout: float = 25):
154
+ if not math.isfinite(timeout) or timeout <= 0:
155
+ raise ValueError("agent-peers timeout must be positive and finite")
156
+ deadline = time.monotonic() + timeout
157
+ request_id = None
158
+ if not self._write_lock.acquire(timeout=timeout):
159
+ raise RuntimeError("agent-peers bridge is busy; no message was sent")
160
+ try:
161
+ with self._lock:
162
+ if len(self._pending) >= PENDING_MAX:
163
+ raise RuntimeError("agent-peers has too many pending requests; no message was sent")
164
+ self._next += 1
165
+ request_id = self._next
166
+ data = (json.dumps({"id": request_id, "method": method, "params": params or {}},
167
+ ensure_ascii=False) + "\n").encode("utf-8")
168
+ if len(data) > MAX_LINE:
169
+ raise ValueError("agent-peers request exceeds the message size limit")
170
+ process = self._start()
171
+ future = Future()
172
+ self._pending[request_id] = (process, future)
173
+ try:
174
+ self._write(process, data, deadline)
175
+ except (TimeoutError, OSError, ValueError) as exc:
176
+ # A partially written frame cannot be reused safely.
177
+ try:
178
+ process.terminate()
179
+ except OSError:
180
+ pass
181
+ message = _TIMED_OUT if isinstance(exc, TimeoutError) else _EXITED
182
+ self._fail_pending(process, message)
183
+ raise RuntimeError(message) from None
184
+ except BaseException:
185
+ with self._lock:
186
+ self._pending.pop(request_id, None)
187
+ raise
188
+ finally:
189
+ self._write_lock.release()
190
+ try:
191
+ return future.result(timeout=max(0, deadline - time.monotonic()))
192
+ except TimeoutError:
193
+ raise RuntimeError(_TIMED_OUT) from None
194
+ finally:
195
+ with self._lock:
196
+ self._pending.pop(request_id, None)
197
+
198
+ def _read(self, process):
199
+ try:
200
+ while not self._stopped:
201
+ line = process.stdout.readline(MAX_LINE + 1)
202
+ if not line:
203
+ break
204
+ if len(line) > MAX_LINE or not line.endswith(b"\n"):
205
+ logger.warning("agent-peers bridge exceeded the protocol line limit")
206
+ break
207
+ try:
208
+ result = json.loads(line.decode("utf-8"))
209
+ except (ValueError, UnicodeError):
210
+ logger.warning("agent-peers bridge returned invalid JSON")
211
+ break
212
+ if not isinstance(result, dict):
213
+ continue
214
+ if result.get("method") == "peer_message":
215
+ try:
216
+ self._messages.put_nowait(result.get("params", {}))
217
+ except Full:
218
+ logger.warning("agent-peers incoming queue is full; dropped peer message")
219
+ continue
220
+ request_id = result.get("id")
221
+ if type(request_id) is not int:
222
+ continue
223
+ with self._lock:
224
+ pending = self._pending.get(request_id)
225
+ if pending and pending[0] is process and not pending[1].done():
226
+ error = result.get("error")
227
+ if error:
228
+ text = error.get("message", "bridge request failed") if isinstance(error, dict) else str(error)
229
+ pending[1].set_exception(RuntimeError(text))
230
+ else:
231
+ pending[1].set_result(result.get("result"))
232
+ except (OSError, ValueError):
233
+ logger.warning("agent-peers bridge connection closed")
234
+ finally:
235
+ self._fail_pending(process, _EXITED)
236
+ # A broken stdout protocol must not leave an orphaned subprocess.
237
+ try:
238
+ if process.poll() is None:
239
+ process.terminate()
240
+ process.wait(timeout=0.5)
241
+ except subprocess.TimeoutExpired:
242
+ process.kill()
243
+ process.wait(timeout=0.5)
244
+ except OSError:
245
+ pass
246
+ process.stdout.close()
247
+
248
+ def _deliver(self):
249
+ while not self._stopped:
250
+ try:
251
+ message = self._messages.get(timeout=0.1)
252
+ except Empty:
253
+ continue
254
+ if self._stopped:
255
+ break
256
+ try:
257
+ self.receive(message)
258
+ except Exception:
259
+ # Never log peer bodies, which may contain task data.
260
+ logger.warning("agent-peers message could not be delivered", exc_info=False)
261
+
262
+ def close(self):
263
+ with self._lock:
264
+ self._stopped = True
265
+ process = self._process
266
+ reader = self._reader
267
+ self._fail_pending(None, "agent-peers plugin has been unloaded")
268
+ if process:
269
+ # Writers poll the stop flag every 100ms and release this lock.
270
+ if self._write_lock.acquire(timeout=0.5):
271
+ try:
272
+ process.stdin.close()
273
+ finally:
274
+ self._write_lock.release()
275
+ try:
276
+ process.wait(timeout=0.5)
277
+ except subprocess.TimeoutExpired:
278
+ process.terminate()
279
+ try:
280
+ process.wait(timeout=0.5)
281
+ except subprocess.TimeoutExpired:
282
+ process.kill()
283
+ process.wait(timeout=0.5)
284
+ except OSError:
285
+ pass
286
+ for worker in (reader, self._delivery):
287
+ if worker and worker is not threading.current_thread():
288
+ worker.join(timeout=0.5)
289
+ while True:
290
+ try:
291
+ self._messages.get_nowait()
292
+ except Empty:
293
+ break
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+ import { allPeers } from "../src/wire.mjs";
3
+ import { formatSessionPeer } from "../src/discovery.mjs";
4
+ import { VERSION } from "../src/version.mjs";
5
+
6
+ const usage = `agent-peers ${VERSION} — local agent messaging
7
+
8
+ Usage: agent-peers <command> [options]
9
+
10
+ list [--json] Discover running Claude Code and Codex inboxes
11
+ doctor [--json] Check local prerequisites without changing configuration
12
+ hermes-bridge Start the plugin-managed JSON-lines subprocess
13
+ --version Print the installed version
14
+ --help Show this help
15
+
16
+ Codex MCP server: codex-peer
17
+ Documentation: https://github.com/ThomasMarcelis/agent-peers`;
18
+
19
+ process.stdout.on("error", (error) => {
20
+ if (error.code === "EPIPE") process.exit(0);
21
+ console.error(`agent-peers: ${error.message}`);
22
+ process.exit(1);
23
+ });
24
+
25
+ async function main() {
26
+ const [cmd, ...args] = process.argv.slice(2);
27
+ if (!cmd || ["--help", "-h", "help"].includes(cmd)) {
28
+ if (args.length) throw new SyntaxError("unexpected arguments");
29
+ console.log(usage);
30
+ return;
31
+ }
32
+ if (["--version", "-v", "version"].includes(cmd) && !args.length) {
33
+ console.log(VERSION);
34
+ return;
35
+ }
36
+ if (!["list", "doctor", "hermes-bridge"].includes(cmd) || args.length > 1 ||
37
+ (args.length && (args[0] !== "--json" || cmd === "hermes-bridge"))) {
38
+ throw new SyntaxError("unknown command or option; run agent-peers --help");
39
+ }
40
+ if (cmd === "hermes-bridge") {
41
+ const { runHermesBridge } = await import("../src/hermes-bridge.mjs");
42
+ runHermesBridge();
43
+ return;
44
+ }
45
+ if (cmd === "doctor") {
46
+ const { diagnose } = await import("../src/diagnostics.mjs");
47
+ const result = diagnose();
48
+ if (args.includes("--json")) console.log(JSON.stringify(result, null, 2));
49
+ else {
50
+ console.log(`agent-peers ${VERSION}`);
51
+ for (const check of result.checks) console.log(`${check.status.toUpperCase()} ${check.name}: ${check.message}`);
52
+ }
53
+ if (!result.ok) process.exitCode = 1;
54
+ return;
55
+ }
56
+ const peers = allPeers();
57
+ if (args.includes("--json")) console.log(JSON.stringify({ peers }, null, 2));
58
+ else {
59
+ if (!peers.length) console.log("No agent sessions are running.");
60
+ for (const peer of peers) console.log(formatSessionPeer(peer));
61
+ if (peers.length) console.log("Codex native reachability depends on your session; call list_peers there for native targets.");
62
+ }
63
+ }
64
+
65
+ try { await main(); }
66
+ catch (error) {
67
+ console.error(`agent-peers: ${error.message}`);
68
+ process.exitCode = error instanceof SyntaxError ? 2 : 1;
69
+ }
@@ -0,0 +1,38 @@
1
+ # Compatibility
2
+
3
+ agent-peers 0.1.0 is an initial release. Its CLI and MCP tools are small, but the agent runtimes it connects expose version-sensitive interfaces. A newer agent release is not automatically compatible.
4
+
5
+ ## Runtime requirements
6
+
7
+ | Component | Requirement |
8
+ | --- | --- |
9
+ | Node.js | 22 or later; CI covers 22, 24, and 26 |
10
+ | Operating system | Linux and macOS; local Unix sockets are required |
11
+ | Python (Hermes only) | 3.11–3.13, as supported by the tested Hermes host |
12
+ | Claude Code | Cross-session inbox and `SendMessage` support |
13
+ | Codex | Shared app-server daemon, thread identity in MCP call metadata, and live `turn/start` delivery |
14
+ | Hermes | Native plugin loader plus `session_message_route`, `inject_session_message`, and route-retirement hook |
15
+
16
+ Windows is not supported. WSL must run all participating agents inside the same Linux environment. CI on macOS verifies the transport and package; live agent interoperability was exercised on Linux.
17
+
18
+ ## Tested agent builds
19
+
20
+ - Codex CLI **0.160.0**.
21
+ - Claude Code **2.1.288**.
22
+ - Hermes fork [ThomasMarcelis/hermes-agent](https://github.com/ThomasMarcelis/hermes-agent) at [`1afb5d16d2d25e1d8e13703c1ce946e3ca0adc0d`](https://github.com/ThomasMarcelis/hermes-agent/commit/1afb5d16d2d25e1d8e13703c1ce946e3ca0adc0d).
23
+
24
+ The pinned Hermes host is reproducible in CI. It is a fork dependency, not a claim that any upstream version with the same version number includes these APIs.
25
+
26
+ Upstream Hermes was inspected at [`5d3c05977bb3c8b7cfd6b3e39d96f6e35a9e0662`](https://github.com/NousResearch/hermes-agent/commit/5d3c05977bb3c8b7cfd6b3e39d96f6e35a9e0662). Its public `inject_message` API cannot atomically pin a late reply to the original conversation across reset and compression. Gateway and TUI injection queue busy-session input rather than steering it. The plugin therefore rejects sends on that host instead of weakening its routing contract.
27
+
28
+ ## What to expect
29
+
30
+ A Codex inbox binds on the session's first `list_peers` or `send_peer` call. Threads hosted in embedded servers, including sessions started with `-c` or `--no-daemon`, are not reachable through the shared daemon. A subagent sends under its root's address; replies return to the root. Native targets only refer to agents in the caller's own tree.
31
+
32
+ Claude's inbox protocol is undocumented. Inbound messages may be held for approval or rejected according to Claude settings. In particular, `bypassPermissions` does not imply accepting external messages. The package does not change that policy.
33
+
34
+ Hermes support is experimental. Tests verify plugin loading, profile isolation, and busy/idle replies to the originating Discord thread on the pinned fork. CLI and Desktop/TUI reset/compression behavior is not covered by this release's compatibility guarantee: the current fork has a Desktop idle-dispatch/reset race that requires a separate host fix. No upstream submission or host upgrade is part of installing this package.
35
+
36
+ Claude sessions may use different private socket directories on the same machine. Codex maintains a reply inbox in each verified directory; Hermes selects a private reply inbox in the destination's directory. Existing owned inboxes survive discovery changes, and a missing Codex inbox can be repaired on the next tool call without resetting the conversation.
37
+
38
+ Run the opt-in live checks after upgrading an agent. See [contribution guidance](../CONTRIBUTING.md) and [release verification](release-verification.md).
package/docs/hermes.md ADDED
@@ -0,0 +1,55 @@
1
+ # Hermes plugin (experimental)
2
+
3
+ The repository is a native Hermes plugin and an npm package. Both use the same Node transport. Hermes starts its own bridge subprocess lazily; a separate standalone daemon is unnecessary.
4
+
5
+ ## Prerequisites
6
+
7
+ Use the [verified Hermes fork](compatibility.md#tested-agent-builds), Python 3.11–3.13, and Node.js 22+. Stock Hermes currently lacks the exact-conversation route APIs. Discovery remains available there, but sending returns an unsupported-host error.
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ hermes plugins install ThomasMarcelis/agent-peers --enable
13
+ ```
14
+
15
+ For a reproducible release install, add `--ref` followed by the full 40-character commit from the [GitHub release](https://github.com/ThomasMarcelis/agent-peers/releases/tag/v0.1.0). Hermes does not accept tag names for this flag. Use `hermes --profile NAME plugins install ...` for a named profile. Current Hermes installers can offer to install the plugin's Node dependencies; accept that step. Older fork installers need a separate install in the plugin directory printed by Hermes:
16
+
17
+ ```sh
18
+ npm --prefix /absolute/plugin/directory ci --omit=dev --ignore-scripts
19
+ ```
20
+
21
+ Replace `/absolute/plugin/directory` with the actual installed directory. The plugin reports the exact recovery command if Node or dependencies are unavailable. There are no install-time scripts in agent-peers.
22
+
23
+ Grant injection explicitly for each selected profile using Hermes configuration:
24
+
25
+ ```sh
26
+ hermes config set plugins.entries.agent-peers.allow_gateway_injection true
27
+ ```
28
+
29
+ This allows automatic peer messages to start or steer the original conversation. For messaging gateways, an agent response may be posted to its existing chat/thread. The plugin does not grant this permission itself. New hosts/conversations may be needed to load changed plugin tools; follow your host's normal restart workflow.
30
+
31
+ If saved platform tool selections exclude new plugins, enable the `agent-peers` toolset for the intended platform using `hermes tools`. Preserve other platform selections.
32
+
33
+ ## Behavior
34
+
35
+ `list_peers()` returns Claude Code and Codex inboxes without publishing Hermes. The first `send_peer(to, message)` creates a private inbox for that conversation. Peers reply to its received address through their normal messaging tools.
36
+
37
+ - Every conversation and profile has an independent reply route.
38
+ - Busy conversations receive input through the host's steering boundary; idle conversations start a turn.
39
+ - Routes belong to the originating host conversation. Unload or host shutdown closes their inboxes; Hermes must send again to establish fresh return addresses.
40
+ - Discord replies return to the original thread. Switching chats does not change the return destination.
41
+ - There is no transcript scraping, history polling, offline mailbox, or automatic retry of an ambiguous send.
42
+
43
+ Discord gateway routing is the verified surface. CLI and Desktop/TUI lifecycle behavior remains experimental on the current fork; see the [host limitation](compatibility.md#what-to-expect).
44
+
45
+ ## Configuration
46
+
47
+ An optional `bridge_command` list under `plugins.entries.agent-peers.settings` overrides the executable and arguments. Use absolute paths. Normal installs resolve Node and the bridge within the installed plugin; no checkout path is required.
48
+
49
+ The bridge inherits only environment needed for discovery and execution. Provider credentials are not passed to it. It reads Claude session registry metadata and agent-peers registry entries, communicates over local sockets, and keeps no transcript archive. Agent replies may themselves invoke the model providers configured in those agents.
50
+
51
+ ## Update and remove
52
+
53
+ Use Hermes's native plugin manager to update or remove the plugin. For an explicitly pinned release, reinstall with the new release ref after reviewing its compatibility notes. The legacy Python installer is a compatibility helper; native installation is the supported path.
54
+
55
+ The optional generic Hermes host extension is separate from this package. It adds conversation-route capabilities for any plugin; peer discovery and messaging remain entirely here.
@@ -0,0 +1,66 @@
1
+ # Reference
2
+
3
+ ## CLI
4
+
5
+ | Command | Output |
6
+ | --- | --- |
7
+ | `agent-peers list` | Sessions with exact Claude `SendMessage` and Codex `send_peer` targets |
8
+ | `agent-peers list --json` | `{ "peers": [...] }`; no status text mixed into stdout |
9
+ | `agent-peers doctor [--json]` | Runtime, dependency, and local path checks; does not modify configuration or send messages |
10
+ | `agent-peers --version` | Package version |
11
+ | `agent-peers --help` | Usage |
12
+ | `codex-peer` | Codex MCP server over stdio |
13
+ | `agent-peers hermes-bridge` | Plugin-managed JSON-lines subprocess |
14
+
15
+ Exit codes: `0` success, `1` runtime/diagnostic failure, `2` invalid CLI usage. Missing agent state is a diagnostic warning because running only one integration is valid. A present socket does not prove compatibility or model readiness.
16
+
17
+ ## Tools
18
+
19
+ `list_peers()` takes no arguments. Codex's structured result contains `peers`, each with its name, kind, working directory, available activity status, and native/bridge route. Native reachability is caller-relative: another root is bridge-only even on the same daemon. Discovery does not load historical Codex threads.
20
+
21
+ `send_peer({to, message})` accepts an exact peer name, unambiguous abbreviated name, or a valid inbox address. Prefer returned exact targets. Empty messages, ambiguous targets, self-sends, and invalid addresses fail clearly. Hermes host handlers return JSON text as required by its dispatcher; Codex uses MCP text content and error results.
22
+
23
+ A successful socket write confirms only inbox acceptance. Claude delivery receipts can report held/rejected input; not every recipient sends a receipt. No receipt is not proof of model consumption. Timed-out delivery is not automatically retried because it may already have arrived.
24
+
25
+ ## Environment
26
+
27
+ | Variable | Purpose |
28
+ | --- | --- |
29
+ | `CLAUDE_CONFIG_DIR` | Claude configuration directory used to locate session registry metadata |
30
+ | `CODEX_HOME` | Codex home used to locate its shared daemon |
31
+ | `AGENT_PEERS_HOME` | agent-peers registry directory |
32
+ | `AGENT_PEERS_CODEX_APP_SERVER` | Explicit absolute path to an app-server Unix socket |
33
+ | `AGENT_PEERS_LOG` | Optional absolute log file for `codex-peer` |
34
+
35
+ Defaults use the current OS user's standard agent directories. Every override should be absolute. Package-owned socket and registry state must have private ownership/permissions. The transport is local and same-user; it is not a network service.
36
+
37
+ ## Hermes subprocess protocol
38
+
39
+ Requests are newline-delimited JSON objects `{id, method, params}`. Responses are `{id, result}` or `{id, error}`. Methods:
40
+
41
+ - `list_peers`: no parameters.
42
+ - `send_peer`: `session`, `name` (a `hermes:` identity), `to`, `message`.
43
+ - `close_session`: `session`.
44
+ - `shutdown`: close owned state and exit.
45
+
46
+ `peer_message` notifications identify the private session, message ID, sender, reply address, and body. Hermes creates no public registry entry. EOF and termination remove owned sockets. Standard output carries protocol data only; diagnostics go to standard error.
47
+
48
+ Messages use Claude's `<cross-session-message>` envelope. Incoming agent input is reframed with sender identity, a reply address, and a reminder that it carries no user approval. Closing tags in message bodies are escaped. A frame is bounded to 1,000,000 bytes. Resource bounds, token buckets, and duplicate suppression limit accidental floods; see the source constants for precise limits.
49
+
50
+ ## Troubleshooting
51
+
52
+ - **No Codex inbox:** call `list_peers` in that session; check that it uses the shared daemon.
53
+ - **Wrong native target:** rediscover from the sending Codex session. A thread UUID alone does not establish native reachability.
54
+ - **Held Claude delivery:** review its inbound-message policy; agent-peers does not bypass it.
55
+ - **Hermes unsupported route:** use the documented host revision. Upstream `inject_message` is not an equivalent fallback.
56
+ - **Missing plugin dependencies:** run the exact install command in the plugin error, then reload through the host's supported lifecycle.
57
+ - **Permission failure:** inspect directory ownership and modes. Do not make the socket directory globally writable.
58
+ - **Old reply address:** ask the peer to discover or send again after the original conversation closes; addresses are not durable mailboxes.
59
+
60
+ ## Updating active integrations
61
+
62
+ Keep production launchers pointed at a tested installed snapshot, separate from development checkouts. Running Node processes cache their modules: changing a file does not update an existing sidecar.
63
+
64
+ Use the agent host's supported MCP refresh with the new snapshot's launcher. Codex can preserve conversations and active work while replacing an MCP connection; an already captured tool step may still use the previous connection. A live legacy inbox owner blocks unsafe takeover. Verify the new registry owner, all inbox paths, and a real reply before considering an update complete.
65
+
66
+ Missing Codex inboxes repair on the next tool call only while the sidecar still owns its registry token. An inbox owned by a replacement is never reclaimed. A Hermes bridge restart retires its private reply addresses, so the Hermes conversation must send again to establish a fresh return route.
@@ -0,0 +1,31 @@
1
+ # Release verification
2
+
3
+ Verified on 2026-10-03 for 0.1.0. Raw live transcripts are excluded from the repository and package.
4
+
5
+ ## Automated checks
6
+
7
+ | Check | Result |
8
+ | --- | --- |
9
+ | JavaScript transport, MCP, discovery and CLI | 62 tests |
10
+ | Portable Python plugin and subprocess | 25 tests |
11
+ | Pinned Hermes loader and Discord routing | 5 tests |
12
+ | Packed npm artifact | 29 files; installed CLI and MCP handshake passed |
13
+ | Production dependency audit | 0 known vulnerabilities |
14
+
15
+ Regressions cover concurrent socket directories, destination-specific reply addresses, legacy sidecar takeover, missing-inbox repair, and an injected replacement race between validation and socket publication. An independent Opus 5.5 review identified lifecycle issues addressed by these changes.
16
+
17
+ All ten jobs in the [transport validation run](https://github.com/ThomasMarcelis/agent-peers/actions/runs/37149288716) passed: Linux/macOS on Node 22, 24 and 26, Python 3.11–3.13, and the pinned-Hermes integration. See [CI](https://github.com/ThomasMarcelis/agent-peers/actions/workflows/ci.yml) for the release commit. A credential-pattern scan of reachable Git history found no potential credentials.
18
+
19
+ ## Agent compatibility
20
+
21
+ Live verification used Node 22.23.1, Codex CLI 0.160.0, and Claude Code 2.1.288 on Linux, with disposable sessions and isolated peer registries:
22
+
23
+ - All 12 directed request/reply routes among two Claude and two Codex sessions passed.
24
+ - All four sessions discovered their expected peers.
25
+ - Native Codex subagent discovery and messaging passed.
26
+ - All four Hermes bridge round trips passed: idle and busy Codex, and idle and busy Claude, with private-inbox lifecycle and discovery checks.
27
+ - A shared design task negotiated ownership, completed independent drafts, and coordinated a changed requirement. No peer messages were sent during the independent phase; this is an observation, not a quota.
28
+
29
+ Deterministic Discord tests exercise the real adapter path with fake model and network calls. They do not claim a production Discord API test. Host requirements and experimental Hermes surfaces are listed in [compatibility](compatibility.md).
30
+
31
+ The generic Hermes host extension remains separate and unsubmitted. Its local preparation does not make stock upstream Hermes supported.
@@ -0,0 +1,15 @@
1
+ """Compatibility entrypoint for existing checkout symlinks; install the repo root."""
2
+
3
+ import importlib.util
4
+ from pathlib import Path
5
+ import sys
6
+
7
+ _package = Path(__file__).resolve().parent.parent / "agent_peers"
8
+ _name = __name__ + "._implementation"
9
+ _spec = importlib.util.spec_from_file_location(_name, _package / "__init__.py",
10
+ submodule_search_locations=[str(_package)])
11
+ _module = importlib.util.module_from_spec(_spec)
12
+ sys.modules[_name] = _module
13
+ _spec.loader.exec_module(_module)
14
+ BridgeClient, PeerTools, register = _module.BridgeClient, _module.PeerTools, _module.register
15
+ __all__ = ["BridgeClient", "PeerTools", "register"]
@@ -0,0 +1,13 @@
1
+ name: agent-peers
2
+ version: "0.1.0"
3
+ description: "Discover and message local Claude Code and Codex sessions from an unlisted Hermes inbox."
4
+ manifest_version: 1
5
+ provides_tools:
6
+ - list_peers
7
+ - send_peer
8
+ provides_hooks:
9
+ - on_session_message_route_closed
10
+ config_schema:
11
+ bridge_command:
12
+ type: list
13
+ description: "Executable and arguments for the agent-peers hermes-bridge subprocess."
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "agent-peers",
3
+ "version": "0.1.0",
4
+ "description": "Local peer messaging for Claude Code, Codex, and Hermes agents",
5
+ "license": "MIT",
6
+ "author": "Thomas Marcelis",
7
+ "homepage": "https://github.com/ThomasMarcelis/agent-peers#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/ThomasMarcelis/agent-peers.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/ThomasMarcelis/agent-peers/issues"
14
+ },
15
+ "keywords": [
16
+ "hermes",
17
+ "codex",
18
+ "claude-code",
19
+ "mcp",
20
+ "agents",
21
+ "ipc"
22
+ ],
23
+ "type": "module",
24
+ "files": [
25
+ "bin/",
26
+ "src/",
27
+ "agent_peers/",
28
+ "__init__.py",
29
+ "plugin.yaml",
30
+ "hermes-plugin/",
31
+ "scripts/install-hermes-plugin.py",
32
+ "docs/",
33
+ "CLAUDE-snippet.md",
34
+ "CHANGELOG.md",
35
+ "LICENSE",
36
+ "after-install.md",
37
+ "!**/__pycache__/**",
38
+ "!**/*.pyc",
39
+ "CONTRIBUTING.md",
40
+ "SECURITY.md"
41
+ ],
42
+ "os": [
43
+ "linux",
44
+ "darwin"
45
+ ],
46
+ "bin": {
47
+ "agent-peers": "bin/agent-peers.mjs",
48
+ "codex-peer": "src/codex-peer.mjs"
49
+ },
50
+ "engines": {
51
+ "node": ">=22"
52
+ },
53
+ "scripts": {
54
+ "check": "node scripts/check.mjs",
55
+ "test": "node --test test/discovery.mjs test/hermes-bridge.mjs test/wire.mjs test/app-server.mjs test/codex-peer.mjs test/cli.mjs",
56
+ "test:python": "python3 -m pytest -q",
57
+ "test:package": "node scripts/test-package.mjs",
58
+ "test:live": "node test/live-matrix.mjs",
59
+ "test:live:hermes": "node test/hermes-live.mjs",
60
+ "prepublishOnly": "npm run check && npm test"
61
+ },
62
+ "dependencies": {
63
+ "@modelcontextprotocol/sdk": "^1",
64
+ "ws": "^8",
65
+ "zod": "^4"
66
+ }
67
+ }