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
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""The runtime's adapters, by name.
|
|
2
|
+
|
|
3
|
+
A group delivers to the adapters of its list, by name: the list the human
|
|
4
|
+
pinned (the ``adapters`` column of the groups table), else the config's
|
|
5
|
+
(``daemon.adapters``). A group none of whose adapters is registered delivers
|
|
6
|
+
to the first one registered (:meth:`AdapterRegistry.first`). Registration
|
|
7
|
+
closes when the runtime starts.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections.abc import Iterable, Iterator, Mapping
|
|
13
|
+
from typing import override
|
|
14
|
+
|
|
15
|
+
from agentconnect.adapter import Adapter
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AdapterRegistry(Mapping[str, Adapter]):
|
|
19
|
+
def __init__(self, adapters: Iterable[Adapter] = ()) -> None:
|
|
20
|
+
self._by_name: dict[str, Adapter] = {}
|
|
21
|
+
self._closed = False
|
|
22
|
+
for adapter in adapters:
|
|
23
|
+
self.register(adapter)
|
|
24
|
+
|
|
25
|
+
def register(self, adapter: Adapter) -> None:
|
|
26
|
+
"""Add an adapter; only before the runtime starts."""
|
|
27
|
+
if self._closed:
|
|
28
|
+
msg = "adapters must be registered before the runtime starts"
|
|
29
|
+
raise RuntimeError(msg)
|
|
30
|
+
if adapter.name in self._by_name:
|
|
31
|
+
msg = f"an adapter named {adapter.name!r} is already registered"
|
|
32
|
+
raise ValueError(msg)
|
|
33
|
+
self._by_name[adapter.name] = adapter
|
|
34
|
+
|
|
35
|
+
def close(self) -> None:
|
|
36
|
+
"""No more registrations (the runtime is starting)."""
|
|
37
|
+
self._closed = True
|
|
38
|
+
|
|
39
|
+
def first(self) -> Adapter | None:
|
|
40
|
+
"""The first adapter registered, if any."""
|
|
41
|
+
return next(iter(self._by_name.values()), None)
|
|
42
|
+
|
|
43
|
+
@override
|
|
44
|
+
def __getitem__(self, name: str) -> Adapter:
|
|
45
|
+
return self._by_name[name]
|
|
46
|
+
|
|
47
|
+
@override
|
|
48
|
+
def __iter__(self) -> Iterator[str]:
|
|
49
|
+
return iter(self._by_name)
|
|
50
|
+
|
|
51
|
+
@override
|
|
52
|
+
def __len__(self) -> int:
|
|
53
|
+
return len(self._by_name)
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""The runtime: one identity and instance, hosted by ``agentconnectd`` or
|
|
2
|
+
inside an agent's process (ADR-005, Decision 1).
|
|
3
|
+
|
|
4
|
+
A :class:`Runtime` owns the data-directory lock, the protocol engine (keys,
|
|
5
|
+
both databases, the relay link, the journal, the inbox and outbox, the
|
|
6
|
+
safety policy, approvals, audit and transcript), the adapters, and the local
|
|
7
|
+
API server. Hosts decide only where the configuration comes from, when the
|
|
8
|
+
runtime starts and stops, which adapters they register and what surface the
|
|
9
|
+
agent sees. No host reimplements or overrides a check.
|
|
10
|
+
|
|
11
|
+
The local API (with the agent and human tokens) is served whenever the
|
|
12
|
+
runtime runs, in every host, so the human can always run the CLI's
|
|
13
|
+
human-only commands against it. Neither :class:`Runtime` nor its
|
|
14
|
+
:attr:`Runtime.agent` offers a human-scope operation.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import contextlib
|
|
20
|
+
from collections.abc import Iterable
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from agentconnect.adapter import Adapter
|
|
25
|
+
from agentconnect.clock import Clock
|
|
26
|
+
from agentconnect.config import Config
|
|
27
|
+
from agentconnect.isolation import PeerGate
|
|
28
|
+
from agentconnect.keystore import Keystore
|
|
29
|
+
from agentconnect.runtime.agent import AgentApi
|
|
30
|
+
from agentconnect.runtime.api import ApiServer
|
|
31
|
+
from agentconnect.runtime.engine import Engine, Options
|
|
32
|
+
from agentconnect.runtime.registry import AdapterRegistry
|
|
33
|
+
from agentconnect.store import HANDLED_KEEP
|
|
34
|
+
|
|
35
|
+
STOP_FLUSH_SECS = 3.0 # how long stop() lets queued sends go out
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class HandledIds:
|
|
39
|
+
"""The ``msg_id`` and ``event_id`` values an in-process agent's handlers
|
|
40
|
+
finished with, in the runtime's encrypted database, so a redelivery
|
|
41
|
+
after a restart doesn't reach user code again. Keeps the newest
|
|
42
|
+
:data:`HANDLED_KEEP`."""
|
|
43
|
+
|
|
44
|
+
def __init__(self, engine: Engine) -> None:
|
|
45
|
+
self._engine = engine
|
|
46
|
+
|
|
47
|
+
def __contains__(self, key: object) -> bool:
|
|
48
|
+
return isinstance(key, str) and self._engine.store.handled_has(key)
|
|
49
|
+
|
|
50
|
+
def add(self, key: str) -> None:
|
|
51
|
+
self._engine.store.handled_add(key, HANDLED_KEEP)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class Runtime:
|
|
55
|
+
"""One runtime. ``async with Runtime(config) as rt:`` or
|
|
56
|
+
:meth:`start` and :meth:`stop`.
|
|
57
|
+
|
|
58
|
+
Construction takes the data-directory lock (``DataDirLockedError`` if
|
|
59
|
+
another runtime holds it) and opens the keys and both databases;
|
|
60
|
+
:meth:`start` finishes the journal, starts the workers and the relay
|
|
61
|
+
link, then serves the local API.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__( # noqa: PLR0913
|
|
65
|
+
self,
|
|
66
|
+
config: Config,
|
|
67
|
+
*,
|
|
68
|
+
adapters: Iterable[Adapter] = (),
|
|
69
|
+
clock: Clock | None = None,
|
|
70
|
+
keystore: Keystore | None = None,
|
|
71
|
+
options: Options | None = None,
|
|
72
|
+
gate: PeerGate | None = None,
|
|
73
|
+
serve_api: bool = True,
|
|
74
|
+
) -> None:
|
|
75
|
+
self.config = config
|
|
76
|
+
self.adapters = AdapterRegistry(adapters)
|
|
77
|
+
# Separate-OS-user mode: refuse a loose setup before any key is opened.
|
|
78
|
+
gate = gate if gate is not None else PeerGate.from_config(config)
|
|
79
|
+
self._engine = Engine(
|
|
80
|
+
config, self.adapters, clock=clock, keystore=keystore, options=options
|
|
81
|
+
)
|
|
82
|
+
try:
|
|
83
|
+
self.agent = AgentApi(self._engine)
|
|
84
|
+
self._api = ApiServer(self._engine, gate, self.agent) if serve_api else None
|
|
85
|
+
except BaseException:
|
|
86
|
+
self._engine.close()
|
|
87
|
+
raise
|
|
88
|
+
self.handled = HandledIds(self._engine)
|
|
89
|
+
self._started = False
|
|
90
|
+
self._stopped = False
|
|
91
|
+
|
|
92
|
+
@property
|
|
93
|
+
def running(self) -> bool:
|
|
94
|
+
return self._engine.running
|
|
95
|
+
|
|
96
|
+
@property
|
|
97
|
+
def api_address(self) -> Path | str | None:
|
|
98
|
+
"""The local API's socket path (or ``http://127.0.0.1:<port>``)
|
|
99
|
+
once started; ``None`` before, or when the API isn't served."""
|
|
100
|
+
return None if self._api is None else self._api.address
|
|
101
|
+
|
|
102
|
+
def status(self) -> dict[str, Any]:
|
|
103
|
+
"""The agent-scope status (what ``GET /v0/status`` answers the agent
|
|
104
|
+
token)."""
|
|
105
|
+
info = self._engine.status(human=False)
|
|
106
|
+
info["held_messages"] = len(self._engine.held_messages(direction="out"))
|
|
107
|
+
return info
|
|
108
|
+
|
|
109
|
+
async def start(self) -> None:
|
|
110
|
+
"""Journal recovery, workers, relay link, then the local API. If
|
|
111
|
+
this fails or is cancelled, everything is released again and the
|
|
112
|
+
error (or the cancellation) propagates."""
|
|
113
|
+
if self._started:
|
|
114
|
+
msg = "a runtime starts once"
|
|
115
|
+
raise RuntimeError(msg)
|
|
116
|
+
self._started = True
|
|
117
|
+
try:
|
|
118
|
+
await self._engine.start()
|
|
119
|
+
if self._api is not None:
|
|
120
|
+
await self._api.start()
|
|
121
|
+
except BaseException:
|
|
122
|
+
await self.stop(flush=0)
|
|
123
|
+
raise
|
|
124
|
+
|
|
125
|
+
async def stop(self, *, flush: float = STOP_FLUSH_SECS) -> None:
|
|
126
|
+
"""Give queued sends up to ``flush`` seconds to go out while the
|
|
127
|
+
relay is connected, then stop the API, the workers and the relay
|
|
128
|
+
link, close the databases and release the lock. What hasn't gone out
|
|
129
|
+
stays in the encrypted outbox for the next start; work in progress
|
|
130
|
+
is finished from the journal then (ADR-004). Idempotent. Every step
|
|
131
|
+
runs even if the caller is cancelled meanwhile; that cancellation
|
|
132
|
+
propagates once the lock is released."""
|
|
133
|
+
if self._stopped:
|
|
134
|
+
return
|
|
135
|
+
self._stopped = True
|
|
136
|
+
try:
|
|
137
|
+
if flush > 0 and self._engine.running:
|
|
138
|
+
await self._flush(flush)
|
|
139
|
+
finally:
|
|
140
|
+
try:
|
|
141
|
+
if self._api is not None:
|
|
142
|
+
await self._api.stop()
|
|
143
|
+
finally:
|
|
144
|
+
await self._engine.stop()
|
|
145
|
+
|
|
146
|
+
async def _flush(self, within: float) -> None:
|
|
147
|
+
engine = self._engine
|
|
148
|
+
with contextlib.suppress(TimeoutError):
|
|
149
|
+
await engine.wait_until(
|
|
150
|
+
lambda: not engine.store.outbox() or not engine.link.connected, within
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
async def __aenter__(self) -> Runtime:
|
|
154
|
+
await self.start()
|
|
155
|
+
return self
|
|
156
|
+
|
|
157
|
+
async def __aexit__(self, *_exc: object) -> None:
|
|
158
|
+
await self.stop()
|