inter-agent-core 0.2.0__py3-none-any.whl
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/__init__.py +0 -0
- inter_agent/core/__init__.py +1 -0
- inter_agent/core/adapter_control.py +350 -0
- inter_agent/core/auth.py +279 -0
- inter_agent/core/channels.py +131 -0
- inter_agent/core/client.py +345 -0
- inter_agent/core/config.py +325 -0
- inter_agent/core/errors.py +31 -0
- inter_agent/core/kick.py +111 -0
- inter_agent/core/list.py +127 -0
- inter_agent/core/publish.py +146 -0
- inter_agent/core/router.py +8 -0
- inter_agent/core/send.py +265 -0
- inter_agent/core/server.py +765 -0
- inter_agent/core/shared.py +182 -0
- inter_agent/core/shutdown.py +92 -0
- inter_agent/core/status.py +291 -0
- inter_agent/core/tls.py +141 -0
- inter_agent/core/transport.py +37 -0
- inter_agent/py.typed +0 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/asyncapi.yaml +225 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/error-codes.md +28 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/examples/auth_challenge.json +6 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/examples/auth_response.json +4 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/examples/broadcast.json +4 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/examples/bye.json +3 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/examples/channels.json +3 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/examples/channels_ok.json +9 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/examples/custom.unknown-pass-through.json +9 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/examples/error.auth-failed.json +5 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/examples/hello.agent-with-label.json +14 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/examples/hello.agent.json +13 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/examples/kick.json +4 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/examples/kick_ok.json +5 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/examples/list.json +3 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/examples/list_ok.json +10 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/examples/msg.custom.json +13 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/examples/msg.text.json +9 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/examples/ping.json +3 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/examples/pong.json +3 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/examples/publish.json +5 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/examples/send.direct.json +5 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/examples/shutdown.json +3 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/examples/shutdown_ok.json +3 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/examples/subscribe.json +4 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/examples/subscribe_ok.json +4 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/examples/unsubscribe.json +4 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/examples/unsubscribe_ok.json +4 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/examples/welcome.json +10 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/schemas/auth_challenge.json +13 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/schemas/auth_response.json +11 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/schemas/broadcast.json +18 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/schemas/bye.json +10 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/schemas/channels.json +10 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/schemas/channels_ok.json +26 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/schemas/custom.json +22 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/schemas/error.json +37 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/schemas/hello.json +59 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/schemas/kick.json +13 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/schemas/kick_ok.json +12 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/schemas/list.json +10 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/schemas/list_ok.json +24 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/schemas/msg.json +29 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/schemas/ping.json +10 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/schemas/pong.json +10 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/schemas/publish.json +23 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/schemas/send.json +22 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/schemas/shutdown.json +10 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/schemas/shutdown_ok.json +10 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/schemas/subscribe.json +15 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/schemas/subscribe_ok.json +11 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/schemas/unsubscribe.json +15 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/schemas/unsubscribe_ok.json +11 -0
- inter_agent_core-0.2.0.data/data/share/inter-agent/spec/schemas/welcome.json +36 -0
- inter_agent_core-0.2.0.dist-info/METADATA +85 -0
- inter_agent_core-0.2.0.dist-info/RECORD +80 -0
- inter_agent_core-0.2.0.dist-info/WHEEL +5 -0
- inter_agent_core-0.2.0.dist-info/entry_points.txt +10 -0
- inter_agent_core-0.2.0.dist-info/licenses/LICENSE.md +21 -0
- inter_agent_core-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import asyncio
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
import uuid
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import websockets
|
|
12
|
+
from websockets.asyncio.client import ClientConnection
|
|
13
|
+
|
|
14
|
+
from inter_agent.core.auth import AuthError, AuthProtocolError, client_handshake
|
|
15
|
+
from inter_agent.core.send import ProtocolErrorResult, SendResult
|
|
16
|
+
from inter_agent.core.shared import (
|
|
17
|
+
Limits,
|
|
18
|
+
control_hello,
|
|
19
|
+
resolve_endpoint,
|
|
20
|
+
resolve_shared_secret,
|
|
21
|
+
validate_channel_name,
|
|
22
|
+
)
|
|
23
|
+
from inter_agent.core.transport import client_ssl_context, websocket_uri
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _text_frame(frame: str | bytes) -> str:
|
|
27
|
+
if isinstance(frame, bytes):
|
|
28
|
+
return frame.decode("utf-8")
|
|
29
|
+
return frame
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _json_object(raw: str) -> dict[str, object]:
|
|
33
|
+
payload: object = json.loads(raw)
|
|
34
|
+
if not isinstance(payload, dict):
|
|
35
|
+
raise ValueError("server response must be a JSON object")
|
|
36
|
+
return {str(key): value for key, value in payload.items()}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _protocol_error_from_payload(
|
|
40
|
+
raw: str, payload: dict[str, object]
|
|
41
|
+
) -> ProtocolErrorResult | None:
|
|
42
|
+
if payload.get("op") != "error":
|
|
43
|
+
return None
|
|
44
|
+
code = payload.get("code")
|
|
45
|
+
message = payload.get("message")
|
|
46
|
+
return ProtocolErrorResult(
|
|
47
|
+
code=code if isinstance(code, str) else "PROTOCOL_ERROR",
|
|
48
|
+
message=message if isinstance(message, str) else "protocol error",
|
|
49
|
+
raw=raw,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
async def _recv_protocol_error(ws: ClientConnection, timeout: float) -> ProtocolErrorResult | None:
|
|
54
|
+
try:
|
|
55
|
+
raw = _text_frame(await asyncio.wait_for(ws.recv(), timeout=timeout))
|
|
56
|
+
except TimeoutError:
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
response = _json_object(raw)
|
|
60
|
+
return _protocol_error_from_payload(raw, response)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
async def publish_to_channel(
|
|
64
|
+
host: str,
|
|
65
|
+
port: int,
|
|
66
|
+
channel: str,
|
|
67
|
+
text: str,
|
|
68
|
+
from_name: str | None = None,
|
|
69
|
+
*,
|
|
70
|
+
tls: bool = False,
|
|
71
|
+
data_dir: Path | None = None,
|
|
72
|
+
tls_cert_path: Path | None = None,
|
|
73
|
+
) -> SendResult:
|
|
74
|
+
"""Publish a text message to a channel through a control connection."""
|
|
75
|
+
if not validate_channel_name(channel, Limits().channel_name_max):
|
|
76
|
+
raise ValueError(f"invalid channel name: {channel!r}")
|
|
77
|
+
secret = resolve_shared_secret().secret
|
|
78
|
+
ssl_context = client_ssl_context(tls, data_dir, tls_cert_path)
|
|
79
|
+
async with websockets.connect(websocket_uri(host, port, tls), ssl=ssl_context) as ws:
|
|
80
|
+
try:
|
|
81
|
+
welcome = await client_handshake(ws, secret, control_hello(f"pub-{uuid.uuid4()}"))
|
|
82
|
+
except AuthError as exc:
|
|
83
|
+
raise SystemExit(str(exc)) from exc
|
|
84
|
+
except (AuthProtocolError, json.JSONDecodeError, UnicodeDecodeError) as exc:
|
|
85
|
+
raise SystemExit(f"server protocol mismatch: {exc}") from exc
|
|
86
|
+
welcome_payload = _json_object(welcome)
|
|
87
|
+
welcome_error = _protocol_error_from_payload(welcome, welcome_payload)
|
|
88
|
+
if welcome_error is not None:
|
|
89
|
+
return SendResult(welcome=welcome, welcome_payload=welcome_payload, error=welcome_error)
|
|
90
|
+
outbound: dict[str, object] = {"op": "publish", "channel": channel, "text": text}
|
|
91
|
+
if from_name:
|
|
92
|
+
outbound["from_name"] = from_name
|
|
93
|
+
await ws.send(json.dumps(outbound))
|
|
94
|
+
return SendResult(
|
|
95
|
+
welcome=welcome,
|
|
96
|
+
welcome_payload=welcome_payload,
|
|
97
|
+
error=await _recv_protocol_error(ws, 0.1),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
102
|
+
parser = argparse.ArgumentParser(prog="inter-agent-publish")
|
|
103
|
+
parser.add_argument("channel")
|
|
104
|
+
parser.add_argument("text")
|
|
105
|
+
parser.add_argument("--from", dest="from_name")
|
|
106
|
+
parser.add_argument("--host")
|
|
107
|
+
parser.add_argument("--port", type=int)
|
|
108
|
+
parser.add_argument("--tls", dest="tls", action="store_true", default=None)
|
|
109
|
+
parser.add_argument("--no-tls", dest="tls", action="store_false")
|
|
110
|
+
parser.add_argument("--tls-cert")
|
|
111
|
+
return parser
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
115
|
+
parser = build_parser()
|
|
116
|
+
args = parser.parse_args(argv)
|
|
117
|
+
endpoint = resolve_endpoint(
|
|
118
|
+
args.host, args.port, allow_discovery=True, tls=args.tls, tls_cert_path=args.tls_cert
|
|
119
|
+
)
|
|
120
|
+
try:
|
|
121
|
+
result = asyncio.run(
|
|
122
|
+
publish_to_channel(
|
|
123
|
+
endpoint.host,
|
|
124
|
+
endpoint.port,
|
|
125
|
+
args.channel,
|
|
126
|
+
args.text,
|
|
127
|
+
args.from_name,
|
|
128
|
+
tls=endpoint.tls,
|
|
129
|
+
data_dir=endpoint.data_dir,
|
|
130
|
+
tls_cert_path=endpoint.tls_cert_path,
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
except ValueError as exc:
|
|
134
|
+
print(f"inter-agent: {exc}", file=sys.stderr)
|
|
135
|
+
return 1
|
|
136
|
+
if result.error is not None:
|
|
137
|
+
print(
|
|
138
|
+
f"inter-agent: publish failed ({result.error.code}): {result.error.message}",
|
|
139
|
+
file=sys.stderr,
|
|
140
|
+
)
|
|
141
|
+
return 1
|
|
142
|
+
return 0
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
if __name__ == "__main__":
|
|
146
|
+
raise SystemExit(main())
|
inter_agent/core/send.py
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import asyncio
|
|
5
|
+
import json
|
|
6
|
+
import sys
|
|
7
|
+
import uuid
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import websockets
|
|
13
|
+
from websockets.asyncio.client import ClientConnection
|
|
14
|
+
|
|
15
|
+
from inter_agent.core.auth import AuthError, AuthProtocolError, client_handshake
|
|
16
|
+
from inter_agent.core.shared import control_hello, resolve_endpoint, resolve_shared_secret
|
|
17
|
+
from inter_agent.core.transport import client_ssl_context, websocket_uri
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass(frozen=True)
|
|
21
|
+
class ProtocolErrorResult:
|
|
22
|
+
"""Protocol error received after sending an outbound payload."""
|
|
23
|
+
|
|
24
|
+
code: str
|
|
25
|
+
message: str
|
|
26
|
+
raw: str
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True)
|
|
30
|
+
class SendResult:
|
|
31
|
+
"""Result returned after a command connection sends one outbound payload."""
|
|
32
|
+
|
|
33
|
+
welcome: str
|
|
34
|
+
welcome_payload: dict[str, object]
|
|
35
|
+
error: ProtocolErrorResult | None = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _text_frame(frame: str | bytes) -> str:
|
|
39
|
+
if isinstance(frame, bytes):
|
|
40
|
+
return frame.decode("utf-8")
|
|
41
|
+
return frame
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _json_object(raw: str) -> dict[str, object]:
|
|
45
|
+
payload: object = json.loads(raw)
|
|
46
|
+
if not isinstance(payload, dict):
|
|
47
|
+
raise ValueError("server response must be a JSON object")
|
|
48
|
+
return {str(key): value for key, value in payload.items()}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _protocol_error_from_payload(
|
|
52
|
+
raw: str, payload: dict[str, object]
|
|
53
|
+
) -> ProtocolErrorResult | None:
|
|
54
|
+
if payload.get("op") != "error":
|
|
55
|
+
return None
|
|
56
|
+
code = payload.get("code")
|
|
57
|
+
message = payload.get("message")
|
|
58
|
+
return ProtocolErrorResult(
|
|
59
|
+
code=code if isinstance(code, str) else "PROTOCOL_ERROR",
|
|
60
|
+
message=message if isinstance(message, str) else "protocol error",
|
|
61
|
+
raw=raw,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def parse_custom_payload(payload: str | None) -> object:
|
|
66
|
+
"""Parse a CLI custom payload string into a JSON-compatible value."""
|
|
67
|
+
if payload is None:
|
|
68
|
+
return {}
|
|
69
|
+
parsed: object = json.loads(payload)
|
|
70
|
+
return parsed
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
async def _recv_protocol_error(ws: ClientConnection, timeout: float) -> ProtocolErrorResult | None:
|
|
74
|
+
try:
|
|
75
|
+
raw = _text_frame(await asyncio.wait_for(ws.recv(), timeout=timeout))
|
|
76
|
+
except TimeoutError:
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
response = _json_object(raw)
|
|
80
|
+
return _protocol_error_from_payload(raw, response)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
async def send_message(
|
|
84
|
+
host: str,
|
|
85
|
+
port: int,
|
|
86
|
+
to: str | None,
|
|
87
|
+
text: str | None,
|
|
88
|
+
custom_type: str | None,
|
|
89
|
+
payload: object | None,
|
|
90
|
+
from_name: str | None = None,
|
|
91
|
+
response_timeout: float = 0.1,
|
|
92
|
+
*,
|
|
93
|
+
tls: bool = False,
|
|
94
|
+
data_dir: Path | None = None,
|
|
95
|
+
tls_cert_path: Path | None = None,
|
|
96
|
+
) -> SendResult:
|
|
97
|
+
"""Send a direct, broadcast, or custom message through a control connection."""
|
|
98
|
+
secret = resolve_shared_secret().secret
|
|
99
|
+
ssl_context = client_ssl_context(tls, data_dir, tls_cert_path)
|
|
100
|
+
async with websockets.connect(websocket_uri(host, port, tls), ssl=ssl_context) as ws:
|
|
101
|
+
try:
|
|
102
|
+
welcome = await client_handshake(ws, secret, control_hello(f"ctl-{uuid.uuid4()}"))
|
|
103
|
+
except AuthError as exc:
|
|
104
|
+
raise SystemExit(str(exc)) from exc
|
|
105
|
+
except (AuthProtocolError, json.JSONDecodeError, UnicodeDecodeError) as exc:
|
|
106
|
+
raise SystemExit(f"server protocol mismatch: {exc}") from exc
|
|
107
|
+
welcome_payload = _json_object(welcome)
|
|
108
|
+
welcome_error = _protocol_error_from_payload(welcome, welcome_payload)
|
|
109
|
+
if welcome_error is not None:
|
|
110
|
+
return SendResult(welcome=welcome, welcome_payload=welcome_payload, error=welcome_error)
|
|
111
|
+
if custom_type is not None:
|
|
112
|
+
msg: dict[str, object] = {
|
|
113
|
+
"op": "custom",
|
|
114
|
+
"custom_type": custom_type,
|
|
115
|
+
"payload": {} if payload is None else payload,
|
|
116
|
+
}
|
|
117
|
+
if to:
|
|
118
|
+
msg["to"] = to
|
|
119
|
+
if from_name:
|
|
120
|
+
msg["from_name"] = from_name
|
|
121
|
+
await ws.send(json.dumps(msg))
|
|
122
|
+
elif to:
|
|
123
|
+
outbound: dict[str, object] = {"op": "send", "to": to, "text": text or ""}
|
|
124
|
+
if from_name:
|
|
125
|
+
outbound["from_name"] = from_name
|
|
126
|
+
await ws.send(json.dumps(outbound))
|
|
127
|
+
else:
|
|
128
|
+
outbound = {"op": "broadcast", "text": text or ""}
|
|
129
|
+
if from_name:
|
|
130
|
+
outbound["from_name"] = from_name
|
|
131
|
+
await ws.send(json.dumps(outbound))
|
|
132
|
+
return SendResult(
|
|
133
|
+
welcome=welcome,
|
|
134
|
+
welcome_payload=welcome_payload,
|
|
135
|
+
error=await _recv_protocol_error(ws, response_timeout),
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
async def send_direct_message(
|
|
140
|
+
host: str,
|
|
141
|
+
port: int,
|
|
142
|
+
to: str,
|
|
143
|
+
text: str,
|
|
144
|
+
from_name: str | None = None,
|
|
145
|
+
*,
|
|
146
|
+
tls: bool = False,
|
|
147
|
+
data_dir: Path | None = None,
|
|
148
|
+
tls_cert_path: Path | None = None,
|
|
149
|
+
) -> SendResult:
|
|
150
|
+
"""Send a direct text message to one routing name."""
|
|
151
|
+
return await send_message(
|
|
152
|
+
host,
|
|
153
|
+
port,
|
|
154
|
+
to,
|
|
155
|
+
text,
|
|
156
|
+
custom_type=None,
|
|
157
|
+
payload=None,
|
|
158
|
+
from_name=from_name,
|
|
159
|
+
tls=tls,
|
|
160
|
+
data_dir=data_dir,
|
|
161
|
+
tls_cert_path=tls_cert_path,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
async def broadcast_message(
|
|
166
|
+
host: str,
|
|
167
|
+
port: int,
|
|
168
|
+
text: str,
|
|
169
|
+
from_name: str | None = None,
|
|
170
|
+
*,
|
|
171
|
+
tls: bool = False,
|
|
172
|
+
data_dir: Path | None = None,
|
|
173
|
+
tls_cert_path: Path | None = None,
|
|
174
|
+
) -> SendResult:
|
|
175
|
+
"""Broadcast a text message to all other connected agent sessions."""
|
|
176
|
+
return await send_message(
|
|
177
|
+
host,
|
|
178
|
+
port,
|
|
179
|
+
to=None,
|
|
180
|
+
text=text,
|
|
181
|
+
custom_type=None,
|
|
182
|
+
payload=None,
|
|
183
|
+
from_name=from_name,
|
|
184
|
+
tls=tls,
|
|
185
|
+
data_dir=data_dir,
|
|
186
|
+
tls_cert_path=tls_cert_path,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
async def send_custom_message(
|
|
191
|
+
host: str,
|
|
192
|
+
port: int,
|
|
193
|
+
to: str | None,
|
|
194
|
+
custom_type: str,
|
|
195
|
+
payload: object,
|
|
196
|
+
*,
|
|
197
|
+
tls: bool = False,
|
|
198
|
+
data_dir: Path | None = None,
|
|
199
|
+
tls_cert_path: Path | None = None,
|
|
200
|
+
) -> SendResult:
|
|
201
|
+
"""Send a custom protocol envelope through a control connection."""
|
|
202
|
+
return await send_message(
|
|
203
|
+
host,
|
|
204
|
+
port,
|
|
205
|
+
to,
|
|
206
|
+
text=None,
|
|
207
|
+
custom_type=custom_type,
|
|
208
|
+
payload=payload,
|
|
209
|
+
tls=tls,
|
|
210
|
+
data_dir=data_dir,
|
|
211
|
+
tls_cert_path=tls_cert_path,
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
216
|
+
parser = argparse.ArgumentParser(prog="inter-agent-send")
|
|
217
|
+
parser.add_argument("to", nargs="?")
|
|
218
|
+
parser.add_argument("text", nargs="?")
|
|
219
|
+
parser.add_argument("--host")
|
|
220
|
+
parser.add_argument("--port", type=int)
|
|
221
|
+
parser.add_argument("--tls", dest="tls", action="store_true", default=None)
|
|
222
|
+
parser.add_argument("--no-tls", dest="tls", action="store_false")
|
|
223
|
+
parser.add_argument("--tls-cert")
|
|
224
|
+
parser.add_argument("--to", dest="to_option")
|
|
225
|
+
parser.add_argument("--text", dest="text_option")
|
|
226
|
+
parser.add_argument("--custom-type")
|
|
227
|
+
parser.add_argument("--payload")
|
|
228
|
+
parser.add_argument("--from", dest="from_name")
|
|
229
|
+
return parser
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
233
|
+
parser = build_parser()
|
|
234
|
+
args = parser.parse_args(argv)
|
|
235
|
+
to = args.to_option or args.to
|
|
236
|
+
text = args.text_option if args.text_option is not None else args.text
|
|
237
|
+
payload = parse_custom_payload(args.payload) if args.custom_type is not None else None
|
|
238
|
+
endpoint = resolve_endpoint(
|
|
239
|
+
args.host, args.port, allow_discovery=True, tls=args.tls, tls_cert_path=args.tls_cert
|
|
240
|
+
)
|
|
241
|
+
result = asyncio.run(
|
|
242
|
+
send_message(
|
|
243
|
+
endpoint.host,
|
|
244
|
+
endpoint.port,
|
|
245
|
+
to,
|
|
246
|
+
text,
|
|
247
|
+
args.custom_type,
|
|
248
|
+
payload,
|
|
249
|
+
args.from_name,
|
|
250
|
+
tls=endpoint.tls,
|
|
251
|
+
data_dir=endpoint.data_dir,
|
|
252
|
+
tls_cert_path=endpoint.tls_cert_path,
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
if result.error is not None:
|
|
256
|
+
print(
|
|
257
|
+
f"inter-agent: delivery failed ({result.error.code}): {result.error.message}",
|
|
258
|
+
file=sys.stderr,
|
|
259
|
+
)
|
|
260
|
+
return 1
|
|
261
|
+
return 0
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
if __name__ == "__main__":
|
|
265
|
+
raise SystemExit(main())
|