agentconnect-cli 0.1.0a1__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.
- agentconnect/__init__.py +3 -0
- agentconnect/a2a_adapter/__init__.py +6 -0
- agentconnect/a2a_adapter/adapter.py +565 -0
- agentconnect/a2a_adapter/cards.py +149 -0
- agentconnect/a2a_adapter/convert.py +157 -0
- agentconnect/a2a_adapter/facade.py +560 -0
- agentconnect/a2a_adapter/seen.py +49 -0
- agentconnect/a2a_adapter/serve.py +67 -0
- agentconnect/adapter.py +319 -0
- agentconnect/adapters.py +52 -0
- agentconnect/apiclient.py +51 -0
- agentconnect/backoff.py +42 -0
- agentconnect/callback.py +314 -0
- agentconnect/cli.py +2324 -0
- agentconnect/cli_json.py +470 -0
- agentconnect/cli_support.py +444 -0
- agentconnect/clock.py +161 -0
- agentconnect/config.py +1213 -0
- agentconnect/daemon/__init__.py +41 -0
- agentconnect/deliveries.py +295 -0
- agentconnect/directory_client.py +255 -0
- agentconnect/failpoints.py +99 -0
- agentconnect/identity.py +111 -0
- agentconnect/isolation.py +345 -0
- agentconnect/keystore.py +279 -0
- agentconnect/limits.py +457 -0
- agentconnect/log.py +143 -0
- agentconnect/mcp/__init__.py +30 -0
- agentconnect/mcp/backend.py +405 -0
- agentconnect/mcp/server.py +382 -0
- agentconnect/mcp/session.py +373 -0
- agentconnect/mcp/tools.py +583 -0
- agentconnect/ownerfile.py +238 -0
- agentconnect/payloads.py +109 -0
- agentconnect/policy.py +1066 -0
- agentconnect/provision.py +239 -0
- agentconnect/py.typed +0 -0
- agentconnect/relay.py +405 -0
- agentconnect/runtime/__init__.py +46 -0
- agentconnect/runtime/agent.py +225 -0
- agentconnect/runtime/api.py +2043 -0
- agentconnect/runtime/engine.py +5359 -0
- agentconnect/runtime/errors.py +47 -0
- agentconnect/runtime/floor.py +649 -0
- agentconnect/runtime/groups.py +1351 -0
- agentconnect/runtime/knocks.py +1422 -0
- agentconnect/runtime/ledger.py +94 -0
- agentconnect/runtime/listing.py +1307 -0
- agentconnect/runtime/registry.py +53 -0
- agentconnect/runtime/runtime.py +158 -0
- agentconnect/runtime/sas.py +1002 -0
- agentconnect/sas_emoji.py +83 -0
- agentconnect/schemas.py +381 -0
- agentconnect/shapes.py +56 -0
- agentconnect/sockets.py +86 -0
- agentconnect/sse.py +234 -0
- agentconnect/store.py +3282 -0
- agentconnect/tasks.py +35 -0
- agentconnect/tokens.py +63 -0
- agentconnect/webhook.py +199 -0
- agentconnect_cli-0.1.0a1.dist-info/METADATA +103 -0
- agentconnect_cli-0.1.0a1.dist-info/RECORD +64 -0
- agentconnect_cli-0.1.0a1.dist-info/WHEEL +4 -0
- agentconnect_cli-0.1.0a1.dist-info/entry_points.txt +3 -0
agentconnect/tasks.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Stopping helper tasks without swallowing the caller's own cancellation.
|
|
2
|
+
|
|
3
|
+
``task.cancel(); with suppress(CancelledError): await task`` is wrong: if
|
|
4
|
+
the *calling* task is cancelled while it waits, its ``CancelledError`` is
|
|
5
|
+
suppressed too, and the caller carries on as if nothing happened (so
|
|
6
|
+
``Engine.stop`` or a shutdown that cancels a worker never takes effect).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def cancel_and_wait(
|
|
16
|
+
*tasks: asyncio.Future[Any], ignore: tuple[type[BaseException], ...] = ()
|
|
17
|
+
) -> None:
|
|
18
|
+
"""Cancel ``tasks`` and wait until every one has finished.
|
|
19
|
+
|
|
20
|
+
How a task ended is ignored when it is its own cancellation or an
|
|
21
|
+
exception in ``ignore``; any other exception is raised (the first, in
|
|
22
|
+
argument order). If the calling task is cancelled meanwhile, its
|
|
23
|
+
``CancelledError`` propagates: ``gather`` passes the caller's
|
|
24
|
+
cancellation on and reports the tasks' own as results.
|
|
25
|
+
|
|
26
|
+
The calling task itself, if it is among ``tasks``, is skipped: a task
|
|
27
|
+
can't wait for its own end, and gathering on itself would never return."""
|
|
28
|
+
current = asyncio.current_task()
|
|
29
|
+
tasks = tuple(t for t in tasks if t is not current)
|
|
30
|
+
for t in tasks:
|
|
31
|
+
t.cancel()
|
|
32
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
33
|
+
for r in results:
|
|
34
|
+
if isinstance(r, BaseException) and not isinstance(r, (asyncio.CancelledError, *ignore)):
|
|
35
|
+
raise r
|
agentconnect/tokens.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""The local API's bearer tokens and other 0600 secret files.
|
|
2
|
+
|
|
3
|
+
``api-token`` (agent scope) and ``human-token`` (human scope) live in the
|
|
4
|
+
data directory, created on first start. The runtime reads each once and
|
|
5
|
+
keeps only its SHA-256 (:class:`TokenDigest`), so the token's value is not
|
|
6
|
+
held in the runtime's memory (ADR-005, Decision 4). The CLI reads the file
|
|
7
|
+
per command; the SDK and agent integrations read ``api-token`` only.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import hashlib
|
|
13
|
+
import hmac
|
|
14
|
+
import os
|
|
15
|
+
import secrets
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import override
|
|
18
|
+
|
|
19
|
+
from agentconnect.keystore import write_private
|
|
20
|
+
|
|
21
|
+
API_TOKEN_FILE = "api-token" # noqa: S105 - a file name
|
|
22
|
+
HUMAN_TOKEN_FILE = "human-token" # noqa: S105 - a file name
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def load_token(path: Path) -> str:
|
|
26
|
+
"""Read the token at ``path``, creating it (0600) if missing. An existing
|
|
27
|
+
token file must be ours and private (no group or other bits)."""
|
|
28
|
+
if path.exists():
|
|
29
|
+
st = path.stat()
|
|
30
|
+
if st.st_mode & 0o077 or st.st_uid != os.geteuid():
|
|
31
|
+
msg = f"{path.name} must be owned by this user with mode 0600"
|
|
32
|
+
raise PermissionError(msg)
|
|
33
|
+
return path.read_text(encoding="ascii").strip()
|
|
34
|
+
token = secrets.token_urlsafe(32)
|
|
35
|
+
write_private(path, token.encode("ascii"))
|
|
36
|
+
return token
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TokenDigest:
|
|
40
|
+
"""The SHA-256 of a token, for a constant-time bearer check. The token's
|
|
41
|
+
value is never stored, and the digest is kept out of ``repr``."""
|
|
42
|
+
|
|
43
|
+
__slots__ = ("_digest",)
|
|
44
|
+
|
|
45
|
+
# SHA-256, not a password hash, on purpose: the tokens are 256 bits from
|
|
46
|
+
# secrets.token_urlsafe(32), not passwords people choose, so there is
|
|
47
|
+
# nothing to brute-force and no stretching (bcrypt, scrypt) to add. The
|
|
48
|
+
# hash only keeps the value out of memory; compare_digest keeps the
|
|
49
|
+
# comparison constant-time.
|
|
50
|
+
def __init__(self, token: str) -> None:
|
|
51
|
+
self._digest = hashlib.sha256(token.encode()).digest()
|
|
52
|
+
|
|
53
|
+
@classmethod
|
|
54
|
+
def of_file(cls, path: Path) -> TokenDigest:
|
|
55
|
+
"""Load (or create) the token file at ``path`` and keep its digest."""
|
|
56
|
+
return cls(load_token(path))
|
|
57
|
+
|
|
58
|
+
def matches(self, bearer_token: str) -> bool:
|
|
59
|
+
return hmac.compare_digest(hashlib.sha256(bearer_token.encode()).digest(), self._digest)
|
|
60
|
+
|
|
61
|
+
@override
|
|
62
|
+
def __repr__(self) -> str:
|
|
63
|
+
return "TokenDigest(<redacted>)"
|
agentconnect/webhook.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""The webhook adapter: POST every message and event to a callback URL.
|
|
2
|
+
|
|
3
|
+
Delivery is **at least once**:
|
|
4
|
+
|
|
5
|
+
* Messages go through the daemon's delivery loop, which keeps them in order
|
|
6
|
+
and in its durable inbound queue until :meth:`WebhookAdapter.deliver`
|
|
7
|
+
returns ``ACK``. A 2xx response is an ack; any other response, a timeout
|
|
8
|
+
or a connection error is ``RETRY``, and the daemon retries with
|
|
9
|
+
exponential backoff on its clock. Nothing later is delivered before it.
|
|
10
|
+
* Events (``member_joined``, ``epoch_changed``, ``loop_guard``, ...) are
|
|
11
|
+
posted at once. If that fails, they wait in a bounded in-memory queue with
|
|
12
|
+
the same retry rule, and ``on_event`` raises, so the daemon keeps the event
|
|
13
|
+
unnotified and sends it again after a reconnect or restart (ADR-004). The
|
|
14
|
+
repeat carries the same ``event_id``.
|
|
15
|
+
|
|
16
|
+
A receiver can therefore see a payload twice (the ack was lost) and must
|
|
17
|
+
treat ``msg_id`` / ``event_id`` as idempotency keys. The Python SDK does.
|
|
18
|
+
|
|
19
|
+
Every request is signed: ``X-AgentConnect-Signature: v0=<hex>`` is
|
|
20
|
+
HMAC-SHA256 over ``<timestamp>.<body>`` with the adapter's secret, and
|
|
21
|
+
``X-AgentConnect-Timestamp`` is the Unix time in seconds. Each attempt is
|
|
22
|
+
signed afresh, so a captured request stops verifying after the receiver's
|
|
23
|
+
skew window (5 minutes in the SDK).
|
|
24
|
+
|
|
25
|
+
The callback is loopback unless the config opts in (``webhook.allow_remote``)
|
|
26
|
+
and then it must be ``https``; redirects are never followed.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
import asyncio
|
|
32
|
+
import hashlib
|
|
33
|
+
import hmac
|
|
34
|
+
from collections import deque
|
|
35
|
+
from typing import Any, override
|
|
36
|
+
from urllib.parse import urlsplit
|
|
37
|
+
|
|
38
|
+
import httpx
|
|
39
|
+
|
|
40
|
+
from agentconnect import log
|
|
41
|
+
from agentconnect.adapter import (
|
|
42
|
+
Capabilities,
|
|
43
|
+
Delivery,
|
|
44
|
+
DeliveryResult,
|
|
45
|
+
Event,
|
|
46
|
+
GroupView,
|
|
47
|
+
OutboundSend,
|
|
48
|
+
Sender,
|
|
49
|
+
)
|
|
50
|
+
from agentconnect.backoff import Backoff
|
|
51
|
+
from agentconnect.clock import Clock, unix_seconds
|
|
52
|
+
from agentconnect.payloads import encode, event_payload, message_payload
|
|
53
|
+
from agentconnect.tasks import cancel_and_wait
|
|
54
|
+
|
|
55
|
+
LOG = log.get("agentconnect.webhook")
|
|
56
|
+
|
|
57
|
+
SIGNATURE_HEADER = "X-AgentConnect-Signature"
|
|
58
|
+
TIMESTAMP_HEADER = "X-AgentConnect-Timestamp"
|
|
59
|
+
SIGNATURE_VERSION = "v0"
|
|
60
|
+
SECRET_FILE = "webhook-secret" # noqa: S105 - a file name
|
|
61
|
+
EVENT_QUEUE_LIMIT = 1000
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def sign(secret: str, timestamp: str, body: bytes) -> str:
|
|
65
|
+
"""``v0=<hex>``: HMAC-SHA256 over ``<timestamp>.<body>``."""
|
|
66
|
+
mac = hmac.new(secret.encode(), timestamp.encode() + b"." + body, hashlib.sha256)
|
|
67
|
+
return f"{SIGNATURE_VERSION}={mac.hexdigest()}"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class WebhookAdapter:
|
|
71
|
+
"""Implements :class:`agentconnect.adapter.Adapter` over HTTP POST."""
|
|
72
|
+
|
|
73
|
+
name = "webhook"
|
|
74
|
+
|
|
75
|
+
def __init__( # noqa: PLR0913
|
|
76
|
+
self,
|
|
77
|
+
url: str,
|
|
78
|
+
secret: str,
|
|
79
|
+
clock: Clock,
|
|
80
|
+
*,
|
|
81
|
+
transport: httpx.AsyncBaseTransport | None = None,
|
|
82
|
+
timeout: float = 10.0,
|
|
83
|
+
backoff: Backoff | None = None,
|
|
84
|
+
) -> None:
|
|
85
|
+
self.url = url
|
|
86
|
+
self._secret = secret
|
|
87
|
+
self._clock = clock
|
|
88
|
+
self._transport, self._timeout = transport, timeout
|
|
89
|
+
self._client: httpx.AsyncClient | None = None
|
|
90
|
+
self._backoff = backoff or Backoff(base=1.0, cap=300.0)
|
|
91
|
+
self._events: deque[dict[str, Any]] = deque()
|
|
92
|
+
self._wake = asyncio.Event()
|
|
93
|
+
self._worker: asyncio.Task[None] | None = None
|
|
94
|
+
self._inflight = 0
|
|
95
|
+
self._quiet = asyncio.Event()
|
|
96
|
+
self._quiet.set()
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def _http(self) -> httpx.AsyncClient:
|
|
100
|
+
if self._client is None:
|
|
101
|
+
self._client = httpx.AsyncClient(
|
|
102
|
+
transport=self._transport,
|
|
103
|
+
timeout=self._timeout,
|
|
104
|
+
follow_redirects=False,
|
|
105
|
+
trust_env=False,
|
|
106
|
+
)
|
|
107
|
+
return self._client
|
|
108
|
+
|
|
109
|
+
@override
|
|
110
|
+
def __repr__(self) -> str:
|
|
111
|
+
host = urlsplit(self.url).netloc
|
|
112
|
+
return f"WebhookAdapter(host={host!r})"
|
|
113
|
+
|
|
114
|
+
def capabilities(self) -> Capabilities:
|
|
115
|
+
return Capabilities(accepts=["text", "data"], originates=False)
|
|
116
|
+
|
|
117
|
+
async def on_outbound(self, send: OutboundSend) -> None:
|
|
118
|
+
# Outbound messages go through the local API; start the event worker.
|
|
119
|
+
self._ensure_worker()
|
|
120
|
+
|
|
121
|
+
def _ensure_worker(self) -> None:
|
|
122
|
+
if self._worker is None or self._worker.done():
|
|
123
|
+
self._worker = asyncio.get_running_loop().create_task(self._run_events())
|
|
124
|
+
|
|
125
|
+
async def drain(self) -> None:
|
|
126
|
+
"""Wait until no request is in flight (for orderly shutdown in tests)."""
|
|
127
|
+
await self._quiet.wait()
|
|
128
|
+
|
|
129
|
+
async def close(self) -> None:
|
|
130
|
+
try:
|
|
131
|
+
if self._worker is not None:
|
|
132
|
+
await cancel_and_wait(self._worker)
|
|
133
|
+
self._worker = None
|
|
134
|
+
finally:
|
|
135
|
+
if self._client is not None:
|
|
136
|
+
await self._client.aclose()
|
|
137
|
+
self._client = None
|
|
138
|
+
|
|
139
|
+
async def _post(self, payload: dict[str, Any]) -> bool:
|
|
140
|
+
body = encode(payload)
|
|
141
|
+
stamp = str(unix_seconds(self._clock))
|
|
142
|
+
headers = {
|
|
143
|
+
"content-type": "application/json",
|
|
144
|
+
TIMESTAMP_HEADER: stamp,
|
|
145
|
+
SIGNATURE_HEADER: sign(self._secret, stamp, body),
|
|
146
|
+
}
|
|
147
|
+
self._inflight += 1
|
|
148
|
+
self._quiet.clear()
|
|
149
|
+
try:
|
|
150
|
+
resp = await self._http.post(self.url, content=body, headers=headers)
|
|
151
|
+
except httpx.HTTPError as e:
|
|
152
|
+
LOG.warning("webhook_unreachable", error=type(e).__name__)
|
|
153
|
+
return False
|
|
154
|
+
finally:
|
|
155
|
+
self._inflight -= 1
|
|
156
|
+
if self._inflight == 0:
|
|
157
|
+
self._quiet.set()
|
|
158
|
+
if 200 <= resp.status_code < 300: # noqa: PLR2004
|
|
159
|
+
return True
|
|
160
|
+
LOG.warning("webhook_rejected", status=resp.status_code)
|
|
161
|
+
return False
|
|
162
|
+
|
|
163
|
+
async def deliver(
|
|
164
|
+
self, group: GroupView, sender: Sender, to: list[str], inner: Delivery
|
|
165
|
+
) -> DeliveryResult:
|
|
166
|
+
ok = await self._post(message_payload(group, inner))
|
|
167
|
+
return DeliveryResult.ACK if ok else DeliveryResult.RETRY
|
|
168
|
+
|
|
169
|
+
async def on_event(self, group: GroupView | None, event: Event) -> None:
|
|
170
|
+
"""Send the event now when nothing is waiting ahead of it. If that
|
|
171
|
+
fails, or earlier events are still queued (order), queue it for the
|
|
172
|
+
retry worker and raise: the daemon then keeps the event unnotified
|
|
173
|
+
and sends it again after a reconnect or restart. The receiver dedupes
|
|
174
|
+
by ``event_id``, so the repeat is harmless."""
|
|
175
|
+
payload = event_payload(group, event)
|
|
176
|
+
if not self._events and await self._post(payload):
|
|
177
|
+
return
|
|
178
|
+
if all(p["event_id"] != event.event_id for p in self._events):
|
|
179
|
+
if len(self._events) >= EVENT_QUEUE_LIMIT:
|
|
180
|
+
self._events.popleft()
|
|
181
|
+
LOG.warning("webhook_event_dropped")
|
|
182
|
+
self._events.append(payload)
|
|
183
|
+
self._wake.set()
|
|
184
|
+
self._ensure_worker()
|
|
185
|
+
msg = "webhook not reachable"
|
|
186
|
+
raise ConnectionError(msg)
|
|
187
|
+
|
|
188
|
+
async def _run_events(self) -> None:
|
|
189
|
+
while True:
|
|
190
|
+
self._wake.clear()
|
|
191
|
+
while self._events:
|
|
192
|
+
head = self._events[0]
|
|
193
|
+
if await self._post(head):
|
|
194
|
+
if self._events and self._events[0] is head:
|
|
195
|
+
self._events.popleft()
|
|
196
|
+
self._backoff.reset()
|
|
197
|
+
else:
|
|
198
|
+
await self._clock.sleep(self._backoff.next())
|
|
199
|
+
await self._wake.wait()
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agentconnect-cli
|
|
3
|
+
Version: 0.1.0a1
|
|
4
|
+
Summary: AgentConnect daemon (agentconnectd) and CLI
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Requires-Dist: a2a-sdk[http-server,signing]>=1.2.2
|
|
7
|
+
Requires-Dist: agentconnect-core==0.1.0a1
|
|
8
|
+
Requires-Dist: httpx>=0.28.1
|
|
9
|
+
Requires-Dist: keyring>=25.7.0
|
|
10
|
+
Requires-Dist: mcp>=2.3.0
|
|
11
|
+
Requires-Dist: pyjwt[crypto]>=2.15.1
|
|
12
|
+
Requires-Dist: structlog>=26.1.0
|
|
13
|
+
Requires-Dist: typer>=0.27.3
|
|
14
|
+
Requires-Dist: uvicorn>=0.54.0
|
|
15
|
+
Requires-Dist: websockets>=17.2
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# agentconnect-cli
|
|
20
|
+
|
|
21
|
+
The AgentConnect daemon (`agentconnectd`) and CLI (`agentconnect`). The
|
|
22
|
+
distribution is `agentconnect-cli`; the command and the import package are
|
|
23
|
+
`agentconnect`. See the repository [README](https://github.com/mbocevski/agent-connect/blob/v0.1.0a1/README.md) and
|
|
24
|
+
[SPEC.md](https://github.com/mbocevski/agent-connect/blob/v0.1.0a1/SPEC.md).
|
|
25
|
+
|
|
26
|
+
```sh
|
|
27
|
+
agentconnect daemon run [--config PATH] [--tcp-port N] # foreground, JSON logs on stderr
|
|
28
|
+
agentconnect status [--json] # reads the local API
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Everything protocol-related is in the runtime, `agentconnect.runtime`
|
|
32
|
+
(`Runtime`, with its engine, local API, adapters and agent-scope
|
|
33
|
+
`AgentApi`). `agentconnectd` (`agentconnect.daemon`) is one host of it; the
|
|
34
|
+
Python SDK's embedded mode (`AgentConnect.embedded`, [docs/sdk.md](https://github.com/mbocevski/agent-connect/blob/v0.1.0a1/docs/sdk.md))
|
|
35
|
+
is the other. Both run the same checks, and both serve the local API below.
|
|
36
|
+
|
|
37
|
+
The invite lifecycle (`group`, `invite`, `accept`, `groups`, `approvals`,
|
|
38
|
+
`verify`, `leave`, `send`, `events`) is documented in
|
|
39
|
+
[docs/cli.md](https://github.com/mbocevski/agent-connect/blob/v0.1.0a1/docs/cli.md), with the `--json` schemas and exit codes.
|
|
40
|
+
`approvals approve`, `approvals reject` and `verify` use the human token:
|
|
41
|
+
**never give an agent the human token or let it run those commands for the
|
|
42
|
+
human.**
|
|
43
|
+
|
|
44
|
+
Don't give agents shell or file access to the data directory either: an
|
|
45
|
+
agent that can read it can approve joins and mark codes verified as you,
|
|
46
|
+
because the human token is a file there. Agents that need a shell can run
|
|
47
|
+
as another OS user, with the daemon under a user of its own
|
|
48
|
+
([docs/isolation.md](https://github.com/mbocevski/agent-connect/blob/v0.1.0a1/docs/isolation.md)).
|
|
49
|
+
|
|
50
|
+
## What the daemon holds
|
|
51
|
+
|
|
52
|
+
- One identity and one instance, created on first start, in the OS keyring
|
|
53
|
+
when one is usable and otherwise in 0600 key files (`keystore = "auto" |
|
|
54
|
+
"keyring" | "file"`).
|
|
55
|
+
- The core's MLS state in `mls.sqlite3`, a SQLCipher database encrypted
|
|
56
|
+
under a `db_key` from the same keystore.
|
|
57
|
+
- Its own `daemon.sqlite3`: groups, members (with the local *verified*
|
|
58
|
+
flag), Agent Cards, out-of-sync flags, and the durable outbox and inbox.
|
|
59
|
+
It is a SQLCipher database too, under a key the core derives from
|
|
60
|
+
`db_key` (ADR-003). Plaintext stays only while it is needed: an outbox
|
|
61
|
+
row keeps only its ids once the relay has answered it, and an inbox row
|
|
62
|
+
drops its message once the adapter has accepted it.
|
|
63
|
+
|
|
64
|
+
The data directory (`~/.local/share/agentconnect`) is 0700 and every file in
|
|
65
|
+
it 0600. Configuration is `~/.config/agentconnect/config.toml`; see
|
|
66
|
+
[docs/config.md](https://github.com/mbocevski/agent-connect/blob/v0.1.0a1/docs/config.md) for the keys and the `AC_*` overrides.
|
|
67
|
+
|
|
68
|
+
## Local control API
|
|
69
|
+
|
|
70
|
+
JSON over HTTP/1.1 on a Unix socket in the data directory, or on
|
|
71
|
+
`127.0.0.1:<port>`. Every request needs `Authorization: Bearer <token>`.
|
|
72
|
+
[docs/api.md](https://github.com/mbocevski/agent-connect/blob/v0.1.0a1/docs/api.md) describes the transport, tokens, errors and
|
|
73
|
+
every route with its request and response shapes.
|
|
74
|
+
|
|
75
|
+
Two tokens live in the data directory: `api-token` (agent scope) and
|
|
76
|
+
`human-token` (human scope, also accepted for everything else). Approving a
|
|
77
|
+
join and marking a member verified are human decisions (SPEC §6.2, §11): an
|
|
78
|
+
agent that could approve its own joins, or mark the short code as compared,
|
|
79
|
+
would remove exactly the checks that catch a leaked link or a relay in the
|
|
80
|
+
middle. Give agent tooling only `api-token`. Both files belong to the same OS
|
|
81
|
+
user, so the split keeps the human capability out of agent integrations by
|
|
82
|
+
default; it is not a sandbox.
|
|
83
|
+
|
|
84
|
+
Invite secrets never leave the daemon: the URL is returned to the caller and
|
|
85
|
+
the relay only receives the public invite metadata.
|
|
86
|
+
|
|
87
|
+
## Adapters
|
|
88
|
+
|
|
89
|
+
`agentconnect.adapter.Adapter` is the interface for agent integrations:
|
|
90
|
+
`deliver(group, sender, to, inner) -> ack | retry`, `on_event(group, event)`,
|
|
91
|
+
`on_outbound(send)` and `capabilities()`. Every delivery carries the SPEC
|
|
92
|
+
§10.1 untrusted framing. A relay frame is acknowledged only after the core
|
|
93
|
+
processed it and the adapter accepted the resulting delivery.
|
|
94
|
+
|
|
95
|
+
Delivery is **at least once**: after a crash, `deliver` and `on_event` may
|
|
96
|
+
repeat a message or an event. Every delivery has a stable `msg_id` and every
|
|
97
|
+
event a stable `event_id`; deduplicate by them. Nothing is lost: an event is
|
|
98
|
+
recorded before the adapter hears of it and sent again on restart until
|
|
99
|
+
`on_event` returns (ADR-004).
|
|
100
|
+
The webhook adapter ([docs/adapters/webhook.md](https://github.com/mbocevski/agent-connect/blob/v0.1.0a1/docs/adapters/webhook.md)) and the SSE adapter (`GET /v0/events`, [docs/api.md](https://github.com/mbocevski/agent-connect/blob/v0.1.0a1/docs/api.md#event-stream)) serve headless agents.
|
|
101
|
+
`CallbackAdapter` (`agentconnect.callback`) is the embedded SDK's in-process adapter, with the webhook's retry rules. `RecordingAdapter` is the in-memory adapter used by tests. Every message in
|
|
102
|
+
and out goes through the safety policy (`agentconnect.policy`) before any
|
|
103
|
+
adapter sees it ([docs/safety.md](https://github.com/mbocevski/agent-connect/blob/v0.1.0a1/docs/safety.md)).
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
agentconnect/__init__.py,sha256=bsDXOpok9J8_HaxKa8g2w5Horjj3pO3C3CHypha20rE,60
|
|
2
|
+
agentconnect/a2a_adapter/__init__.py,sha256=xkEkmb6Kh7t0rSUkdt6Y57ki6M6awjZqTd7--Y7EA8M,249
|
|
3
|
+
agentconnect/a2a_adapter/adapter.py,sha256=tMQHp_UpXUwaUMNYgWQ_H9OYZBFUPS8CVoWoEpKzR7M,24019
|
|
4
|
+
agentconnect/a2a_adapter/cards.py,sha256=D49D0B3M0LPErmI-fAh8hh7xnxKvoswNeb0M_g0Zf3Y,5291
|
|
5
|
+
agentconnect/a2a_adapter/convert.py,sha256=A6POHwZXlAlbdojXcWHmcgGfq79KxnJlxQ4O1we6Ijo,5470
|
|
6
|
+
agentconnect/a2a_adapter/facade.py,sha256=921kpYNFhpZQV9F51guGNLy-gp-O2xEkgUpGzRlOdao,23155
|
|
7
|
+
agentconnect/a2a_adapter/seen.py,sha256=YDOwFVbPTGjrojcDQbMS52VkZGcNDPdhX83g1ET35_Q,1680
|
|
8
|
+
agentconnect/a2a_adapter/serve.py,sha256=U9HXGItnEqPbTvDw90Lc62fKNAUj0Dm2KtM6A49zNbU,2164
|
|
9
|
+
agentconnect/adapter.py,sha256=mjDQc5WRqB_Yb_ZI31L96AzDEG3fU2Oo3ew18KVijeo,11043
|
|
10
|
+
agentconnect/adapters.py,sha256=WnIZ_QoeRT5pysFpY7ukX1QV3PHotbU8zftEGofng_M,2425
|
|
11
|
+
agentconnect/apiclient.py,sha256=nNpwBpkj2WovWz51XDRCRg92Zmpov3-f9j3JE89enuY,2059
|
|
12
|
+
agentconnect/backoff.py,sha256=hmBRJyMegRmNrlGNJLFE6fASVcuaDPYF3DV759bLaqo,1358
|
|
13
|
+
agentconnect/callback.py,sha256=4OW64hpeIA9pZNQ7Sm6b5usqsNE66Gv24Uf-uIAMTcY,13224
|
|
14
|
+
agentconnect/cli.py,sha256=lj8Jcr2JX95dXN-pyxQ2OTAogQ8fAOHE6pQPLc2PPmQ,95692
|
|
15
|
+
agentconnect/cli_json.py,sha256=l0O9_ffTXw-V7RERAHlA9-i-cZGNub8VG2Mu7R8jiHA,15950
|
|
16
|
+
agentconnect/cli_support.py,sha256=SW8xzdXht2rOPe5B_LsLewmaL4xTnVq077_XlCkYjY8,16936
|
|
17
|
+
agentconnect/clock.py,sha256=oXLBoyrD67daCGt-8i0kLPeGN1q_KG5ORA3mE-SVXCI,5631
|
|
18
|
+
agentconnect/config.py,sha256=2JLWf-7TUQMbSe41MCPNVjXcV7d8oiClMAMTibSnAMw,51265
|
|
19
|
+
agentconnect/daemon/__init__.py,sha256=vJk7Nc3pdvFPAeiF_PeWwpy2elGxLOEznicVBN5B_3s,1456
|
|
20
|
+
agentconnect/deliveries.py,sha256=lK9nMSszUZflsHu6uFIly_jnjVAEKwT_-u66XzC7DLI,12106
|
|
21
|
+
agentconnect/directory_client.py,sha256=C_PvVCYHUP7Z12t8A79sKvaSRI7VKOINCLV_GYcYY-M,10031
|
|
22
|
+
agentconnect/failpoints.py,sha256=GcCNIuF0-95uMdbjhswAbtEWme3o0CWK3WYtdP7XH5g,4496
|
|
23
|
+
agentconnect/identity.py,sha256=YxKeG8HTmMKK_0VkXfXnssKdjU-93EE0ia8_04nnP_U,3883
|
|
24
|
+
agentconnect/isolation.py,sha256=8glVNZxHMPbZ-oAsFceFjKwEjH2C6m4u3cx851JEADA,13406
|
|
25
|
+
agentconnect/keystore.py,sha256=wtxjMr3j5kpqpwJyqoe0GRUOiw0qecoaLSLIzKqexrE,9525
|
|
26
|
+
agentconnect/limits.py,sha256=aBRJpjA9JoJvi7eG87tJvSK9OUAtpGjAA20FKFFgOLA,18787
|
|
27
|
+
agentconnect/log.py,sha256=qgLeXr4lP5T43k4Tr4VYjBVplwrTzoX1rGL2Vp9DSc0,4144
|
|
28
|
+
agentconnect/mcp/__init__.py,sha256=tFETWMpHa02lUm4folUmiV9QPWYb0TySN9xVysuvoik,854
|
|
29
|
+
agentconnect/mcp/backend.py,sha256=qZkVRf9QOW0uTJ8ufJJhpYwBVIsnAmKu5ZTdHcoesII,15593
|
|
30
|
+
agentconnect/mcp/server.py,sha256=L0RX5zUdvoYfOF47ufNMXZ-Hhj-80-JFBmS2bZbSBBA,14641
|
|
31
|
+
agentconnect/mcp/session.py,sha256=XwBv7GiwFsNKY9qsRaXb5tsdDSq8VX7o24rNLtd5rbI,15902
|
|
32
|
+
agentconnect/mcp/tools.py,sha256=duUEt1IXusgH1Wpu1XGvM8dZ_U1GDYCy5nWRe3JlFMs,21696
|
|
33
|
+
agentconnect/ownerfile.py,sha256=v8L0wXcuTYg0xrrBvixXEfdOoHbG5poCm7EToyP7wMI,8520
|
|
34
|
+
agentconnect/payloads.py,sha256=Nha7_yGQpQ1LTJBk0qwgTb75ZGYJ-zZm-YrV7Ycl4zE,3737
|
|
35
|
+
agentconnect/policy.py,sha256=y0i3F_ZGRVKT7jhOUT0N2Bip9jCBH98XEJATTEEIDgk,41104
|
|
36
|
+
agentconnect/provision.py,sha256=85hG9Sm5RQ3vfRq1kjRj7DPECXJ5evXACgtxJ6hz-Aw,10015
|
|
37
|
+
agentconnect/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
38
|
+
agentconnect/relay.py,sha256=NHZ7BJbiwHI-ENz7tyJcydHXObBugEGwEPq5N5SD-KM,16524
|
|
39
|
+
agentconnect/runtime/__init__.py,sha256=-R18y4c8GEI1jsMnCNwSlhtDEfmJg-O_O_EQKD4sRB0,1535
|
|
40
|
+
agentconnect/runtime/agent.py,sha256=EpGX1GKXxF22x4mc8sPAx3tF6qTqxbzH71OY7B3UvJQ,10298
|
|
41
|
+
agentconnect/runtime/api.py,sha256=ntQcuw4QbThHovlyTGW0PMiVuZuC1AVNK863rlxVUFY,89586
|
|
42
|
+
agentconnect/runtime/engine.py,sha256=NMk1hUwFPZ_-yLJPpmS1kvYV69TTHVqhfusbHu2Mc6I,248443
|
|
43
|
+
agentconnect/runtime/errors.py,sha256=28QEhRUh1NYriVWV_Z9Ic7KdQzOlsEXN_SmRI5ya18c,1412
|
|
44
|
+
agentconnect/runtime/floor.py,sha256=kczLLji-8brEILTA3-4AYjfmd2RmoWFZ_nImXnM51QQ,30026
|
|
45
|
+
agentconnect/runtime/groups.py,sha256=h47KUZHwSIeJpwnhtPQy9RwSRVYFnkyB-kO-sXmlBUs,63499
|
|
46
|
+
agentconnect/runtime/knocks.py,sha256=-Cs6TAGskxXQ3iZcc2XW_RG3_MDW-16E4NObvP0U9Zo,63059
|
|
47
|
+
agentconnect/runtime/ledger.py,sha256=2wGmTc4Ba893uWgYkBhAVYumGF6Ep426ukZV5dnHpgg,4002
|
|
48
|
+
agentconnect/runtime/listing.py,sha256=dnfsfOQErWK4z3J1Hio1jzkZyGbwdU_cdfOSCM074yM,59261
|
|
49
|
+
agentconnect/runtime/registry.py,sha256=pYjEFglau2syD8GLqcuXAYA8Ve0y1api7fI5Aydwf9E,1776
|
|
50
|
+
agentconnect/runtime/runtime.py,sha256=0_7ppJ-RJTWjPxY3M13uWkSq_lzTlnMVb-9A8Gm7AZ4,6038
|
|
51
|
+
agentconnect/runtime/sas.py,sha256=ZKNqklBmqWgiVnzhzr4OTgtJE40C4e1pyq1qOZDmbRg,43312
|
|
52
|
+
agentconnect/sas_emoji.py,sha256=5DVw27KmRp81c-C_diteTzkH7t35OZTjAzMzh5xm4bc,2097
|
|
53
|
+
agentconnect/schemas.py,sha256=glRv29sy62y12hliqR2MKsS7Tk0QYV5f-jpavhVo908,14195
|
|
54
|
+
agentconnect/shapes.py,sha256=fHEwMWyxLBP2S4Y3QIokh-QblRXPUI8NmOfi27uth4s,1946
|
|
55
|
+
agentconnect/sockets.py,sha256=Ia8p8cBn4ZwHwSnd5noD2ksr_FHRwd3bXm17kZDXtYc,3674
|
|
56
|
+
agentconnect/sse.py,sha256=wdCnxFEY0VF_XvxvMuD-O0LiLls7Db-5ahr2f9gTeh8,8644
|
|
57
|
+
agentconnect/store.py,sha256=hSTqOJ9bpOlofjMFrOjs-KB1ObotpBluwwbeHfGcgec,132319
|
|
58
|
+
agentconnect/tasks.py,sha256=rKwA4AtBXWrxlMMKazuEdt51l_jZAeFAWjMBcxwOBBw,1458
|
|
59
|
+
agentconnect/tokens.py,sha256=CFV3v1HjxBBzuO3U17xaC4DFrYE89nd5JngoYAabR48,2360
|
|
60
|
+
agentconnect/webhook.py,sha256=jd_lvGPdlGecFwTDuU6HWhvHLgEd7v1v-3cInvbrcrE,7373
|
|
61
|
+
agentconnect_cli-0.1.0a1.dist-info/WHEEL,sha256=1dD_GoEqEIxQ7VnrUExgdj_eExY4b4UgfR0iBlGKVac,80
|
|
62
|
+
agentconnect_cli-0.1.0a1.dist-info/entry_points.txt,sha256=R0I5wTCDhHPFR4SWEr2j4uDfNXpJjC-CkqHVVPRliAI,55
|
|
63
|
+
agentconnect_cli-0.1.0a1.dist-info/METADATA,sha256=ABcvhL4lNZyf4iKjmsLldaj4zDUYzGhArEL5nNHgVI0,5722
|
|
64
|
+
agentconnect_cli-0.1.0a1.dist-info/RECORD,,
|