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.
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/ARCHITECTURE.md +9 -1
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/CHANGELOG.md +6 -0
- inter_agent_core-0.3.0/PKG-INFO +136 -0
- inter_agent_core-0.3.0/README.md +113 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/SECURITY.md +4 -2
- inter_agent_core-0.3.0/docs/SECURITY_BASELINE.md +10 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/docs/THREAT_MODEL.md +1 -1
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/pyproject.toml +2 -2
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent/core/adapter_control.py +139 -39
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent/core/client.py +163 -4
- inter_agent_core-0.3.0/src/inter_agent_core.egg-info/PKG-INFO +136 -0
- inter_agent_core-0.2.0/PKG-INFO +0 -85
- inter_agent_core-0.2.0/README.md +0 -62
- inter_agent_core-0.2.0/docs/SECURITY_BASELINE.md +0 -10
- inter_agent_core-0.2.0/src/inter_agent_core.egg-info/PKG-INFO +0 -85
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/LICENSE.md +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/MANIFEST.in +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/setup.cfg +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/asyncapi.yaml +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/error-codes.md +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/auth_challenge.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/auth_response.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/broadcast.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/bye.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/channels.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/channels_ok.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/custom.unknown-pass-through.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/error.auth-failed.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/hello.agent-with-label.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/hello.agent.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/kick.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/kick_ok.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/list.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/list_ok.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/msg.custom.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/msg.text.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/ping.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/pong.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/publish.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/send.direct.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/shutdown.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/shutdown_ok.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/subscribe.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/subscribe_ok.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/unsubscribe.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/unsubscribe_ok.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/examples/welcome.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/auth_challenge.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/auth_response.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/broadcast.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/bye.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/channels.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/channels_ok.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/custom.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/error.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/hello.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/kick.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/kick_ok.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/list.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/list_ok.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/msg.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/ping.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/pong.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/publish.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/send.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/shutdown.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/shutdown_ok.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/subscribe.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/subscribe_ok.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/unsubscribe.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/unsubscribe_ok.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/spec/schemas/welcome.json +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent/__init__.py +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent/core/__init__.py +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent/core/auth.py +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent/core/channels.py +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent/core/config.py +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent/core/errors.py +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent/core/kick.py +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent/core/list.py +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent/core/publish.py +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent/core/router.py +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent/core/send.py +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent/core/server.py +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent/core/shared.py +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent/core/shutdown.py +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent/core/status.py +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent/core/tls.py +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent/core/transport.py +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent/py.typed +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent_core.egg-info/SOURCES.txt +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent_core.egg-info/dependency_links.txt +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent_core.egg-info/entry_points.txt +0 -0
- {inter_agent_core-0.2.0 → inter_agent_core-0.3.0}/src/inter_agent_core.egg-info/requires.txt +0 -0
- {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
|
|
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
|
-
-
|
|
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.
|
|
8
|
-
description = "
|
|
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
|
|
4
|
-
matching live agent listener through a local
|
|
5
|
-
opening a new agent or control identity on the
|
|
6
|
-
JSON request and response are exchanged per
|
|
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
|
|
10
|
-
|
|
11
|
-
and
|
|
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
|
|
34
|
-
#: compromised or buggy command cannot exfiltrate state
|
|
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__(
|
|
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
|
-
|
|
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
|
|
288
|
-
|
|
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/
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
+
)
|