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,149 @@
1
+ """Agent Cards: signature checks and the cards the facade serves.
2
+
3
+ Signatures (A2A v1.0 ``AgentCard.signatures``, JWS over the RFC 8785 form of
4
+ the card) are checked against keys the user pinned in a JWKS file
5
+ (``a2a.card_keys_file``). A key is never fetched from a URL a card names
6
+ (``jku``): for a peer's card that URL is peer-controlled.
7
+
8
+ * ``verified``: a signature checks out with a pinned key.
9
+ * ``unsigned``: the card has no signature.
10
+ * ``unverified``: signed, but with no pinned key for it.
11
+ * ``invalid``: signed with a pinned key, and no signature checks out. An
12
+ invalid card is not used.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ from collections.abc import Mapping
19
+ from pathlib import Path
20
+
21
+ from a2a.types import (
22
+ AgentCapabilities,
23
+ AgentCard,
24
+ AgentInterface,
25
+ AgentSkill,
26
+ HTTPAuthSecurityScheme,
27
+ SecurityRequirement,
28
+ SecurityScheme,
29
+ StringList,
30
+ )
31
+ from a2a.utils.signing import SignatureVerificationError, create_signature_verifier
32
+ from jwt import PyJWK
33
+ from jwt.exceptions import PyJWKError, PyJWTError
34
+ from jwt.utils import base64url_decode
35
+
36
+ # Asymmetric only: a symmetric (HS*) key in a JWKS would let anyone holding
37
+ # it forge cards, and accepting HS* invites algorithm confusion.
38
+ ALGORITHMS = ["ES256", "ES384", "ES512", "RS256", "RS384", "RS512", "PS256", "PS384", "EdDSA"]
39
+ BEARER = "agentconnectBearer"
40
+
41
+
42
+ def load_jwks(path: Path | None) -> dict[str, PyJWK]:
43
+ """The pinned card-signing keys, by ``kid``."""
44
+ if path is None:
45
+ return {}
46
+ data = json.loads(path.read_text(encoding="utf-8"))
47
+ keys: dict[str, PyJWK] = {}
48
+ for jwk in data.get("keys", []) if isinstance(data, dict) else []:
49
+ if not isinstance(jwk, dict) or not isinstance(jwk.get("kid"), str):
50
+ continue
51
+ if jwk.get("kty") == "oct":
52
+ continue # symmetric keys are refused, see ALGORITHMS
53
+ try:
54
+ keys[jwk["kid"]] = PyJWK(jwk)
55
+ except PyJWTError:
56
+ continue
57
+ return keys
58
+
59
+
60
+ def _kids(card: AgentCard) -> list[str | None]:
61
+ kids: list[str | None] = []
62
+ for sig in card.signatures:
63
+ try:
64
+ header = json.loads(base64url_decode(sig.protected.encode("ascii")))
65
+ except (ValueError, UnicodeError):
66
+ kids.append(None)
67
+ continue
68
+ kid = header.get("kid") if isinstance(header, dict) else None
69
+ kids.append(kid if isinstance(kid, str) else None)
70
+ return kids
71
+
72
+
73
+ def signature_status(card: AgentCard, keys: Mapping[str, PyJWK]) -> str:
74
+ """``verified``, ``unsigned``, ``unverified`` or ``invalid`` (above)."""
75
+ if len(card.signatures) == 0:
76
+ return "unsigned"
77
+ if not any(kid in keys for kid in _kids(card) if kid is not None):
78
+ return "unverified"
79
+
80
+ def key_for(kid: str | None, _jku: str | None) -> PyJWK:
81
+ if kid is None or kid not in keys:
82
+ msg = "no pinned key"
83
+ raise PyJWKError(msg)
84
+ return keys[kid]
85
+
86
+ try:
87
+ create_signature_verifier(key_for, ALGORITHMS)(card)
88
+ except SignatureVerificationError:
89
+ return "invalid"
90
+ return "verified"
91
+
92
+
93
+ def _bearer(card: AgentCard) -> None:
94
+ card.security_schemes[BEARER].CopyFrom(
95
+ SecurityScheme(
96
+ http_auth_security_scheme=HTTPAuthSecurityScheme(
97
+ scheme="Bearer", description="The facade token (a2a-facade-token)."
98
+ )
99
+ )
100
+ )
101
+ card.security_requirements.append(SecurityRequirement(schemes={BEARER: StringList(list=[])}))
102
+
103
+
104
+ def facade_card(
105
+ url: str,
106
+ name: str,
107
+ description: str,
108
+ skills: list[AgentSkill],
109
+ source: AgentCard | None = None,
110
+ ) -> AgentCard:
111
+ """A card for the local facade at ``url``: ``source`` (a peer's card)
112
+ mirrored with the facade's interface and auth, or a composite one."""
113
+ card = AgentCard()
114
+ if source is not None:
115
+ card.CopyFrom(source)
116
+ # The peer's interfaces, auth and signatures don't apply here.
117
+ card.ClearField("supported_interfaces")
118
+ card.ClearField("security_schemes")
119
+ card.ClearField("security_requirements")
120
+ card.ClearField("signatures")
121
+ else:
122
+ card.name = name
123
+ card.description = description
124
+ card.version = "0"
125
+ card.default_input_modes.extend(["text/plain", "application/json"])
126
+ card.default_output_modes.extend(["text/plain", "application/json"])
127
+ card.skills.extend(skills)
128
+ card.supported_interfaces.append(
129
+ AgentInterface(url=url, protocol_binding="JSONRPC", protocol_version="1.0")
130
+ )
131
+ card.capabilities.CopyFrom(AgentCapabilities(streaming=True, push_notifications=True))
132
+ _bearer(card)
133
+ return card
134
+
135
+
136
+ def composite_skills(cards: Mapping[str, AgentCard], names: Mapping[str, str]) -> list[AgentSkill]:
137
+ """Every member's skills, ids prefixed by the member so they stay unique
138
+ and say whom to address (``metadata.to``)."""
139
+ out: list[AgentSkill] = []
140
+ for identity in sorted(cards):
141
+ for skill in cards[identity].skills:
142
+ s = AgentSkill()
143
+ s.CopyFrom(skill)
144
+ s.id = f"{identity}/{skill.id}"
145
+ label = names.get(identity) or identity
146
+ s.name = f"{label}: {skill.name}"
147
+ s.tags.append(f"member:{identity}")
148
+ out.append(s)
149
+ return out
@@ -0,0 +1,157 @@
1
+ """Conversions between inner-message bodies (A2A v1.0 JSON, SPEC §7.2) and
2
+ the ``a2a-sdk`` protobuf types.
3
+
4
+ Bodies are peer-controlled. Parsing ignores unknown fields and never raises:
5
+ a body that isn't a valid A2A ``Message`` becomes a message carrying its
6
+ ``text`` (or the whole body as a ``data`` part), so a peer can't crash the
7
+ adapter with a malformed body.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any
13
+
14
+ from a2a.types import (
15
+ AgentCard,
16
+ CancelTaskRequest,
17
+ Message,
18
+ Part,
19
+ Role,
20
+ StreamResponse,
21
+ TaskArtifactUpdateEvent,
22
+ TaskState,
23
+ TaskStatusUpdateEvent,
24
+ )
25
+ from google.protobuf.json_format import MessageToDict, ParseDict, ParseError
26
+ from google.protobuf.message import Message as ProtoMessage
27
+
28
+ from agentconnect.limits import part_kind
29
+
30
+ TERMINAL = frozenset(
31
+ {
32
+ TaskState.TASK_STATE_COMPLETED,
33
+ TaskState.TASK_STATE_FAILED,
34
+ TaskState.TASK_STATE_CANCELED,
35
+ TaskState.TASK_STATE_REJECTED,
36
+ }
37
+ )
38
+ INTERRUPTED = frozenset({TaskState.TASK_STATE_INPUT_REQUIRED, TaskState.TASK_STATE_AUTH_REQUIRED})
39
+
40
+
41
+ def to_json(msg: ProtoMessage) -> dict[str, Any]:
42
+ """The A2A v1.0 JSON form (camelCase) of a protobuf message."""
43
+ return MessageToDict(msg)
44
+
45
+
46
+ def _parse[T: ProtoMessage](body: Any, into: T) -> T | None:
47
+ if not isinstance(body, dict):
48
+ return None
49
+ try:
50
+ ParseDict(body, into, ignore_unknown_fields=True)
51
+ except (ParseError, TypeError, ValueError, RecursionError):
52
+ return None
53
+ return into
54
+
55
+
56
+ def parse_message(body: dict[str, Any]) -> Message:
57
+ """A peer's ``a2a.message`` body as an A2A ``Message`` (never raises).
58
+ Malformed parts are left out (agentconnect.limits.part_kind): the daemon
59
+ has dropped any message carrying one, and this keeps it so here too."""
60
+ parts = body.get("parts")
61
+ if isinstance(parts, list):
62
+ msg = _parse({**body, "parts": [p for p in parts if part_kind(p) != "invalid"]}, Message())
63
+ return Message() if msg is None else msg
64
+ msg = _parse(body, Message())
65
+ if msg is not None and len(msg.parts) > 0:
66
+ return msg
67
+ out = Message() if msg is None else msg
68
+ text = body.get("text")
69
+ if isinstance(text, str):
70
+ out.parts.append(Part(text=text))
71
+ return out
72
+ fallback = _parse({"parts": [{"data": body}]}, Message())
73
+ if fallback is not None:
74
+ out.parts.extend(fallback.parts)
75
+ return out
76
+
77
+
78
+ def parse_status(body: dict[str, Any]) -> TaskStatusUpdateEvent | None:
79
+ return _parse(body, TaskStatusUpdateEvent())
80
+
81
+
82
+ def parse_artifact(body: dict[str, Any]) -> TaskArtifactUpdateEvent | None:
83
+ return _parse(body, TaskArtifactUpdateEvent())
84
+
85
+
86
+ def parse_card(body: dict[str, Any]) -> AgentCard | None:
87
+ return _parse(body, AgentCard())
88
+
89
+
90
+ def stream_to_inner(resp: StreamResponse, group_id: str) -> list[tuple[str, dict[str, Any]]]:
91
+ """What one event from the local agent becomes in the group: a list of
92
+ ``(kind, body)``. ``contextId`` is always the group (SPEC §7.2)."""
93
+ out: list[tuple[str, dict[str, Any]]] = []
94
+ payload = resp.WhichOneof("payload")
95
+ if payload == "task":
96
+ task = resp.task
97
+ status = TaskStatusUpdateEvent(task_id=task.id, context_id=group_id)
98
+ status.status.CopyFrom(task.status)
99
+ out.append(("a2a.task_status", to_json(status)))
100
+ for artifact in task.artifacts:
101
+ event = TaskArtifactUpdateEvent(task_id=task.id, context_id=group_id)
102
+ event.artifact.CopyFrom(artifact)
103
+ out.append(("a2a.task_artifact", to_json(event)))
104
+ elif payload == "status_update":
105
+ event = TaskStatusUpdateEvent()
106
+ event.CopyFrom(resp.status_update)
107
+ event.context_id = group_id
108
+ out.append(("a2a.task_status", to_json(event)))
109
+ elif payload == "artifact_update":
110
+ event = TaskArtifactUpdateEvent()
111
+ event.CopyFrom(resp.artifact_update)
112
+ event.context_id = group_id
113
+ out.append(("a2a.task_artifact", to_json(event)))
114
+ elif payload == "message":
115
+ msg = Message()
116
+ msg.CopyFrom(resp.message)
117
+ msg.context_id = group_id
118
+ out.append(("a2a.message", to_json(msg)))
119
+ return out
120
+
121
+
122
+ def task_id_of(resp: StreamResponse) -> str:
123
+ payload = resp.WhichOneof("payload")
124
+ if payload == "task":
125
+ return resp.task.id
126
+ if payload == "status_update":
127
+ return resp.status_update.task_id
128
+ if payload == "artifact_update":
129
+ return resp.artifact_update.task_id
130
+ return ""
131
+
132
+
133
+ def is_final(resp: StreamResponse) -> bool:
134
+ """Whether the stream has nothing more for this call."""
135
+ payload = resp.WhichOneof("payload")
136
+ if payload == "message":
137
+ return True
138
+ if payload == "task":
139
+ return resp.task.status.state in TERMINAL | INTERRUPTED
140
+ if payload == "status_update":
141
+ return resp.status_update.status.state in TERMINAL | INTERRUPTED
142
+ return False
143
+
144
+
145
+ def cancel_body(task_id: str) -> dict[str, Any]:
146
+ """The ``a2a.task_cancel`` body (an A2A ``CancelTaskRequest``, SPEC 7.2)
147
+ a requester sends to cancel a bridged task."""
148
+ return to_json(CancelTaskRequest(id=task_id))
149
+
150
+
151
+ def parse_cancel(body: dict[str, Any]) -> CancelTaskRequest | None:
152
+ request = _parse(body, CancelTaskRequest())
153
+ return request if request is not None and request.id else None
154
+
155
+
156
+ def agent_text(text: str, group_id: str) -> Message:
157
+ return Message(role=Role.ROLE_AGENT, parts=[Part(text=text)], context_id=group_id)