agent-protocols 0.4.0__tar.gz → 0.4.1__tar.gz
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.
- {agent_protocols-0.4.0 → agent_protocols-0.4.1}/PKG-INFO +6 -5
- {agent_protocols-0.4.0 → agent_protocols-0.4.1}/README.md +4 -3
- {agent_protocols-0.4.0 → agent_protocols-0.4.1}/pyproject.toml +2 -2
- {agent_protocols-0.4.0 → agent_protocols-0.4.1}/src/agent_protocols/__init__.py +22 -0
- agent_protocols-0.4.1/src/agent_protocols/delegation.py +150 -0
- {agent_protocols-0.4.0 → agent_protocols-0.4.1}/src/agent_protocols/discourse.py +15 -1
- {agent_protocols-0.4.0 → agent_protocols-0.4.1}/src/agent_protocols/http_client.py +42 -0
- {agent_protocols-0.4.0 → agent_protocols-0.4.1}/src/agent_protocols/profile.py +1 -0
- {agent_protocols-0.4.0 → agent_protocols-0.4.1}/src/agent_protocols.egg-info/PKG-INFO +6 -5
- {agent_protocols-0.4.0 → agent_protocols-0.4.1}/src/agent_protocols.egg-info/SOURCES.txt +2 -0
- agent_protocols-0.4.1/tests/test_delegation.py +124 -0
- {agent_protocols-0.4.0 → agent_protocols-0.4.1}/tests/test_http_client.py +38 -0
- {agent_protocols-0.4.0 → agent_protocols-0.4.1}/tests/test_profile.py +9 -0
- {agent_protocols-0.4.0 → agent_protocols-0.4.1}/setup.cfg +0 -0
- {agent_protocols-0.4.0 → agent_protocols-0.4.1}/src/agent_protocols/errors.py +0 -0
- {agent_protocols-0.4.0 → agent_protocols-0.4.1}/src/agent_protocols/identity.py +0 -0
- {agent_protocols-0.4.0 → agent_protocols-0.4.1}/src/agent_protocols.egg-info/dependency_links.txt +0 -0
- {agent_protocols-0.4.0 → agent_protocols-0.4.1}/src/agent_protocols.egg-info/requires.txt +0 -0
- {agent_protocols-0.4.0 → agent_protocols-0.4.1}/src/agent_protocols.egg-info/top_level.txt +0 -0
- {agent_protocols-0.4.0 → agent_protocols-0.4.1}/tests/test_discourse.py +0 -0
- {agent_protocols-0.4.0 → agent_protocols-0.4.1}/tests/test_discourse_coverage.py +0 -0
- {agent_protocols-0.4.0 → agent_protocols-0.4.1}/tests/test_identity.py +0 -0
- {agent_protocols-0.4.0 → agent_protocols-0.4.1}/tests/test_identity_coverage.py +0 -0
- {agent_protocols-0.4.0 → agent_protocols-0.4.1}/tests/test_profile_coverage.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-protocols
|
|
3
|
-
Version: 0.4.
|
|
4
|
-
Summary: Python SDK for Agent Identity, Agent Profile, and Agent Discourse protocols
|
|
3
|
+
Version: 0.4.1
|
|
4
|
+
Summary: Python SDK for Agent Identity, Agent Profile, Agent Delegation, and Agent Discourse protocols
|
|
5
5
|
Author: LDCLabs
|
|
6
6
|
License: MIT
|
|
7
7
|
Keywords: agent,protocol,ed25519,sdk
|
|
@@ -15,14 +15,15 @@ Requires-Dist: requests<3,>=2.31; extra == "http"
|
|
|
15
15
|
|
|
16
16
|
# agent-protocols Python SDK
|
|
17
17
|
|
|
18
|
-
Python SDK for the draft Agent Identity, Agent Profile, and Agent Discourse protocols.
|
|
18
|
+
Python SDK for the draft Agent Identity, Agent Profile, Agent Delegation, and Agent Discourse protocols.
|
|
19
19
|
|
|
20
20
|
## Modules
|
|
21
21
|
|
|
22
22
|
- `agent_protocols.identity`: `did:agent:` encoding, JCS canonicalization, event hashes, Ed25519 signing and verification, live-write nonce checks, request JWT helpers.
|
|
23
|
-
- `agent_protocols.profile`: `profile.update` payload helpers, validation, materialization.
|
|
23
|
+
- `agent_protocols.profile`: `profile.update` payload helpers, delegation discovery hints, validation, materialization.
|
|
24
|
+
- `agent_protocols.delegation`: Agent Delegation principal documents, grant/revoke payloads, credential documents, validation, and materialization.
|
|
24
25
|
- `agent_protocols.discourse`: ADP kernel event constants, the room type system (type definitions, pack imports, type registry, JSON Schema payload validation), join request helpers, room-path checks, kind-based permission and state helpers.
|
|
25
|
-
- `agent_protocols.http_client`: optional requests-based Profile and Discourse clients. Install with `agent-protocols[http]`.
|
|
26
|
+
- `agent_protocols.http_client`: optional requests-based Profile, Delegation, and Discourse clients. Install with `agent-protocols[http]`.
|
|
26
27
|
|
|
27
28
|
## Example
|
|
28
29
|
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# agent-protocols Python SDK
|
|
2
2
|
|
|
3
|
-
Python SDK for the draft Agent Identity, Agent Profile, and Agent Discourse protocols.
|
|
3
|
+
Python SDK for the draft Agent Identity, Agent Profile, Agent Delegation, and Agent Discourse protocols.
|
|
4
4
|
|
|
5
5
|
## Modules
|
|
6
6
|
|
|
7
7
|
- `agent_protocols.identity`: `did:agent:` encoding, JCS canonicalization, event hashes, Ed25519 signing and verification, live-write nonce checks, request JWT helpers.
|
|
8
|
-
- `agent_protocols.profile`: `profile.update` payload helpers, validation, materialization.
|
|
8
|
+
- `agent_protocols.profile`: `profile.update` payload helpers, delegation discovery hints, validation, materialization.
|
|
9
|
+
- `agent_protocols.delegation`: Agent Delegation principal documents, grant/revoke payloads, credential documents, validation, and materialization.
|
|
9
10
|
- `agent_protocols.discourse`: ADP kernel event constants, the room type system (type definitions, pack imports, type registry, JSON Schema payload validation), join request helpers, room-path checks, kind-based permission and state helpers.
|
|
10
|
-
- `agent_protocols.http_client`: optional requests-based Profile and Discourse clients. Install with `agent-protocols[http]`.
|
|
11
|
+
- `agent_protocols.http_client`: optional requests-based Profile, Delegation, and Discourse clients. Install with `agent-protocols[http]`.
|
|
11
12
|
|
|
12
13
|
## Example
|
|
13
14
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "agent-protocols"
|
|
3
|
-
version = "0.4.
|
|
4
|
-
description = "Python SDK for Agent Identity, Agent Profile, and Agent Discourse protocols"
|
|
3
|
+
version = "0.4.1"
|
|
4
|
+
description = "Python SDK for Agent Identity, Agent Profile, Agent Delegation, and Agent Discourse protocols"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
7
7
|
license = { text = "MIT" }
|
|
@@ -32,9 +32,24 @@ from .identity import (
|
|
|
32
32
|
with_room_id,
|
|
33
33
|
)
|
|
34
34
|
from .profile import PROFILE_PROTOCOL, PROFILE_UPDATE, materialize_profile, profile_update_event, validate_profile_update
|
|
35
|
+
from .delegation import (
|
|
36
|
+
DELEGATION_GRANT,
|
|
37
|
+
DELEGATION_PROTOCOL,
|
|
38
|
+
DELEGATION_REVOKE,
|
|
39
|
+
delegation_grant_event,
|
|
40
|
+
delegation_revoke_event,
|
|
41
|
+
materialize_delegation_credential,
|
|
42
|
+
validate_delegation_envelope,
|
|
43
|
+
validate_delegation_grant_payload,
|
|
44
|
+
validate_delegation_revoke_payload,
|
|
45
|
+
validate_principal_document,
|
|
46
|
+
)
|
|
35
47
|
|
|
36
48
|
__all__ = [
|
|
37
49
|
"AGENT_ID_PREFIX",
|
|
50
|
+
"DELEGATION_GRANT",
|
|
51
|
+
"DELEGATION_PROTOCOL",
|
|
52
|
+
"DELEGATION_REVOKE",
|
|
38
53
|
"MAX_NONCE_HEADER",
|
|
39
54
|
"PROFILE_PROTOCOL",
|
|
40
55
|
"PROFILE_UPDATE",
|
|
@@ -46,10 +61,13 @@ __all__ = [
|
|
|
46
61
|
"agent_id_from_public_key",
|
|
47
62
|
"canonical_event_bytes",
|
|
48
63
|
"create_event",
|
|
64
|
+
"delegation_grant_event",
|
|
65
|
+
"delegation_revoke_event",
|
|
49
66
|
"create_request_jwt_claims",
|
|
50
67
|
"event_hash",
|
|
51
68
|
"event_hash_bytes",
|
|
52
69
|
"materialize_profile",
|
|
70
|
+
"materialize_delegation_credential",
|
|
53
71
|
"profile_update_event",
|
|
54
72
|
"public_key_bytes",
|
|
55
73
|
"sign_event",
|
|
@@ -57,7 +75,11 @@ __all__ = [
|
|
|
57
75
|
"unix_ms",
|
|
58
76
|
"unix_secs",
|
|
59
77
|
"validate_agent_id",
|
|
78
|
+
"validate_delegation_envelope",
|
|
79
|
+
"validate_delegation_grant_payload",
|
|
80
|
+
"validate_delegation_revoke_payload",
|
|
60
81
|
"validate_nonce",
|
|
82
|
+
"validate_principal_document",
|
|
61
83
|
"validate_profile_update",
|
|
62
84
|
"verify_envelope",
|
|
63
85
|
"verify_event_hash",
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
from urllib.parse import urlparse
|
|
5
|
+
|
|
6
|
+
from .errors import AgentProtocolError
|
|
7
|
+
from .identity import AgentId, Envelope, Event, create_event, validate_agent_id, verify_envelope
|
|
8
|
+
|
|
9
|
+
DELEGATION_PROTOCOL = "agent-delegation/1.0"
|
|
10
|
+
DELEGATION_GRANT = "delegation.grant"
|
|
11
|
+
DELEGATION_REVOKE = "delegation.revoke"
|
|
12
|
+
|
|
13
|
+
DelegationGrantPayload = dict[str, Any]
|
|
14
|
+
DelegationRevokePayload = dict[str, Any]
|
|
15
|
+
DelegationCredential = dict[str, Any]
|
|
16
|
+
PrincipalDocument = dict[str, Any]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def delegation_grant_event(
|
|
20
|
+
actor: AgentId,
|
|
21
|
+
created_at: int,
|
|
22
|
+
nonce: int,
|
|
23
|
+
payload: DelegationGrantPayload,
|
|
24
|
+
) -> Event:
|
|
25
|
+
return create_event(DELEGATION_PROTOCOL, DELEGATION_GRANT, actor, created_at, nonce, payload)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def delegation_revoke_event(
|
|
29
|
+
actor: AgentId,
|
|
30
|
+
created_at: int,
|
|
31
|
+
nonce: int,
|
|
32
|
+
payload: DelegationRevokePayload,
|
|
33
|
+
) -> Event:
|
|
34
|
+
return create_event(DELEGATION_PROTOCOL, DELEGATION_REVOKE, actor, created_at, nonce, payload)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def validate_principal_document(document: PrincipalDocument) -> None:
|
|
38
|
+
_validate_https_url(document.get("id"), "principal.id")
|
|
39
|
+
controllers = document.get("controllers")
|
|
40
|
+
if not isinstance(controllers, list) or not controllers:
|
|
41
|
+
raise AgentProtocolError(
|
|
42
|
+
"invalid_principal",
|
|
43
|
+
"principal document controllers must be non-empty",
|
|
44
|
+
)
|
|
45
|
+
for controller in controllers:
|
|
46
|
+
validate_agent_id(controller)
|
|
47
|
+
if document.get("delegations_url") is not None:
|
|
48
|
+
_validate_https_url(document["delegations_url"], "delegations_url")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def validate_delegation_grant_payload(
|
|
52
|
+
payload: DelegationGrantPayload,
|
|
53
|
+
created_at: int | None = None,
|
|
54
|
+
) -> None:
|
|
55
|
+
_validate_non_empty(payload.get("id"), "payload.id")
|
|
56
|
+
_validate_principal_descriptor(payload.get("principal"))
|
|
57
|
+
validate_agent_id(payload.get("subject"))
|
|
58
|
+
scopes = payload.get("scopes")
|
|
59
|
+
if not isinstance(scopes, list) or not scopes:
|
|
60
|
+
raise AgentProtocolError("invalid_delegation", "delegation scopes must be non-empty")
|
|
61
|
+
for scope in scopes:
|
|
62
|
+
_validate_non_empty(scope, "scope")
|
|
63
|
+
constraints = payload.get("constraints")
|
|
64
|
+
if constraints is not None and not isinstance(constraints, dict):
|
|
65
|
+
raise AgentProtocolError("invalid_delegation", "constraints must be an object")
|
|
66
|
+
expires_at = payload.get("expires_at")
|
|
67
|
+
if expires_at is not None:
|
|
68
|
+
not_before = payload.get("not_before", created_at)
|
|
69
|
+
if not_before is not None and expires_at <= not_before:
|
|
70
|
+
raise AgentProtocolError(
|
|
71
|
+
"invalid_delegation",
|
|
72
|
+
"expires_at must be greater than not_before or created_at",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def validate_delegation_revoke_payload(payload: DelegationRevokePayload) -> None:
|
|
77
|
+
_validate_non_empty(payload.get("id"), "payload.id")
|
|
78
|
+
_validate_https_url(payload.get("principal_id"), "principal_id")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def validate_delegation_envelope(envelope: Envelope) -> None:
|
|
82
|
+
verify_envelope(envelope)
|
|
83
|
+
event = envelope["event"]
|
|
84
|
+
if event["protocol"] != DELEGATION_PROTOCOL:
|
|
85
|
+
raise AgentProtocolError(
|
|
86
|
+
"invalid_event_protocol",
|
|
87
|
+
f"expected {DELEGATION_PROTOCOL}, got {event['protocol']}",
|
|
88
|
+
)
|
|
89
|
+
if event["type"] == DELEGATION_GRANT:
|
|
90
|
+
validate_delegation_grant_payload(event["payload"], event["created_at"])
|
|
91
|
+
elif event["type"] == DELEGATION_REVOKE:
|
|
92
|
+
validate_delegation_revoke_payload(event["payload"])
|
|
93
|
+
else:
|
|
94
|
+
raise AgentProtocolError(
|
|
95
|
+
"invalid_event_type",
|
|
96
|
+
f"expected {DELEGATION_GRANT} or {DELEGATION_REVOKE}, got {event['type']}",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def materialize_delegation_credential(
|
|
101
|
+
envelope: Envelope,
|
|
102
|
+
*,
|
|
103
|
+
status: str = "active",
|
|
104
|
+
status_url: str | None = None,
|
|
105
|
+
updated_at: int | None = None,
|
|
106
|
+
) -> DelegationCredential:
|
|
107
|
+
validate_delegation_envelope(envelope)
|
|
108
|
+
event = envelope["event"]
|
|
109
|
+
if event["type"] != DELEGATION_GRANT:
|
|
110
|
+
raise AgentProtocolError(
|
|
111
|
+
"invalid_event_type",
|
|
112
|
+
"delegation credential materialization requires a grant event",
|
|
113
|
+
)
|
|
114
|
+
payload = event["payload"]
|
|
115
|
+
credential = {
|
|
116
|
+
"id": payload["id"],
|
|
117
|
+
"protocol": DELEGATION_PROTOCOL,
|
|
118
|
+
"principal": payload["principal"],
|
|
119
|
+
"controller": event["actor"],
|
|
120
|
+
"subject": payload["subject"],
|
|
121
|
+
"relationship": payload.get("relationship"),
|
|
122
|
+
"scopes": payload["scopes"],
|
|
123
|
+
"constraints": payload.get("constraints"),
|
|
124
|
+
"not_before": payload.get("not_before"),
|
|
125
|
+
"expires_at": payload.get("expires_at"),
|
|
126
|
+
"status": status,
|
|
127
|
+
"status_url": status_url,
|
|
128
|
+
"updated_at": updated_at if updated_at is not None else event["created_at"],
|
|
129
|
+
"event_id": envelope["hash"],
|
|
130
|
+
}
|
|
131
|
+
return credential
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _validate_principal_descriptor(value: Any) -> None:
|
|
135
|
+
if not isinstance(value, dict):
|
|
136
|
+
raise AgentProtocolError("invalid_principal", "principal must be an object")
|
|
137
|
+
_validate_https_url(value.get("id"), "principal.id")
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _validate_https_url(value: Any, field: str) -> None:
|
|
141
|
+
if not isinstance(value, str):
|
|
142
|
+
raise AgentProtocolError("invalid_url", f"{field} must be an HTTPS URL")
|
|
143
|
+
parsed = urlparse(value)
|
|
144
|
+
if parsed.scheme != "https" or not parsed.netloc:
|
|
145
|
+
raise AgentProtocolError("invalid_url", f"{field} must be an HTTPS URL")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _validate_non_empty(value: Any, field: str) -> None:
|
|
149
|
+
if not isinstance(value, str) or not value.strip():
|
|
150
|
+
raise AgentProtocolError("invalid_delegation", f"{field} must not be empty")
|
|
@@ -94,6 +94,7 @@ class PermissionContext(TypedDict, total=False):
|
|
|
94
94
|
|
|
95
95
|
role: Role
|
|
96
96
|
is_creator: bool
|
|
97
|
+
public_join_allowed: bool
|
|
97
98
|
join_request_approved: bool
|
|
98
99
|
|
|
99
100
|
|
|
@@ -438,6 +439,19 @@ def validate_message_create_payload(payload: dict[str, Any]) -> None:
|
|
|
438
439
|
raise AgentProtocolError("invalid_event", "content_type must not be empty")
|
|
439
440
|
|
|
440
441
|
|
|
442
|
+
def validate_room_join_payload(payload: dict[str, Any]) -> None:
|
|
443
|
+
if payload.get("role") not in ROLES:
|
|
444
|
+
raise AgentProtocolError("invalid_event", f"invalid room role: {payload.get('role')}")
|
|
445
|
+
request_id = payload.get("request_id")
|
|
446
|
+
if request_id is not None and not str(request_id).strip():
|
|
447
|
+
raise AgentProtocolError("invalid_event", "request_id must not be empty")
|
|
448
|
+
if request_id is not None and "perspective" in payload:
|
|
449
|
+
raise AgentProtocolError(
|
|
450
|
+
"invalid_event",
|
|
451
|
+
"room.join payload cannot include both request_id and perspective",
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
|
|
441
455
|
def server_record_hash_payload(
|
|
442
456
|
room_id: str,
|
|
443
457
|
seq: int,
|
|
@@ -537,7 +551,7 @@ def can_submit_event(
|
|
|
537
551
|
if event_type == ROOM_CREATE:
|
|
538
552
|
return True
|
|
539
553
|
if event_type == ROOM_JOIN:
|
|
540
|
-
return bool(context.get("join_request_approved"))
|
|
554
|
+
return bool(context.get("public_join_allowed") or context.get("join_request_approved"))
|
|
541
555
|
if event_type == ROOM_LEAVE:
|
|
542
556
|
return is_creator or role is not None
|
|
543
557
|
if event_type in {ROOM_JOIN_REVIEW, ROOM_MEMBER_ROLE_UPDATE, ROOM_CLOSE, ROOM_CANCEL, TYPE_DEFINE}:
|
|
@@ -163,6 +163,48 @@ class DiscourseClient:
|
|
|
163
163
|
return response.json()
|
|
164
164
|
|
|
165
165
|
|
|
166
|
+
class DelegationClient:
|
|
167
|
+
def __init__(self, base_url: str, session: Any | None = None):
|
|
168
|
+
self.base_url = base_url.rstrip("/")
|
|
169
|
+
self.session = session or _requests_session()
|
|
170
|
+
|
|
171
|
+
def protocol(self) -> dict[str, Any]:
|
|
172
|
+
return self._get("/.well-known/agent-delegation")
|
|
173
|
+
|
|
174
|
+
def principal(self, principal_url: str | None = None) -> dict[str, Any]:
|
|
175
|
+
response = self.session.get(
|
|
176
|
+
principal_url or self.base_url,
|
|
177
|
+
headers={"Accept": "application/json"},
|
|
178
|
+
)
|
|
179
|
+
response.raise_for_status()
|
|
180
|
+
return response.json()
|
|
181
|
+
|
|
182
|
+
def delegation(self, delegation_id: str) -> dict[str, Any]:
|
|
183
|
+
return self._get(f"/v1/delegations/{delegation_id}")
|
|
184
|
+
|
|
185
|
+
def delegation_status(self, delegation_id: str) -> dict[str, Any]:
|
|
186
|
+
return self._get(f"/v1/delegations/{delegation_id}/status")
|
|
187
|
+
|
|
188
|
+
def delegation_events(self, delegation_id: str) -> dict[str, Any]:
|
|
189
|
+
return self._get(f"/v1/delegations/{delegation_id}/events")
|
|
190
|
+
|
|
191
|
+
def submit_delegation_event(self, envelope: Envelope) -> dict[str, Any]:
|
|
192
|
+
return self._post("/v1/delegations", envelope)
|
|
193
|
+
|
|
194
|
+
def query_delegations(self, request: dict[str, Any]) -> dict[str, Any]:
|
|
195
|
+
return self._post("/v1/delegations/query", request)
|
|
196
|
+
|
|
197
|
+
def _get(self, path: str) -> Any:
|
|
198
|
+
response = self.session.get(self.base_url + path)
|
|
199
|
+
response.raise_for_status()
|
|
200
|
+
return response.json()
|
|
201
|
+
|
|
202
|
+
def _post(self, path: str, body: Any) -> dict[str, Any]:
|
|
203
|
+
response = self.session.post(self.base_url + path, json=body)
|
|
204
|
+
response.raise_for_status()
|
|
205
|
+
return response.json()
|
|
206
|
+
|
|
207
|
+
|
|
166
208
|
def sse_events_url(base_url: str, room_id: str) -> str:
|
|
167
209
|
return f"{base_url.rstrip('/')}/v1/rooms/{quote(room_id, safe='')}/events/live"
|
|
168
210
|
|
|
@@ -42,6 +42,7 @@ def materialize_profile(envelope: Envelope) -> AgentProfile:
|
|
|
42
42
|
"capabilities": payload.get("capabilities", []),
|
|
43
43
|
"service_endpoints": payload.get("service_endpoints", []),
|
|
44
44
|
"links": payload.get("links", []),
|
|
45
|
+
"delegations": payload.get("delegations", []),
|
|
45
46
|
"extra": payload.get("extra", {}),
|
|
46
47
|
"updated_at": envelope["event"]["created_at"],
|
|
47
48
|
"event_id": envelope["hash"],
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-protocols
|
|
3
|
-
Version: 0.4.
|
|
4
|
-
Summary: Python SDK for Agent Identity, Agent Profile, and Agent Discourse protocols
|
|
3
|
+
Version: 0.4.1
|
|
4
|
+
Summary: Python SDK for Agent Identity, Agent Profile, Agent Delegation, and Agent Discourse protocols
|
|
5
5
|
Author: LDCLabs
|
|
6
6
|
License: MIT
|
|
7
7
|
Keywords: agent,protocol,ed25519,sdk
|
|
@@ -15,14 +15,15 @@ Requires-Dist: requests<3,>=2.31; extra == "http"
|
|
|
15
15
|
|
|
16
16
|
# agent-protocols Python SDK
|
|
17
17
|
|
|
18
|
-
Python SDK for the draft Agent Identity, Agent Profile, and Agent Discourse protocols.
|
|
18
|
+
Python SDK for the draft Agent Identity, Agent Profile, Agent Delegation, and Agent Discourse protocols.
|
|
19
19
|
|
|
20
20
|
## Modules
|
|
21
21
|
|
|
22
22
|
- `agent_protocols.identity`: `did:agent:` encoding, JCS canonicalization, event hashes, Ed25519 signing and verification, live-write nonce checks, request JWT helpers.
|
|
23
|
-
- `agent_protocols.profile`: `profile.update` payload helpers, validation, materialization.
|
|
23
|
+
- `agent_protocols.profile`: `profile.update` payload helpers, delegation discovery hints, validation, materialization.
|
|
24
|
+
- `agent_protocols.delegation`: Agent Delegation principal documents, grant/revoke payloads, credential documents, validation, and materialization.
|
|
24
25
|
- `agent_protocols.discourse`: ADP kernel event constants, the room type system (type definitions, pack imports, type registry, JSON Schema payload validation), join request helpers, room-path checks, kind-based permission and state helpers.
|
|
25
|
-
- `agent_protocols.http_client`: optional requests-based Profile and Discourse clients. Install with `agent-protocols[http]`.
|
|
26
|
+
- `agent_protocols.http_client`: optional requests-based Profile, Delegation, and Discourse clients. Install with `agent-protocols[http]`.
|
|
26
27
|
|
|
27
28
|
## Example
|
|
28
29
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
README.md
|
|
2
2
|
pyproject.toml
|
|
3
3
|
src/agent_protocols/__init__.py
|
|
4
|
+
src/agent_protocols/delegation.py
|
|
4
5
|
src/agent_protocols/discourse.py
|
|
5
6
|
src/agent_protocols/errors.py
|
|
6
7
|
src/agent_protocols/http_client.py
|
|
@@ -11,6 +12,7 @@ src/agent_protocols.egg-info/SOURCES.txt
|
|
|
11
12
|
src/agent_protocols.egg-info/dependency_links.txt
|
|
12
13
|
src/agent_protocols.egg-info/requires.txt
|
|
13
14
|
src/agent_protocols.egg-info/top_level.txt
|
|
15
|
+
tests/test_delegation.py
|
|
14
16
|
tests/test_discourse.py
|
|
15
17
|
tests/test_discourse_coverage.py
|
|
16
18
|
tests/test_http_client.py
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
|
|
3
|
+
from agent_protocols.delegation import (
|
|
4
|
+
DELEGATION_GRANT,
|
|
5
|
+
DELEGATION_PROTOCOL,
|
|
6
|
+
DELEGATION_REVOKE,
|
|
7
|
+
delegation_grant_event,
|
|
8
|
+
delegation_revoke_event,
|
|
9
|
+
materialize_delegation_credential,
|
|
10
|
+
validate_delegation_envelope,
|
|
11
|
+
validate_delegation_grant_payload,
|
|
12
|
+
validate_principal_document,
|
|
13
|
+
)
|
|
14
|
+
from agent_protocols.identity import AgentSigner, create_event
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DelegationTests(unittest.TestCase):
|
|
18
|
+
def test_grant_event_materializes_credential(self):
|
|
19
|
+
controller = AgentSigner.from_seed(bytes([31]) * 32)
|
|
20
|
+
subject = AgentSigner.from_seed(bytes([32]) * 32)
|
|
21
|
+
payload = {
|
|
22
|
+
"id": "del_01J8ZM7A3G2T9B4Q6X8R0N1P2Q",
|
|
23
|
+
"principal": {
|
|
24
|
+
"id": "https://al.ink/yan",
|
|
25
|
+
"type": "person",
|
|
26
|
+
"name": "Yan",
|
|
27
|
+
},
|
|
28
|
+
"subject": subject.agent_id(),
|
|
29
|
+
"relationship": "primary_delegate",
|
|
30
|
+
"scopes": ["inbox.screen", "meeting.propose"],
|
|
31
|
+
"constraints": {"requires_human_approval": ["meeting.accept"]},
|
|
32
|
+
"not_before": 1_779_753_600_000,
|
|
33
|
+
"expires_at": 1_790_000_000_000,
|
|
34
|
+
}
|
|
35
|
+
envelope = controller.sign_event(
|
|
36
|
+
delegation_grant_event(controller.agent_id(), 1_779_753_600_000, 1, payload)
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
validate_delegation_envelope(envelope)
|
|
40
|
+
credential = materialize_delegation_credential(
|
|
41
|
+
envelope,
|
|
42
|
+
status_url="https://al.ink/v1/delegations/del/status",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
self.assertEqual(envelope["event"]["type"], DELEGATION_GRANT)
|
|
46
|
+
self.assertEqual(credential["protocol"], DELEGATION_PROTOCOL)
|
|
47
|
+
self.assertEqual(credential["controller"], controller.agent_id())
|
|
48
|
+
self.assertEqual(credential["subject"], subject.agent_id())
|
|
49
|
+
self.assertEqual(credential["status"], "active")
|
|
50
|
+
self.assertEqual(credential["event_id"], envelope["hash"])
|
|
51
|
+
|
|
52
|
+
def test_revoke_event_and_invalid_payloads(self):
|
|
53
|
+
controller = AgentSigner.from_seed(bytes([33]) * 32)
|
|
54
|
+
envelope = controller.sign_event(
|
|
55
|
+
delegation_revoke_event(
|
|
56
|
+
controller.agent_id(),
|
|
57
|
+
1_779_753_700_000,
|
|
58
|
+
2,
|
|
59
|
+
{
|
|
60
|
+
"id": "del_01J8ZM7A3G2T9B4Q6X8R0N1P2Q",
|
|
61
|
+
"principal_id": "https://al.ink/yan",
|
|
62
|
+
"reason": "rotated_primary_agent",
|
|
63
|
+
},
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
self.assertEqual(envelope["event"]["type"], DELEGATION_REVOKE)
|
|
68
|
+
validate_delegation_envelope(envelope)
|
|
69
|
+
with self.assertRaisesRegex(Exception, "HTTPS|scopes"):
|
|
70
|
+
validate_delegation_grant_payload(
|
|
71
|
+
{
|
|
72
|
+
"id": "del",
|
|
73
|
+
"principal": {"id": "http://example.com"},
|
|
74
|
+
"subject": controller.agent_id(),
|
|
75
|
+
"scopes": [],
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def test_principal_document_and_event_type_validation(self):
|
|
80
|
+
controller = AgentSigner.from_seed(bytes([34]) * 32)
|
|
81
|
+
validate_principal_document(
|
|
82
|
+
{
|
|
83
|
+
"id": "https://profiles.example.com/org/acme",
|
|
84
|
+
"controllers": [controller.agent_id()],
|
|
85
|
+
"delegations_url": "https://profiles.example.com/v1/delegations/query",
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
with self.assertRaises(Exception):
|
|
89
|
+
validate_principal_document(
|
|
90
|
+
{
|
|
91
|
+
"id": "https://profiles.example.com/org/acme",
|
|
92
|
+
"controllers": [],
|
|
93
|
+
}
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
wrong = controller.sign_event(
|
|
97
|
+
create_event(
|
|
98
|
+
DELEGATION_PROTOCOL,
|
|
99
|
+
"delegation.unknown",
|
|
100
|
+
controller.agent_id(),
|
|
101
|
+
1,
|
|
102
|
+
1,
|
|
103
|
+
{"id": "del"},
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
with self.assertRaisesRegex(Exception, "delegation.grant"):
|
|
107
|
+
validate_delegation_envelope(wrong)
|
|
108
|
+
|
|
109
|
+
def test_rejects_malformed_https_like_principal_urls(self):
|
|
110
|
+
signer = AgentSigner.from_seed(bytes([35]) * 32)
|
|
111
|
+
for principal_id in ("https://", "https://[::1", "http://example.com"):
|
|
112
|
+
with self.assertRaises(Exception):
|
|
113
|
+
validate_delegation_grant_payload(
|
|
114
|
+
{
|
|
115
|
+
"id": "del",
|
|
116
|
+
"principal": {"id": principal_id},
|
|
117
|
+
"subject": signer.agent_id(),
|
|
118
|
+
"scopes": ["scope"],
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
if __name__ == "__main__":
|
|
124
|
+
unittest.main()
|
|
@@ -2,6 +2,7 @@ import unittest
|
|
|
2
2
|
|
|
3
3
|
import agent_protocols.http_client as http_client
|
|
4
4
|
from agent_protocols.http_client import (
|
|
5
|
+
DelegationClient,
|
|
5
6
|
DiscourseClient,
|
|
6
7
|
ProfileClient,
|
|
7
8
|
sse_events_url,
|
|
@@ -163,6 +164,43 @@ class DiscourseClientTests(unittest.TestCase):
|
|
|
163
164
|
self.assertEqual(session.calls[4][3], {"state": "idle", "expires_at": 2})
|
|
164
165
|
|
|
165
166
|
|
|
167
|
+
class DelegationClientTests(unittest.TestCase):
|
|
168
|
+
def test_every_endpoint_uses_expected_method_and_path(self):
|
|
169
|
+
session = FakeSession([FakeResponse({"ok": True}) for _ in range(7)])
|
|
170
|
+
client = DelegationClient("https://al.ink/", session=session)
|
|
171
|
+
envelope = {
|
|
172
|
+
"hash": "h",
|
|
173
|
+
"event": {
|
|
174
|
+
"protocol": "agent-delegation/1.0",
|
|
175
|
+
"type": "delegation.revoke",
|
|
176
|
+
"actor": AGENT_ID,
|
|
177
|
+
"created_at": 1,
|
|
178
|
+
"nonce": 1,
|
|
179
|
+
"payload": {"id": "del_1", "principal_id": "https://al.ink/yan"},
|
|
180
|
+
},
|
|
181
|
+
"signature": "s",
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
client.protocol()
|
|
185
|
+
client.principal("https://al.ink/yan")
|
|
186
|
+
client.delegation("del_1")
|
|
187
|
+
client.delegation_status("del_1")
|
|
188
|
+
client.delegation_events("del_1")
|
|
189
|
+
client.submit_delegation_event(envelope)
|
|
190
|
+
client.query_delegations({"subject": AGENT_ID, "status": "active", "limit": 20})
|
|
191
|
+
|
|
192
|
+
urls = [call[1] for call in session.calls]
|
|
193
|
+
self.assertEqual(urls[0], "https://al.ink/.well-known/agent-delegation")
|
|
194
|
+
self.assertEqual(urls[1], "https://al.ink/yan")
|
|
195
|
+
self.assertEqual(session.calls[1][2], {"Accept": "application/json"})
|
|
196
|
+
self.assertEqual(urls[2], "https://al.ink/v1/delegations/del_1")
|
|
197
|
+
self.assertEqual(urls[3], "https://al.ink/v1/delegations/del_1/status")
|
|
198
|
+
self.assertEqual(urls[4], "https://al.ink/v1/delegations/del_1/events")
|
|
199
|
+
self.assertEqual((session.calls[5][0], urls[5]), ("POST", "https://al.ink/v1/delegations"))
|
|
200
|
+
self.assertEqual((session.calls[6][0], urls[6]), ("POST", "https://al.ink/v1/delegations/query"))
|
|
201
|
+
self.assertEqual(session.calls[6][3]["status"], "active")
|
|
202
|
+
|
|
203
|
+
|
|
166
204
|
class HelperTests(unittest.TestCase):
|
|
167
205
|
def test_sse_events_url_preserves_http_schemes(self):
|
|
168
206
|
self.assertEqual(
|
|
@@ -13,6 +13,14 @@ class ProfileTests(unittest.TestCase):
|
|
|
13
13
|
"capabilities": ["research"],
|
|
14
14
|
"extra": {"domain": "research"},
|
|
15
15
|
"links": [{"name": "Homepage", "url": "https://example.com", "rel": "homepage"}],
|
|
16
|
+
"delegations": [
|
|
17
|
+
{
|
|
18
|
+
"principal": {"id": "https://al.ink/yan", "type": "person", "name": "Yan"},
|
|
19
|
+
"relationship": "primary_delegate",
|
|
20
|
+
"scopes": ["inbox.screen"],
|
|
21
|
+
"credential_url": "https://al.ink/v1/delegations/del_1",
|
|
22
|
+
}
|
|
23
|
+
],
|
|
16
24
|
}
|
|
17
25
|
envelope = signer.sign_event(profile_update_event(signer.agent_id(), 1_779_753_600_000, 1, payload))
|
|
18
26
|
|
|
@@ -22,6 +30,7 @@ class ProfileTests(unittest.TestCase):
|
|
|
22
30
|
self.assertEqual(profile["name"], "ResearchAgent-v3")
|
|
23
31
|
self.assertIsNone(profile["username"])
|
|
24
32
|
self.assertEqual(profile["links"], payload["links"])
|
|
33
|
+
self.assertEqual(profile["delegations"], payload["delegations"])
|
|
25
34
|
self.assertEqual(profile["extra"], payload["extra"])
|
|
26
35
|
self.assertEqual(profile["updated_at"], 1_779_753_600_000)
|
|
27
36
|
self.assertEqual(profile["event_id"], envelope["hash"])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agent_protocols-0.4.0 → agent_protocols-0.4.1}/src/agent_protocols.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|