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.
Files changed (64) hide show
  1. agentconnect/__init__.py +3 -0
  2. agentconnect/a2a_adapter/__init__.py +6 -0
  3. agentconnect/a2a_adapter/adapter.py +565 -0
  4. agentconnect/a2a_adapter/cards.py +149 -0
  5. agentconnect/a2a_adapter/convert.py +157 -0
  6. agentconnect/a2a_adapter/facade.py +560 -0
  7. agentconnect/a2a_adapter/seen.py +49 -0
  8. agentconnect/a2a_adapter/serve.py +67 -0
  9. agentconnect/adapter.py +319 -0
  10. agentconnect/adapters.py +52 -0
  11. agentconnect/apiclient.py +51 -0
  12. agentconnect/backoff.py +42 -0
  13. agentconnect/callback.py +314 -0
  14. agentconnect/cli.py +2324 -0
  15. agentconnect/cli_json.py +470 -0
  16. agentconnect/cli_support.py +444 -0
  17. agentconnect/clock.py +161 -0
  18. agentconnect/config.py +1213 -0
  19. agentconnect/daemon/__init__.py +41 -0
  20. agentconnect/deliveries.py +295 -0
  21. agentconnect/directory_client.py +255 -0
  22. agentconnect/failpoints.py +99 -0
  23. agentconnect/identity.py +111 -0
  24. agentconnect/isolation.py +345 -0
  25. agentconnect/keystore.py +279 -0
  26. agentconnect/limits.py +457 -0
  27. agentconnect/log.py +143 -0
  28. agentconnect/mcp/__init__.py +30 -0
  29. agentconnect/mcp/backend.py +405 -0
  30. agentconnect/mcp/server.py +382 -0
  31. agentconnect/mcp/session.py +373 -0
  32. agentconnect/mcp/tools.py +583 -0
  33. agentconnect/ownerfile.py +238 -0
  34. agentconnect/payloads.py +109 -0
  35. agentconnect/policy.py +1066 -0
  36. agentconnect/provision.py +239 -0
  37. agentconnect/py.typed +0 -0
  38. agentconnect/relay.py +405 -0
  39. agentconnect/runtime/__init__.py +46 -0
  40. agentconnect/runtime/agent.py +225 -0
  41. agentconnect/runtime/api.py +2043 -0
  42. agentconnect/runtime/engine.py +5359 -0
  43. agentconnect/runtime/errors.py +47 -0
  44. agentconnect/runtime/floor.py +649 -0
  45. agentconnect/runtime/groups.py +1351 -0
  46. agentconnect/runtime/knocks.py +1422 -0
  47. agentconnect/runtime/ledger.py +94 -0
  48. agentconnect/runtime/listing.py +1307 -0
  49. agentconnect/runtime/registry.py +53 -0
  50. agentconnect/runtime/runtime.py +158 -0
  51. agentconnect/runtime/sas.py +1002 -0
  52. agentconnect/sas_emoji.py +83 -0
  53. agentconnect/schemas.py +381 -0
  54. agentconnect/shapes.py +56 -0
  55. agentconnect/sockets.py +86 -0
  56. agentconnect/sse.py +234 -0
  57. agentconnect/store.py +3282 -0
  58. agentconnect/tasks.py +35 -0
  59. agentconnect/tokens.py +63 -0
  60. agentconnect/webhook.py +199 -0
  61. agentconnect_cli-0.1.0a1.dist-info/METADATA +103 -0
  62. agentconnect_cli-0.1.0a1.dist-info/RECORD +64 -0
  63. agentconnect_cli-0.1.0a1.dist-info/WHEEL +4 -0
  64. 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()