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,225 @@
1
+ """The agent-scope operations of a runtime (ADR-005, Decision 1).
2
+
3
+ :class:`AgentApi` is everything an agent may do: groups, invites (create,
4
+ inspect, accept, revoke), send, leave, granting the floor of a ``moderated``
5
+ group it moderates, events, its own held sends, reading the safety policy
6
+ and raising ``require_verified``. The local API's routes
7
+ call it for the agent token, and the embedded SDK calls it in-process, so
8
+ the two paths can't drift apart. Every answer has the shape of the matching
9
+ route's JSON (``docs/api.md``).
10
+
11
+ There is no human-scope operation here: approving or rejecting joins,
12
+ knocks and held messages, listing or reading knock approvals, marking
13
+ members verified, reporting a SAS mismatch, removing members, changing a
14
+ member's role, handing ownership over, any group policy change other than
15
+ raising ``require_verified`` (at creation too), a replica invite,
16
+ changing the safety policy and publishing a listing exist only as local
17
+ API routes that need the human token (SPEC 6.2, 6.6.4, 9.2, 10.4, 11,
18
+ 13.2.2). Exporting and importing the identity key (provisioning a
19
+ replica) is not an API operation at all: only the CLI does it, on a
20
+ stopped runtime's data directory (ADR-005). Withdrawing the
21
+ listing is here: it only reduces exposure (SPEC
22
+ 13.2.3). So is knocking on another agent's listing (SPEC 6.6.2), unless
23
+ ``knock.agent_may_knock`` is off; approving a knock never is.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ from collections.abc import Sequence
29
+ from typing import Any
30
+
31
+ from agentconnect.policy import HUMAN_ONLY_KINDS
32
+ from agentconnect.runtime.engine import DaemonError, Engine, new_msg_id
33
+ from agentconnect.runtime.errors import ApiError
34
+
35
+
36
+ def a2a_text(group_id: str, text: str, human: bool) -> dict[str, Any]:
37
+ """The ``text`` shorthand as an A2A v1.0 ``Message`` (SPEC 7.2)."""
38
+ return {
39
+ "messageId": new_msg_id(),
40
+ "contextId": group_id,
41
+ "role": "ROLE_USER" if human else "ROLE_AGENT",
42
+ "parts": [{"text": text}],
43
+ }
44
+
45
+
46
+ class AgentApi:
47
+ """The agent-scope operation set over one engine."""
48
+
49
+ def __init__(self, engine: Engine) -> None:
50
+ self._engine = engine
51
+
52
+ def _visible(self, group_id: str) -> str:
53
+ """An approved knock's group is the agent's once the knocker is in
54
+ (docs/discovery.md): until then it doesn't exist for the agent."""
55
+ if self._engine.hidden_from_agent(group_id):
56
+ raise DaemonError("not_found", "no such active group")
57
+ return group_id
58
+
59
+ async def status(self) -> dict[str, Any]:
60
+ """The runtime's status. SPEC 10.2: the agent can't learn (and tell
61
+ a peer) what is held of the peers' messages; ``held_messages``
62
+ counts its own held sends only."""
63
+ info = self._engine.status(human=False)
64
+ info["held_messages"] = len(self._engine.held_messages(direction="out"))
65
+ return info
66
+
67
+ async def list_groups(self) -> dict[str, Any]:
68
+ groups = self._engine.list_groups()
69
+ return {"groups": [g for g in groups if not self._engine.hidden_from_agent(g["group_id"])]}
70
+
71
+ async def create_group(
72
+ self, name: str, purpose: str = "", *, require_verified: bool = False
73
+ ) -> dict[str, Any]:
74
+ """A group this identity owns, with the default policy; the agent may
75
+ only ask for ``require_verified``, which tightens the gate (any other
76
+ policy at creation is the human's: ``POST /v0/groups``)."""
77
+ policy = {"require_verified": True} if require_verified else None
78
+ return {"group_id": self._engine.create_group(name, purpose, policy)}
79
+
80
+ async def group_info(self, group_id: str) -> dict[str, Any]:
81
+ return self._engine.group_info(self._visible(group_id))
82
+
83
+ async def create_invite(
84
+ self,
85
+ group_id: str,
86
+ *,
87
+ ttl_secs: int = 86_400,
88
+ max_uses: int = 1,
89
+ inviter_name: str | None = None,
90
+ ) -> dict[str, Any]:
91
+ return await self._engine.create_invite(
92
+ self._visible(group_id), ttl_secs, max_uses, inviter_name
93
+ )
94
+
95
+ async def revoke_invite(self, invite_id: str) -> dict[str, Any]:
96
+ await self._engine.revoke_invite(invite_id)
97
+ return {"revoked": invite_id}
98
+
99
+ async def inspect_invite(self, url: str) -> dict[str, Any]:
100
+ return await self._engine.inspect_invite(url)
101
+
102
+ async def accept_invite(self, url: str, display_name: str | None = None) -> dict[str, Any]:
103
+ return await self._engine.accept_invite(url, display_name)
104
+
105
+ async def set_policy(self, group_id: str, *, require_verified: bool) -> dict[str, Any]:
106
+ """Only ``require_verified``, and only on: turning it on tightens
107
+ the gate; turning it off needs the human (SPEC 10.4), and so does
108
+ every other policy change. ``status`` is ``committing`` or
109
+ ``unchanged``; a change this identity may not make is refused with
110
+ ``403 not_permitted``."""
111
+ if not require_verified:
112
+ raise ApiError(403, "human_only", "relaxing verification needs the human token")
113
+ return {"status": self._engine.set_policy(self._visible(group_id), require_verified=True)}
114
+
115
+ async def grant_floor(self, group_id: str, holder: str | None) -> dict[str, Any]:
116
+ """Grant the floor of a ``moderated`` group to a member, or release
117
+ it (``None``), as the agent (SPEC 9.3). Only the effective moderator
118
+ or the owner may (``403 not_permitted``): the human chose the
119
+ moderator by commit, and a grant triggers nobody by itself. The floor
120
+ mode and the moderator are the human's (``POST .../policy``)."""
121
+ return self._engine.grant_floor(self._visible(group_id), holder, origin="agent")
122
+
123
+ async def send( # noqa: PLR0913
124
+ self,
125
+ group_id: str,
126
+ body: dict[str, Any] | None = None,
127
+ *,
128
+ text: str | None = None,
129
+ to: Sequence[str] = ("*",),
130
+ kind: str = "a2a.message",
131
+ reply_to: Sequence[str] | None = None,
132
+ addressee: Sequence[str] | None = None,
133
+ instance: str | None = None,
134
+ ) -> dict[str, Any]:
135
+ """Send as the agent (SPEC 7.2/10.3: only the human's own requests
136
+ are human-originated). The controls SPEC 7.3 reserves to humans
137
+ (``control.resume``) are refused. ``instance`` (16 lowercase hex)
138
+ narrows triggering to that instance (SPEC 9.4)."""
139
+ if kind in HUMAN_ONLY_KINDS:
140
+ raise ApiError(403, "human_only", f"{kind} needs the human token")
141
+ self._visible(group_id)
142
+ if body is None:
143
+ if text is None:
144
+ raise ApiError(400, "bad_request", "give text or body")
145
+ body = a2a_text(group_id, text, human=False)
146
+ msg_id, status = self._engine.queue_message(
147
+ group_id,
148
+ body,
149
+ to=list(to),
150
+ kind=kind,
151
+ origin="agent",
152
+ reply_to=None if reply_to is None else list(reply_to),
153
+ addressee=None if addressee is None else list(addressee),
154
+ instance=instance,
155
+ )
156
+ return {"msg_id": msg_id, "status": status}
157
+
158
+ async def leave(self, group_id: str) -> dict[str, Any]:
159
+ return {"status": await self._engine.leave(self._visible(group_id))}
160
+
161
+ async def approvals(self) -> dict[str, Any]:
162
+ """No join requests and no held peer messages (their sender, msg_id
163
+ and task would tell the agent what the policy did); only the
164
+ agent's own held sends."""
165
+ await self._engine.expire_held()
166
+ return {"approvals": [], "messages": self._engine.held_messages(direction="out")}
167
+
168
+ async def safety(self, group_id: str) -> dict[str, Any]:
169
+ """The group's safety settings (read only)."""
170
+ return self._engine.safety_settings(self._visible(group_id))
171
+
172
+ async def listing_status(self) -> dict[str, Any]:
173
+ """The runtime's own listing, if it has one (docs/discovery.md)."""
174
+ return self._engine.listings.status()
175
+
176
+ async def withdraw_listing(self) -> dict[str, Any]:
177
+ """Close the listing's doors at the relay, then withdraw it from its
178
+ directory. Allowed to the agent: it only reduces exposure (SPEC
179
+ 13.2.3). Publishing is the human's (no method here)."""
180
+ return await self._engine.listings.withdraw()
181
+
182
+ async def knock_inspect(
183
+ self, listing: str, *, domain_check: bool | None = None
184
+ ) -> dict[str, Any]:
185
+ """A listing, checked as a knock would check it, with its text marked
186
+ ``untrusted`` (docs/discovery.md). ``domain_check`` defaults to
187
+ ``knock.domain_check``."""
188
+ return await self._engine.knocks.inspect(listing, domain_check)
189
+
190
+ async def knock(
191
+ self,
192
+ listing: str,
193
+ *,
194
+ listing_sha256: str | None = None,
195
+ display_name: str | None = None,
196
+ note: str | None = None,
197
+ domain_check: bool | None = None,
198
+ ) -> dict[str, Any]:
199
+ """Knock on a listing: ask its owner's human for a new group (SPEC
200
+ 6.6.2). Only a Welcome answers it (``joined``); otherwise it lapses
201
+ as "no answer" (``knock_lapsed``). Refused with ``403 human_only``
202
+ when ``knock.agent_may_knock`` is off."""
203
+ return await self._engine.knocks.knock(
204
+ listing,
205
+ listing_sha256=listing_sha256,
206
+ display_name=display_name,
207
+ note=note,
208
+ domain_check=domain_check,
209
+ )
210
+
211
+ async def knocks(self) -> dict[str, Any]:
212
+ """This runtime's own knocks (not the ones it received). The listing
213
+ text in them is its owner's: ``untrusted``."""
214
+ return {"knocks": self._engine.knocks.knocks(), "untrusted": True}
215
+
216
+ async def events(self, after: int = 0, group_id: str | None = None) -> dict[str, Any]:
217
+ """Events in order, without the safety policy's own (SPEC 10.2): an
218
+ agent that heard of a hold could tell the sender. Nor the knocks this
219
+ runtime received: those are the human's. ``seq`` is the agent's own
220
+ sequence (no gap where an event it doesn't see was recorded), and
221
+ ``after`` takes it."""
222
+ store = self._engine.store
223
+ rows = store.agent_events(after, group_id=group_id)
224
+ _, purged = store.events_purged()
225
+ return {"events": rows, "gap": after < purged, "oldest_available": purged + 1}