inter-agent-core 0.2.0__tar.gz → 0.3.0__tar.gz

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.
Files changed (95) hide show
  1. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/ARCHITECTURE.md +9 -1
  2. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/CHANGELOG.md +6 -0
  3. inter_agent_core-0.3.0/PKG-INFO +136 -0
  4. inter_agent_core-0.3.0/README.md +113 -0
  5. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/SECURITY.md +4 -2
  6. inter_agent_core-0.3.0/docs/SECURITY_BASELINE.md +10 -0
  7. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/docs/THREAT_MODEL.md +1 -1
  8. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/pyproject.toml +2 -2
  9. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent/core/adapter_control.py +139 -39
  10. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent/core/client.py +163 -4
  11. inter_agent_core-0.3.0/src/inter_agent_core.egg-info/PKG-INFO +136 -0
  12. inter_agent_core-0.2.0/PKG-INFO +0 -85
  13. inter_agent_core-0.2.0/README.md +0 -62
  14. inter_agent_core-0.2.0/docs/SECURITY_BASELINE.md +0 -10
  15. inter_agent_core-0.2.0/src/inter_agent_core.egg-info/PKG-INFO +0 -85
  16. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/LICENSE.md +0 -0
  17. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/MANIFEST.in +0 -0
  18. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/setup.cfg +0 -0
  19. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/asyncapi.yaml +0 -0
  20. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/error-codes.md +0 -0
  21. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/auth_challenge.json +0 -0
  22. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/auth_response.json +0 -0
  23. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/broadcast.json +0 -0
  24. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/bye.json +0 -0
  25. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/channels.json +0 -0
  26. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/channels_ok.json +0 -0
  27. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/custom.unknown-pass-through.json +0 -0
  28. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/error.auth-failed.json +0 -0
  29. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/hello.agent-with-label.json +0 -0
  30. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/hello.agent.json +0 -0
  31. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/kick.json +0 -0
  32. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/kick_ok.json +0 -0
  33. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/list.json +0 -0
  34. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/list_ok.json +0 -0
  35. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/msg.custom.json +0 -0
  36. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/msg.text.json +0 -0
  37. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/ping.json +0 -0
  38. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/pong.json +0 -0
  39. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/publish.json +0 -0
  40. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/send.direct.json +0 -0
  41. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/shutdown.json +0 -0
  42. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/shutdown_ok.json +0 -0
  43. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/subscribe.json +0 -0
  44. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/subscribe_ok.json +0 -0
  45. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/unsubscribe.json +0 -0
  46. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/unsubscribe_ok.json +0 -0
  47. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/welcome.json +0 -0
  48. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/auth_challenge.json +0 -0
  49. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/auth_response.json +0 -0
  50. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/broadcast.json +0 -0
  51. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/bye.json +0 -0
  52. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/channels.json +0 -0
  53. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/channels_ok.json +0 -0
  54. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/custom.json +0 -0
  55. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/error.json +0 -0
  56. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/hello.json +0 -0
  57. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/kick.json +0 -0
  58. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/kick_ok.json +0 -0
  59. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/list.json +0 -0
  60. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/list_ok.json +0 -0
  61. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/msg.json +0 -0
  62. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/ping.json +0 -0
  63. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/pong.json +0 -0
  64. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/publish.json +0 -0
  65. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/send.json +0 -0
  66. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/shutdown.json +0 -0
  67. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/shutdown_ok.json +0 -0
  68. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/subscribe.json +0 -0
  69. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/subscribe_ok.json +0 -0
  70. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/unsubscribe.json +0 -0
  71. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/unsubscribe_ok.json +0 -0
  72. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/welcome.json +0 -0
  73. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent/__init__.py +0 -0
  74. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent/core/__init__.py +0 -0
  75. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent/core/auth.py +0 -0
  76. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent/core/channels.py +0 -0
  77. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent/core/config.py +0 -0
  78. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent/core/errors.py +0 -0
  79. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent/core/kick.py +0 -0
  80. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent/core/list.py +0 -0
  81. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent/core/publish.py +0 -0
  82. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent/core/router.py +0 -0
  83. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent/core/send.py +0 -0
  84. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent/core/server.py +0 -0
  85. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent/core/shared.py +0 -0
  86. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent/core/shutdown.py +0 -0
  87. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent/core/status.py +0 -0
  88. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent/core/tls.py +0 -0
  89. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent/core/transport.py +0 -0
  90. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent/py.typed +0 -0
  91. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent_core.egg-info/SOURCES.txt +0 -0
  92. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent_core.egg-info/dependency_links.txt +0 -0
  93. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent_core.egg-info/entry_points.txt +0 -0
  94. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent_core.egg-info/requires.txt +0 -0
  95. {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent_core.egg-info/top_level.txt +0 -0
@@ -19,9 +19,17 @@ The protocol contract lives in [spec/](spec/):
19
19
 
20
20
  `core.version` is a protocol capability, not the package version.
21
21
 
22
+ ### Persistent custom messages
23
+
24
+ `AgentSession.send_custom(custom_type, payload, to)` is a typed host-neutral API for a targeted generic custom envelope. It reuses the persistent authenticated connection, session identity, reader, inbox, and serialized command lock. The request contains only the generic custom type, pass-through payload, and routing target; Core does not define the receiver's custom schema or semantics and does not expose a caller-supplied sender identity through this API.
25
+
26
+ After writing the custom frame, the session writes an application-level `ping` and waits for its ordered `pong`. Server frame ordering makes that `pong` a response barrier: routing errors from the preceding custom operation are consumed and returned before the barrier, while unrelated inbound frames remain in the inbox. The local result reports `submitted` only after the barrier arrives without a routing error. This is evidence of local server-side submission processing, not evidence of delivery, receiver acceptance, or application completion.
27
+
28
+ A barrier timeout, connection failure, or cancellation closes the session before the operation returns or propagates. This fail-closed teardown prevents a late routing error or barrier from leaking into the ordinary inbound queue. Custom envelopes remain bounded by the existing custom-type, payload, and WebSocket frame limits; the private local adapter bridge additionally retains its strict 64 KiB request/response bound.
29
+
22
30
  ## Extension boundary
23
31
 
24
- Host extensions own host interaction, listener lifetime, rendering, and host-local state. Python extensions use the typed core command and listener APIs. The narrow `inter_agent.core.adapter_control` module provides a same-user local control socket for a host listener's subscribe/unsubscribe operations; it does not carry the bus secret.
32
+ Host extensions own host interaction, listener lifetime, rendering, and host-local state. Python extensions use the typed core command and listener APIs. The narrow `inter_agent.core.adapter_control` module provides a same-user local control socket for a host listener's subscribe/unsubscribe operations and an optional strict generic custom-send operation; it does not carry the bus secret. Existing channel handlers keep their original `(op, channel)` signature and behavior. The custom bridge accepts only `op`, `custom_type`, `payload`, and `to`, with no caller-supplied identity or other operation.
25
33
 
26
34
  Core does not ship host plugins, host adapters, marketplace metadata, or host-specific setup.
27
35
 
@@ -6,6 +6,12 @@ All notable changes to `inter-agent-core` are recorded here.
6
6
 
7
7
  The Python distribution version in `pyproject.toml` is the source of truth for core artifacts. The protocol capability `core.version` is a compatibility value and is versioned independently from the Python distribution.
8
8
 
9
+ ## 0.3.0
10
+
11
+ Release-ready host-neutral custom-message support adds a persistent-session `send_custom` API and a strict local adapter bridge operation. Custom sends preserve the authenticated session identity, use an ordered response barrier to isolate routing errors, and fail closed on barrier or connection failure without claiming delivery. The generic core continues to pass through custom type and payload data without defining host-specific semantics.
12
+
13
+ No publication, tag, or push is implied by this entry.
14
+
9
15
  ## 0.2.0
10
16
 
11
17
  Clean standalone baseline for the host-neutral inter-agent runtime. It includes the local authenticated server, generic command-line clients, protocol contract, TLS/authentication/state behavior, and the typed `inter_agent.core.adapter_control` extension-support API.
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.4
2
+ Name: inter-agent-core
3
+ Version: 0.3.0
4
+ Summary: A local websocket-based message bus that allows your agentic coding harnesses to talk to each other.
5
+ Author-email: Nicholas Moen <arcanemachine@gmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/arcanemachine/inter-agent-core
8
+ Project-URL: Repository, https://github.com/arcanemachine/inter-agent-core
9
+ Project-URL: Issues, https://github.com/arcanemachine/inter-agent-core/issues
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Topic :: Communications
17
+ Requires-Python: >=3.10
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE.md
20
+ Requires-Dist: cryptography==46.0.3
21
+ Requires-Dist: websockets==16.0
22
+ Dynamic: license-file
23
+
24
+ # inter-agent-core
25
+
26
+ `inter-agent-core` is the host-neutral Python runtime for a local authenticated message bus. It provides the WebSocket server, protocol, routing, channels, TLS, shared state, and generic command-line clients used by host integrations such as [Pi](https://github.com/arcanemachine/inter-agent-pi), [Claude Code](https://github.com/arcanemachine/inter-agent-claude-code), and [OpenCode](https://github.com/arcanemachine/inter-agent-opencode).
27
+
28
+ ## Requirements
29
+
30
+ - Python 3.10 or newer
31
+ - [`uv`](https://docs.astral.sh/uv/) for the installation and development commands below
32
+
33
+ ## Install
34
+
35
+ Create an isolated environment and install the released `0.3.0` package:
36
+
37
+ ```bash
38
+ uv venv .venv
39
+ source .venv/bin/activate
40
+ uv pip install inter-agent-core==0.3.0
41
+ ```
42
+
43
+ The package installs the `inter-agent-*` command-line clients and the `inter-agent` Python API. On Windows, activate `.venv` with the equivalent PowerShell or Command Prompt command. Contributors working from a checkout should use the separate development procedure below instead of installing the registry package into that checkout’s environment.
44
+
45
+ ## Quick start
46
+
47
+ Start the bus in one terminal:
48
+
49
+ ```bash
50
+ inter-agent-server
51
+ ```
52
+
53
+ In a second terminal, connect a named client and leave it listening:
54
+
55
+ ```bash
56
+ inter-agent-connect alice
57
+ ```
58
+
59
+ In a third terminal, inspect the bus and send a direct message:
60
+
61
+ ```bash
62
+ inter-agent-status
63
+ inter-agent-list
64
+ inter-agent-send alice "hello from the command line"
65
+ ```
66
+
67
+ The `alice` terminal receives the message as a JSON protocol frame. Stop a foreground server with `Ctrl-C`. Use `inter-agent-shutdown` only when you intend to disconnect every client on that shared bus.
68
+
69
+ By default, local clients use `127.0.0.1:16837` and discover the same generated secret from the shared state directory. If you configure a different endpoint, data directory, or secret, use the same values for every client and server.
70
+
71
+ ### Persistent custom messages
72
+
73
+ A host integration with an established `AgentSession` can send a generic targeted custom envelope with `send_custom(custom_type, payload, to)`. The method reuses that session's authenticated connection, routing identity, reader, inbox, and serialized command lock; it does not create another connection or accept a caller-supplied sender identity. `to` is a target routing name or an unambiguous target prefix, and `payload` is passed through for the receiving integration to interpret.
74
+
75
+ The method sends the custom frame followed by an ordered application-level `ping` barrier. A routing error observed before the matching `pong` is returned as an unsuccessful local result. `submitted` is true only after the barrier is observed without such an error: it means the server finished processing the preceding frame on that connection, not that a target received, accepted, or acted on the payload. The generic core does not define the custom type or payload schema.
76
+
77
+ If the barrier times out, the connection closes, or the operation is cancelled, the session fails closed before returning or propagating the failure. The late routing error and barrier cannot enter the ordinary inbound queue; a listener may establish a replacement session according to its own lifecycle. Custom envelopes and local bridge exchanges remain subject to the existing protocol, frame, and 64 KiB local-bridge limits.
78
+
79
+ ## Commands
80
+
81
+ | Command | Purpose |
82
+ | --- | --- |
83
+ | `inter-agent-server` | Run the message bus. Use `--idle-timeout N` for an idle auto-shutdown. |
84
+ | `inter-agent-connect <name>` | Join as a named agent and print incoming protocol frames. |
85
+ | `inter-agent-send <name> <text>` | Send a direct message. Omit the target and use `--text` to broadcast. |
86
+ | `inter-agent-list` | List connected agent sessions. |
87
+ | `inter-agent-status` | Show endpoint resolution and server reachability. Use `--json` for structured output. |
88
+ | `inter-agent-publish <channel> <text>` | Publish to an existing channel. |
89
+ | `inter-agent-channels` | List channels and their subscribers. |
90
+ | `inter-agent-kick <name>` | Disconnect an agent session. |
91
+ | `inter-agent-shutdown` | Stop the server and disconnect every session. |
92
+
93
+ Every command supports `--help`. Endpoint-aware commands also accept `--host`, `--port`, `--tls` or `--no-tls`, and TLS certificate overrides.
94
+
95
+ Use direct messages for normal coordination. Use broadcasts only when every connected session needs the message. Channels are in-memory groups owned by one server and disappear when that server stops.
96
+
97
+ ## Configuration and security
98
+
99
+ Configuration resolves in this order: command-line options, environment variables, the JSON configuration file, then built-in defaults. The main environment variables are:
100
+
101
+ - `INTER_AGENT_HOST` and `INTER_AGENT_PORT` — endpoint overrides;
102
+ - `INTER_AGENT_SECRET` — an explicit shared secret;
103
+ - `INTER_AGENT_DATA_DIR` — state, generated secret, and generated TLS material;
104
+ - `INTER_AGENT_CONFIG` — an alternate JSON configuration file;
105
+ - `INTER_AGENT_TLS`, `INTER_AGENT_TLS_CERT`, and `INTER_AGENT_TLS_KEY` — TLS settings.
106
+
107
+ Loopback endpoints default to plaintext `ws://`. Non-loopback endpoints default to TLS `wss://`; clients never downgrade a failed TLS connection automatically.
108
+
109
+ The default trust boundary is one trusted operating-system user on one machine. HMAC-SHA-256 authentication prevents clients without the shared secret from joining, and TLS protects network transport, but neither protects against hostile code running as the same user. Do not commit or share secrets, private keys, certificates, or state. See [`SECURITY.md`](SECURITY.md) and the detailed [`threat model`](docs/THREAT_MODEL.md).
110
+
111
+ For a common failure, run `inter-agent-status` first. If clients cannot authenticate, check that they resolve the same endpoint, state directory, and secret. Use a separate endpoint and data directory for tests or isolated buses rather than disturbing an existing one.
112
+
113
+ ## Reference and development
114
+
115
+ - [`spec/`](spec/) — protocol definition, schemas, examples, and error codes
116
+ - [`ARCHITECTURE.md`](ARCHITECTURE.md) — runtime and extension boundaries
117
+ - [`CHANGELOG.md`](CHANGELOG.md) — released changes
118
+ - [`README.md#commands`](#commands) — generic CLI reference
119
+ - [inter-agent-pi](https://github.com/arcanemachine/inter-agent-pi) — Pi integration
120
+ - [inter-agent-claude-code](https://github.com/arcanemachine/inter-agent-claude-code) — Claude Code integration
121
+ - [inter-agent-opencode](https://github.com/arcanemachine/inter-agent-opencode) — OpenCode integration
122
+
123
+ Host-specific commands, listeners, rendering, and tools belong in those extension repositories; this package supplies the shared runtime.
124
+
125
+ For development, clone the repository and run:
126
+
127
+ ```bash
128
+ git clone https://github.com/arcanemachine/inter-agent-core.git
129
+ cd inter-agent-core
130
+ uv sync --locked
131
+ ./run-checks.sh
132
+ ```
133
+
134
+ ## License
135
+
136
+ MIT. See [`LICENSE.md`](LICENSE.md).
@@ -0,0 +1,113 @@
1
+ # inter-agent-core
2
+
3
+ `inter-agent-core` is the host-neutral Python runtime for a local authenticated message bus. It provides the WebSocket server, protocol, routing, channels, TLS, shared state, and generic command-line clients used by host integrations such as [Pi](https://github.com/arcanemachine/inter-agent-pi), [Claude Code](https://github.com/arcanemachine/inter-agent-claude-code), and [OpenCode](https://github.com/arcanemachine/inter-agent-opencode).
4
+
5
+ ## Requirements
6
+
7
+ - Python 3.10 or newer
8
+ - [`uv`](https://docs.astral.sh/uv/) for the installation and development commands below
9
+
10
+ ## Install
11
+
12
+ Create an isolated environment and install the released `0.3.0` package:
13
+
14
+ ```bash
15
+ uv venv .venv
16
+ source .venv/bin/activate
17
+ uv pip install inter-agent-core==0.3.0
18
+ ```
19
+
20
+ The package installs the `inter-agent-*` command-line clients and the `inter-agent` Python API. On Windows, activate `.venv` with the equivalent PowerShell or Command Prompt command. Contributors working from a checkout should use the separate development procedure below instead of installing the registry package into that checkout’s environment.
21
+
22
+ ## Quick start
23
+
24
+ Start the bus in one terminal:
25
+
26
+ ```bash
27
+ inter-agent-server
28
+ ```
29
+
30
+ In a second terminal, connect a named client and leave it listening:
31
+
32
+ ```bash
33
+ inter-agent-connect alice
34
+ ```
35
+
36
+ In a third terminal, inspect the bus and send a direct message:
37
+
38
+ ```bash
39
+ inter-agent-status
40
+ inter-agent-list
41
+ inter-agent-send alice "hello from the command line"
42
+ ```
43
+
44
+ The `alice` terminal receives the message as a JSON protocol frame. Stop a foreground server with `Ctrl-C`. Use `inter-agent-shutdown` only when you intend to disconnect every client on that shared bus.
45
+
46
+ By default, local clients use `127.0.0.1:16837` and discover the same generated secret from the shared state directory. If you configure a different endpoint, data directory, or secret, use the same values for every client and server.
47
+
48
+ ### Persistent custom messages
49
+
50
+ A host integration with an established `AgentSession` can send a generic targeted custom envelope with `send_custom(custom_type, payload, to)`. The method reuses that session's authenticated connection, routing identity, reader, inbox, and serialized command lock; it does not create another connection or accept a caller-supplied sender identity. `to` is a target routing name or an unambiguous target prefix, and `payload` is passed through for the receiving integration to interpret.
51
+
52
+ The method sends the custom frame followed by an ordered application-level `ping` barrier. A routing error observed before the matching `pong` is returned as an unsuccessful local result. `submitted` is true only after the barrier is observed without such an error: it means the server finished processing the preceding frame on that connection, not that a target received, accepted, or acted on the payload. The generic core does not define the custom type or payload schema.
53
+
54
+ If the barrier times out, the connection closes, or the operation is cancelled, the session fails closed before returning or propagating the failure. The late routing error and barrier cannot enter the ordinary inbound queue; a listener may establish a replacement session according to its own lifecycle. Custom envelopes and local bridge exchanges remain subject to the existing protocol, frame, and 64 KiB local-bridge limits.
55
+
56
+ ## Commands
57
+
58
+ | Command | Purpose |
59
+ | --- | --- |
60
+ | `inter-agent-server` | Run the message bus. Use `--idle-timeout N` for an idle auto-shutdown. |
61
+ | `inter-agent-connect <name>` | Join as a named agent and print incoming protocol frames. |
62
+ | `inter-agent-send <name> <text>` | Send a direct message. Omit the target and use `--text` to broadcast. |
63
+ | `inter-agent-list` | List connected agent sessions. |
64
+ | `inter-agent-status` | Show endpoint resolution and server reachability. Use `--json` for structured output. |
65
+ | `inter-agent-publish <channel> <text>` | Publish to an existing channel. |
66
+ | `inter-agent-channels` | List channels and their subscribers. |
67
+ | `inter-agent-kick <name>` | Disconnect an agent session. |
68
+ | `inter-agent-shutdown` | Stop the server and disconnect every session. |
69
+
70
+ Every command supports `--help`. Endpoint-aware commands also accept `--host`, `--port`, `--tls` or `--no-tls`, and TLS certificate overrides.
71
+
72
+ Use direct messages for normal coordination. Use broadcasts only when every connected session needs the message. Channels are in-memory groups owned by one server and disappear when that server stops.
73
+
74
+ ## Configuration and security
75
+
76
+ Configuration resolves in this order: command-line options, environment variables, the JSON configuration file, then built-in defaults. The main environment variables are:
77
+
78
+ - `INTER_AGENT_HOST` and `INTER_AGENT_PORT` — endpoint overrides;
79
+ - `INTER_AGENT_SECRET` — an explicit shared secret;
80
+ - `INTER_AGENT_DATA_DIR` — state, generated secret, and generated TLS material;
81
+ - `INTER_AGENT_CONFIG` — an alternate JSON configuration file;
82
+ - `INTER_AGENT_TLS`, `INTER_AGENT_TLS_CERT`, and `INTER_AGENT_TLS_KEY` — TLS settings.
83
+
84
+ Loopback endpoints default to plaintext `ws://`. Non-loopback endpoints default to TLS `wss://`; clients never downgrade a failed TLS connection automatically.
85
+
86
+ The default trust boundary is one trusted operating-system user on one machine. HMAC-SHA-256 authentication prevents clients without the shared secret from joining, and TLS protects network transport, but neither protects against hostile code running as the same user. Do not commit or share secrets, private keys, certificates, or state. See [`SECURITY.md`](SECURITY.md) and the detailed [`threat model`](docs/THREAT_MODEL.md).
87
+
88
+ For a common failure, run `inter-agent-status` first. If clients cannot authenticate, check that they resolve the same endpoint, state directory, and secret. Use a separate endpoint and data directory for tests or isolated buses rather than disturbing an existing one.
89
+
90
+ ## Reference and development
91
+
92
+ - [`spec/`](spec/) — protocol definition, schemas, examples, and error codes
93
+ - [`ARCHITECTURE.md`](ARCHITECTURE.md) — runtime and extension boundaries
94
+ - [`CHANGELOG.md`](CHANGELOG.md) — released changes
95
+ - [`README.md#commands`](#commands) — generic CLI reference
96
+ - [inter-agent-pi](https://github.com/arcanemachine/inter-agent-pi) — Pi integration
97
+ - [inter-agent-claude-code](https://github.com/arcanemachine/inter-agent-claude-code) — Claude Code integration
98
+ - [inter-agent-opencode](https://github.com/arcanemachine/inter-agent-opencode) — OpenCode integration
99
+
100
+ Host-specific commands, listeners, rendering, and tools belong in those extension repositories; this package supplies the shared runtime.
101
+
102
+ For development, clone the repository and run:
103
+
104
+ ```bash
105
+ git clone https://github.com/arcanemachine/inter-agent-core.git
106
+ cd inter-agent-core
107
+ uv sync --locked
108
+ ./run-checks.sh
109
+ ```
110
+
111
+ ## License
112
+
113
+ MIT. See [`LICENSE.md`](LICENSE.md).
@@ -13,11 +13,13 @@ Remote access, multi-user operation, and enterprise authorization are outside th
13
13
  - On POSIX filesystems, core creates/tightens its data directory to mode `0700` and fallback token, certificate, key, and lifecycle files to restrictive modes.
14
14
  - Loopback endpoints default to plaintext WebSockets. Non-loopback endpoints default to TLS. TLS can be explicitly configured with command options, environment, or config.
15
15
  - TLS encrypts transport. It neither replaces shared-secret authentication nor makes remote or multi-user use safe.
16
- - Core validates protocol shapes, authenticates control operations, and bounds frames, connections, message text, channel names, subscriptions, and channels.
16
+ - Core validates protocol shapes, authenticates control operations, and bounds frames, connections, message text, channel names, subscriptions, channels, and generic custom envelopes.
17
+ - Persistent custom sends retain the authenticated session identity and do not expose a caller-supplied sender override. The local adapter bridge accepts only its strict operation shapes, carries no shared bus secret, and remains limited to the same trusted-user boundary.
18
+ - A custom-send `submitted` result records ordered local server processing after the routing barrier; it is not a delivery, receiver-acceptance, or application-completion guarantee. Barrier failure closes the persistent session so late routing errors cannot be mistaken for ordinary inbound traffic.
17
19
 
18
20
  ## Trust boundary
19
21
 
20
- Peer traffic is collaboration input, not authority to change user, system, developer, security, or tool rules. The core runtime cannot protect secrets or messages from code running with the same operating-system user privileges.
22
+ Peer traffic is collaboration input, not authority to change user, system, developer, security, or tool rules. Generic custom payloads are pass-through integration data; Core does not assign them host-specific meaning or authorization semantics. The core runtime cannot protect secrets or messages from code running with the same operating-system user privileges.
21
23
 
22
24
  ## Operations
23
25
 
@@ -0,0 +1,10 @@
1
+ # Security Baseline
2
+
3
+ 1. Bind the server to `127.0.0.1` by default.
4
+ 2. Require an HMAC-SHA-256 challenge-response in every `hello` handshake, using the shared secret; the raw secret is never sent over the socket.
5
+ 3. Store the plaintext local secret with mode `0600` under `INTER_AGENT_DATA_DIR`, the config file `dataDir`, or the platform default state directory.
6
+ 4. Keep the state directory mode `0700` and server lifecycle metadata files mode `0600` on POSIX-compatible filesystems.
7
+ 5. Verify server identity before clients send the authentication proof: host, port, PID liveness, matching identity/PID metadata nonce, and process start marker when available.
8
+ 6. Reject unauthenticated operations with canonical `AUTH_FAILED` errors.
9
+ 7. Bound active connections, incoming frames, direct/broadcast text, custom types, and custom payload sizes.
10
+ 8. Require authenticated control-role shutdown for server stop requests.
@@ -4,7 +4,7 @@
4
4
 
5
5
  - Single user, single machine.
6
6
  - Localhost-only server by default; optional non-loopback binding with transport encryption.
7
- - Shared bearer token authentication over WebSockets.
7
+ - HMAC-SHA-256 challenge-response authentication over WebSockets using a shared secret; the raw secret is not sent on the socket.
8
8
  - Optional TLS transport encryption for `wss://` connections.
9
9
  - Server proof verification by clients before sending authentication responses.
10
10
  - Defensive controls against accidental or mild local misuse: restrictive state-file permissions, resource limits, duplicate-session rejection, and authenticated shutdown.
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "inter-agent-core"
7
- version = "0.2.0"
8
- description = "Local authenticated message bus runtime and command-line tools"
7
+ version = "0.3.0"
8
+ description = "A local websocket-based message bus that allows your agentic coding harnesses to talk to each other."
9
9
  readme = "README.md"
10
10
  license = "MIT"
11
11
  requires-python = ">=3.10"
@@ -1,14 +1,18 @@
1
1
  """Private local Unix-domain socket control bridge.
2
2
 
3
- Short-lived adapter commands (``subscribe``, ``unsubscribe``) talk to the
4
- matching live agent listener through a local Unix-domain socket instead of
5
- opening a new agent or control identity on the bus. One newline-delimited
6
- JSON request and response are exchanged per connection.
3
+ Short-lived adapter commands (``subscribe``, ``unsubscribe``, and the strict
4
+ ``custom`` send) talk to the matching live agent listener through a local
5
+ Unix-domain socket instead of opening a new agent or control identity on the
6
+ bus. One newline-delimited JSON request and response are exchanged per
7
+ connection.
7
8
 
8
9
  The bridge is strictly local and private: it never carries the shared server
9
- secret and never accepts anything but ``subscribe`` and ``unsubscribe``
10
- requests. Each endpoint is derived from the adapter, normalized endpoint,
11
- and routing name so distinct listeners never collide on the socket path.
10
+ secret and never accepts anything but ``subscribe``, ``unsubscribe``, and the
11
+ strict targeted ``custom`` request (exactly ``op``, ``custom_type``,
12
+ ``payload``, and ``to``). Caller-supplied ``from_name``, secret material, and
13
+ any other key are rejected. Each endpoint is derived from the adapter,
14
+ normalized endpoint, and routing name so distinct listeners never collide on
15
+ the socket path.
12
16
  """
13
17
 
14
18
  from __future__ import annotations
@@ -30,10 +34,15 @@ SUPPORTED_OPS = frozenset({"subscribe", "unsubscribe"})
30
34
  #: oversized line raises ``LimitOverrunError`` before the length check.
31
35
  _READ_LIMIT = CONTROL_MAX_REQUEST_BYTES + 1
32
36
 
33
- #: Exactly the request keys the bridge accepts. Any other key is rejected so a
34
- #: compromised or buggy command cannot exfiltrate state through the bridge.
37
+ #: Exactly the request keys the bridge accepts for channel ops. Any other
38
+ #: key is rejected so a compromised or buggy command cannot exfiltrate state
39
+ #: through the bridge.
35
40
  _REQUEST_KEYS = frozenset({"op", "channel"})
36
41
 
42
+ #: Exactly the request keys the bridge accepts for the strict custom send.
43
+ #: ``from_name``, secrets, and any other key are rejected outright.
44
+ _CUSTOM_REQUEST_KEYS = frozenset({"op", "custom_type", "payload", "to"})
45
+
37
46
  #: Amount of the SHA-256 digest carried in the socket filename so the path
38
47
  #: stays bounded regardless of name length.
39
48
  _SOCKET_HASH_LEN = 16
@@ -45,6 +54,12 @@ class ControlError(Exception):
45
54
 
46
55
  RequestHandler = Callable[[str, str], Awaitable[dict[str, object]]]
47
56
 
57
+ #: Optional handler for the strict targeted ``custom`` send: receives the
58
+ #: custom type, the pass-through payload, and the required routing target.
59
+ #: Kept separate from ``RequestHandler`` so the existing subscribe/unsubscribe
60
+ #: signature stays unchanged for existing listeners.
61
+ CustomRequestHandler = Callable[[str, object, str], Awaitable[dict[str, object]]]
62
+
48
63
 
49
64
  def _normalize_host(host: str) -> str:
50
65
  return host.strip().lower()
@@ -126,9 +141,15 @@ class ControlServer:
126
141
  rather than left in a permissive state or allowed to break the listener.
127
142
  """
128
143
 
129
- def __init__(self, path: Path, handle: RequestHandler) -> None:
144
+ def __init__(
145
+ self,
146
+ path: Path,
147
+ handle: RequestHandler,
148
+ custom_handler: CustomRequestHandler | None = None,
149
+ ) -> None:
130
150
  self._path = path
131
151
  self._handle = handle
152
+ self._custom_handle = custom_handler
132
153
  self._server: asyncio.Server | None = None
133
154
  self._inode: int | None = None
134
155
  self._dev: int | None = None
@@ -229,6 +250,9 @@ class ControlServer:
229
250
  writer, _local_error("BAD_REQUEST", "control request must be an object")
230
251
  )
231
252
  return
253
+ if payload.get("op") == "custom":
254
+ await self._handle_custom_request(writer, payload)
255
+ return
232
256
  if set(payload.keys()) != _REQUEST_KEYS:
233
257
  await _write_line(
234
258
  writer,
@@ -243,14 +267,7 @@ class ControlServer:
243
267
  if not isinstance(channel, str) or not channel:
244
268
  await _write_line(writer, _local_error("BAD_CHANNEL", "channel required"))
245
269
  return
246
- try:
247
- response = await asyncio.wait_for(
248
- self._handle(op, channel), timeout=CONTROL_TIMEOUT_S
249
- )
250
- except TimeoutError:
251
- response = _local_error("TIMEOUT", "listener did not respond in time")
252
- except Exception as exc: # listener-side failure, never propagate as traceback
253
- response = _local_error("LISTENER_UNAVAILABLE", str(exc))
270
+ response = await self._run_handler(self._handle(op, channel))
254
271
  await _write_line(writer, response)
255
272
  finally:
256
273
  try:
@@ -259,6 +276,47 @@ class ControlServer:
259
276
  except (OSError, asyncio.CancelledError, BrokenPipeError):
260
277
  pass
261
278
 
279
+ async def _handle_custom_request(
280
+ self, writer: asyncio.StreamWriter, payload: dict[str, object]
281
+ ) -> None:
282
+ """Validate and dispatch one strict targeted custom-send request.
283
+
284
+ The op is already known to be ``custom``. The request must contain
285
+ exactly ``op``, ``custom_type``, ``payload``, and ``to``; anything else
286
+ (including ``from_name`` or secret material) is rejected before any
287
+ handler runs. The custom handler is optional: a server without one
288
+ rejects the request without touching the channel handler.
289
+ """
290
+ if self._custom_handle is None:
291
+ await _write_line(writer, _local_error("BAD_OP", "unsupported control op"))
292
+ return
293
+ if set(payload.keys()) != _CUSTOM_REQUEST_KEYS:
294
+ message = "control request must contain only op, custom_type, payload, and to"
295
+ await _write_line(writer, _local_error("BAD_REQUEST", message))
296
+ return
297
+ custom_type = payload.get("custom_type")
298
+ if not isinstance(custom_type, str) or not custom_type:
299
+ await _write_line(writer, _local_error("BAD_CUSTOM_TYPE", "custom_type required"))
300
+ return
301
+ to = payload.get("to")
302
+ if not isinstance(to, str) or not to:
303
+ await _write_line(writer, _local_error("BAD_TO", "to required"))
304
+ return
305
+ response = await self._run_handler(
306
+ self._custom_handle(custom_type, payload.get("payload"), to)
307
+ )
308
+ await _write_line(writer, response)
309
+
310
+ async def _run_handler(self, handler: Awaitable[dict[str, object]]) -> dict[str, object]:
311
+ """Await a listener handler with the same timeout/exception mapping for
312
+ every request kind."""
313
+ try:
314
+ return await asyncio.wait_for(handler, timeout=CONTROL_TIMEOUT_S)
315
+ except TimeoutError:
316
+ return _local_error("TIMEOUT", "listener did not respond in time")
317
+ except Exception as exc: # listener-side failure, never propagate as traceback
318
+ return _local_error("LISTENER_UNAVAILABLE", str(exc))
319
+
262
320
  async def stop(self) -> None:
263
321
  await self._close_server()
264
322
  await self._unlink_owned(self._inode, self._dev)
@@ -284,25 +342,13 @@ class ControlServer:
284
342
  pass
285
343
 
286
344
 
287
- async def request(
288
- adapter: str,
289
- host: str,
290
- port: int,
291
- name: str,
292
- base_data_dir: Path,
293
- op: str,
294
- channel: str,
295
- ) -> dict[str, object]:
296
- """Send one control request to the listener owning ``name``'s socket.
345
+ async def _request_payload(path: Path, payload: dict[str, object]) -> dict[str, object]:
346
+ """Exchange one strict request/response over ``path`` with clean error mapping.
297
347
 
298
- Path/setup, connect, read, and decode failures are converted to clean
348
+ Path/connect, read, and decode failures are converted to clean
299
349
  ``ControlError`` diagnostics; oversized or malformed responses raise rather
300
- than returning partial data.
350
+ than returning partial data. ``payload`` must be JSON-serializable.
301
351
  """
302
- try:
303
- path = control_socket_path(adapter, host, port, name, base_data_dir)
304
- except OSError as exc:
305
- raise ControlError(f"control socket unavailable: {exc}") from exc
306
352
  try:
307
353
  reader, writer = await asyncio.wait_for(
308
354
  asyncio.open_unix_connection(str(path), limit=_READ_LIMIT),
@@ -318,8 +364,7 @@ async def request(
318
364
  raise ControlError(f"listener control connection failed: {exc}") from exc
319
365
 
320
366
  try:
321
- request_payload = json.dumps({"op": op, "channel": channel}) + "\n"
322
- writer.write(request_payload.encode("utf-8"))
367
+ writer.write((json.dumps(payload, ensure_ascii=False) + "\n").encode("utf-8"))
323
368
  try:
324
369
  await asyncio.wait_for(writer.drain(), timeout=CONTROL_TIMEOUT_S)
325
370
  except TimeoutError as exc:
@@ -342,9 +387,64 @@ async def request(
342
387
  if len(raw) > CONTROL_MAX_REQUEST_BYTES:
343
388
  raise ControlError("oversized response from listener")
344
389
  try:
345
- payload: object = json.loads(raw.decode("utf-8"))
390
+ response_value: object = json.loads(raw.decode("utf-8"))
346
391
  except (json.JSONDecodeError, UnicodeDecodeError) as exc:
347
392
  raise ControlError("malformed response from listener") from exc
348
- if not isinstance(payload, dict):
393
+ if not isinstance(response_value, dict):
349
394
  raise ControlError("response from listener must be an object")
350
- return {str(key): value for key, value in payload.items()}
395
+ return {str(key): value for key, value in response_value.items()}
396
+
397
+
398
+ async def request(
399
+ adapter: str,
400
+ host: str,
401
+ port: int,
402
+ name: str,
403
+ base_data_dir: Path,
404
+ op: str,
405
+ channel: str,
406
+ ) -> dict[str, object]:
407
+ """Send one channel control request to the listener owning ``name``'s socket.
408
+
409
+ Path/setup, connect, read, and decode failures are converted to clean
410
+ ``ControlError`` diagnostics; oversized or malformed responses raise rather
411
+ than returning partial data.
412
+ """
413
+ try:
414
+ path = control_socket_path(adapter, host, port, name, base_data_dir)
415
+ except OSError as exc:
416
+ raise ControlError(f"control socket unavailable: {exc}") from exc
417
+ return await _request_payload(path, {"op": op, "channel": channel})
418
+
419
+
420
+ async def request_custom(
421
+ adapter: str,
422
+ host: str,
423
+ port: int,
424
+ name: str,
425
+ base_data_dir: Path,
426
+ custom_type: str,
427
+ payload: object,
428
+ to: str,
429
+ ) -> dict[str, object]:
430
+ """Send one strict targeted custom-send request to ``name``'s listener.
431
+
432
+ Carries a custom type, a pass-through JSON payload, and a required routing
433
+ target. The listener rejects any other key (``from_name``, secret material,
434
+ or anything else) before a handler runs; the bridge never carries the
435
+ shared server secret. Failures map to the same clean ``ControlError``
436
+ diagnostics as ``request``.
437
+ """
438
+ try:
439
+ path = control_socket_path(adapter, host, port, name, base_data_dir)
440
+ except OSError as exc:
441
+ raise ControlError(f"control socket unavailable: {exc}") from exc
442
+ return await _request_payload(
443
+ path,
444
+ {
445
+ "op": "custom",
446
+ "custom_type": custom_type,
447
+ "payload": payload,
448
+ "to": to,
449
+ },
450
+ )