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,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}
|