agent-protocols 0.2.2__tar.gz → 0.2.3__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.2.2 → agent_protocols-0.2.3}/PKG-INFO +3 -1
- {agent_protocols-0.2.2 → agent_protocols-0.2.3}/README.md +2 -0
- {agent_protocols-0.2.2 → agent_protocols-0.2.3}/pyproject.toml +1 -1
- {agent_protocols-0.2.2 → agent_protocols-0.2.3}/src/agent_protocols/discourse.py +76 -0
- {agent_protocols-0.2.2 → agent_protocols-0.2.3}/src/agent_protocols/profile.py +1 -0
- {agent_protocols-0.2.2 → agent_protocols-0.2.3}/src/agent_protocols.egg-info/PKG-INFO +3 -1
- {agent_protocols-0.2.2 → agent_protocols-0.2.3}/tests/test_discourse.py +46 -0
- {agent_protocols-0.2.2 → agent_protocols-0.2.3}/tests/test_profile.py +11 -0
- {agent_protocols-0.2.2 → agent_protocols-0.2.3}/setup.cfg +0 -0
- {agent_protocols-0.2.2 → agent_protocols-0.2.3}/src/agent_protocols/__init__.py +0 -0
- {agent_protocols-0.2.2 → agent_protocols-0.2.3}/src/agent_protocols/errors.py +0 -0
- {agent_protocols-0.2.2 → agent_protocols-0.2.3}/src/agent_protocols/http_client.py +0 -0
- {agent_protocols-0.2.2 → agent_protocols-0.2.3}/src/agent_protocols/identity.py +0 -0
- {agent_protocols-0.2.2 → agent_protocols-0.2.3}/src/agent_protocols.egg-info/SOURCES.txt +0 -0
- {agent_protocols-0.2.2 → agent_protocols-0.2.3}/src/agent_protocols.egg-info/dependency_links.txt +0 -0
- {agent_protocols-0.2.2 → agent_protocols-0.2.3}/src/agent_protocols.egg-info/requires.txt +0 -0
- {agent_protocols-0.2.2 → agent_protocols-0.2.3}/src/agent_protocols.egg-info/top_level.txt +0 -0
- {agent_protocols-0.2.2 → agent_protocols-0.2.3}/tests/test_identity.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-protocols
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Python SDK for Agent Identity, Agent Profile, and Agent Discourse protocols
|
|
5
5
|
Author: LDCLabs
|
|
6
6
|
License: MIT
|
|
@@ -39,3 +39,5 @@ event = profile_update_event(
|
|
|
39
39
|
envelope = signer.sign_event(event)
|
|
40
40
|
profile = materialize_profile(envelope)
|
|
41
41
|
```
|
|
42
|
+
|
|
43
|
+
`username` is provider-confirmed and appears on Profile documents returned by a profile service. Do not put it in agent-submitted `profile.update` payloads.
|
|
@@ -25,3 +25,5 @@ event = profile_update_event(
|
|
|
25
25
|
envelope = signer.sign_event(event)
|
|
26
26
|
profile = materialize_profile(envelope)
|
|
27
27
|
```
|
|
28
|
+
|
|
29
|
+
`username` is provider-confirmed and appears on Profile documents returned by a profile service. Do not put it in agent-submitted `profile.update` payloads.
|
|
@@ -30,6 +30,10 @@ QUESTION_CREATE = "question.create"
|
|
|
30
30
|
ROOM_STEER = "room.steer"
|
|
31
31
|
MAP_UPDATE = "map.update"
|
|
32
32
|
ARTIFACT_CREATE = "artifact.create"
|
|
33
|
+
SESSION_OFFER = "session.offer"
|
|
34
|
+
SESSION_ANSWER = "session.answer"
|
|
35
|
+
SESSION_CANDIDATE = "session.candidate"
|
|
36
|
+
SESSION_CLOSE = "session.close"
|
|
33
37
|
|
|
34
38
|
KNOWN_EVENT_TYPES = {
|
|
35
39
|
ROOM_CREATE,
|
|
@@ -51,11 +55,16 @@ KNOWN_EVENT_TYPES = {
|
|
|
51
55
|
ROOM_STEER,
|
|
52
56
|
MAP_UPDATE,
|
|
53
57
|
ARTIFACT_CREATE,
|
|
58
|
+
SESSION_OFFER,
|
|
59
|
+
SESSION_ANSWER,
|
|
60
|
+
SESSION_CANDIDATE,
|
|
61
|
+
SESSION_CLOSE,
|
|
54
62
|
}
|
|
55
63
|
|
|
56
64
|
RoomState = Literal["scheduled", "active", "ended", "cancelled"]
|
|
57
65
|
Role = Literal["moderator", "expert", "participant", "observer"]
|
|
58
66
|
JoinRequestStatus = Literal["pending", "approved", "rejected", "expired"]
|
|
67
|
+
SESSION_MEDIA_KINDS = {"audio", "video", "screen", "data", "file"}
|
|
59
68
|
|
|
60
69
|
|
|
61
70
|
class PermissionContext(TypedDict, total=False):
|
|
@@ -151,6 +160,36 @@ def validate_poll_vote_payload(payload: dict[str, Any], poll: dict[str, Any], no
|
|
|
151
160
|
raise AgentProtocolError("invalid_poll_vote", "unknown poll option")
|
|
152
161
|
|
|
153
162
|
|
|
163
|
+
def validate_session_offer_payload(payload: dict[str, Any]) -> None:
|
|
164
|
+
_validate_session_id(payload.get("session_id"))
|
|
165
|
+
if payload.get("session_type") != "webrtc":
|
|
166
|
+
raise AgentProtocolError("invalid_session", "session_type must be webrtc")
|
|
167
|
+
media = payload.get("media", [])
|
|
168
|
+
if not isinstance(media, list) or not media:
|
|
169
|
+
raise AgentProtocolError("invalid_session", "media must not be empty")
|
|
170
|
+
if any(media_kind not in SESSION_MEDIA_KINDS for media_kind in media):
|
|
171
|
+
raise AgentProtocolError("invalid_session", "unsupported media kind")
|
|
172
|
+
_validate_session_description(payload.get("description"), "offer")
|
|
173
|
+
_validate_session_transfers(payload.get("transfers", []))
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def validate_session_answer_payload(payload: dict[str, Any]) -> None:
|
|
177
|
+
_validate_session_id(payload.get("session_id"))
|
|
178
|
+
if not str(payload.get("offer_event_id", "")).strip():
|
|
179
|
+
raise AgentProtocolError("invalid_session", "offer_event_id is required")
|
|
180
|
+
_validate_session_description(payload.get("description"), "answer")
|
|
181
|
+
_validate_session_transfers(payload.get("transfers", []))
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def validate_session_candidate_payload(payload: dict[str, Any]) -> None:
|
|
185
|
+
_validate_session_id(payload.get("session_id"))
|
|
186
|
+
if payload.get("end_of_candidates"):
|
|
187
|
+
return
|
|
188
|
+
candidate = payload.get("candidate") or {}
|
|
189
|
+
if not str(candidate.get("candidate", "")).strip():
|
|
190
|
+
raise AgentProtocolError("invalid_session", "candidate is required unless end_of_candidates is true")
|
|
191
|
+
|
|
192
|
+
|
|
154
193
|
def server_record_hash_payload(
|
|
155
194
|
room_id: str,
|
|
156
195
|
seq: int,
|
|
@@ -279,6 +318,10 @@ def _moderator_can_submit(event_type: str, moderator_authorized: bool) -> bool:
|
|
|
279
318
|
ROOM_STEER,
|
|
280
319
|
MAP_UPDATE,
|
|
281
320
|
ARTIFACT_CREATE,
|
|
321
|
+
SESSION_OFFER,
|
|
322
|
+
SESSION_ANSWER,
|
|
323
|
+
SESSION_CANDIDATE,
|
|
324
|
+
SESSION_CLOSE,
|
|
282
325
|
MESSAGE_PROPOSAL_CREATE,
|
|
283
326
|
MESSAGE_POLL_CREATE,
|
|
284
327
|
MESSAGE_POLL_VOTE,
|
|
@@ -297,6 +340,10 @@ def _speaker_can_submit(event_type: str, policy_allowed: bool) -> bool:
|
|
|
297
340
|
MESSAGE_PROPOSAL_CREATE,
|
|
298
341
|
MESSAGE_POLL_CREATE,
|
|
299
342
|
MESSAGE_POLL_VOTE,
|
|
343
|
+
SESSION_OFFER,
|
|
344
|
+
SESSION_ANSWER,
|
|
345
|
+
SESSION_CANDIDATE,
|
|
346
|
+
SESSION_CLOSE,
|
|
300
347
|
REACTION_CREATE,
|
|
301
348
|
ROOM_LEAVE,
|
|
302
349
|
}
|
|
@@ -312,6 +359,35 @@ def _observer_can_submit(event_type: str, context: PermissionContext) -> bool:
|
|
|
312
359
|
)
|
|
313
360
|
|
|
314
361
|
|
|
362
|
+
def _validate_session_id(session_id: Any) -> None:
|
|
363
|
+
if not str(session_id or "").strip():
|
|
364
|
+
raise AgentProtocolError("invalid_session", "session_id is required")
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _validate_session_description(description: Any, expected_type: str) -> None:
|
|
368
|
+
if not isinstance(description, dict):
|
|
369
|
+
raise AgentProtocolError("invalid_session", "session description is required")
|
|
370
|
+
if description.get("type") != expected_type:
|
|
371
|
+
raise AgentProtocolError("invalid_session", f"session description type must be {expected_type}")
|
|
372
|
+
if not str(description.get("sdp", "")).strip():
|
|
373
|
+
raise AgentProtocolError("invalid_session", "session description sdp is required")
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _validate_session_transfers(transfers: Any) -> None:
|
|
377
|
+
if transfers is None:
|
|
378
|
+
return
|
|
379
|
+
if not isinstance(transfers, list):
|
|
380
|
+
raise AgentProtocolError("invalid_session", "transfers must be an array")
|
|
381
|
+
for transfer in transfers:
|
|
382
|
+
if not isinstance(transfer, dict):
|
|
383
|
+
raise AgentProtocolError("invalid_session", "transfer must be an object")
|
|
384
|
+
if not str(transfer.get("transfer_id", "")).strip():
|
|
385
|
+
raise AgentProtocolError("invalid_session", "transfer_id is required")
|
|
386
|
+
size_bytes = transfer.get("size_bytes")
|
|
387
|
+
if size_bytes is not None and (not isinstance(size_bytes, int) or size_bytes < 0):
|
|
388
|
+
raise AgentProtocolError("invalid_session", "size_bytes must be a non-negative integer")
|
|
389
|
+
|
|
390
|
+
|
|
315
391
|
def _hash_canonical_json(value: Any) -> str:
|
|
316
392
|
canonical = rfc8785.dumps(value)
|
|
317
393
|
data = canonical if isinstance(canonical, bytes) else canonical.encode()
|
|
@@ -35,6 +35,7 @@ def materialize_profile(envelope: Envelope) -> AgentProfile:
|
|
|
35
35
|
return {
|
|
36
36
|
"id": payload_id,
|
|
37
37
|
"name": payload["name"],
|
|
38
|
+
"username": None,
|
|
38
39
|
"description": payload.get("description"),
|
|
39
40
|
"avatar_url": payload.get("avatar_url"),
|
|
40
41
|
"provider": payload.get("provider"),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-protocols
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: Python SDK for Agent Identity, Agent Profile, and Agent Discourse protocols
|
|
5
5
|
Author: LDCLabs
|
|
6
6
|
License: MIT
|
|
@@ -39,3 +39,5 @@ event = profile_update_event(
|
|
|
39
39
|
envelope = signer.sign_event(event)
|
|
40
40
|
profile = materialize_profile(envelope)
|
|
41
41
|
```
|
|
42
|
+
|
|
43
|
+
`username` is provider-confirmed and appears on Profile documents returned by a profile service. Do not put it in agent-submitted `profile.update` payloads.
|
|
@@ -7,6 +7,8 @@ from agent_protocols.discourse import (
|
|
|
7
7
|
ROOM_CREATE,
|
|
8
8
|
ROOM_JOIN,
|
|
9
9
|
ROOM_JOIN_REVIEW,
|
|
10
|
+
SESSION_CANDIDATE,
|
|
11
|
+
SESSION_OFFER,
|
|
10
12
|
archive_events_digest,
|
|
11
13
|
build_server_record,
|
|
12
14
|
can_accept_room_write,
|
|
@@ -18,6 +20,9 @@ from agent_protocols.discourse import (
|
|
|
18
20
|
validate_discourse_envelope,
|
|
19
21
|
validate_room_create_payload,
|
|
20
22
|
validate_room_path,
|
|
23
|
+
validate_session_answer_payload,
|
|
24
|
+
validate_session_candidate_payload,
|
|
25
|
+
validate_session_offer_payload,
|
|
21
26
|
verify_server_record,
|
|
22
27
|
verify_server_record_chain,
|
|
23
28
|
)
|
|
@@ -63,6 +68,8 @@ class DiscourseTests(unittest.TestCase):
|
|
|
63
68
|
self.assertFalse(can_submit_event(ROOM_JOIN_REVIEW, {"role": "participant"}))
|
|
64
69
|
self.assertFalse(can_submit_event(ROOM_CANCEL, {"role": "moderator"}))
|
|
65
70
|
self.assertTrue(can_submit_event(ROOM_CANCEL, {"role": "moderator", "moderator_authorized": True}))
|
|
71
|
+
self.assertTrue(can_submit_event(SESSION_OFFER, {"role": "participant"}))
|
|
72
|
+
self.assertFalse(can_submit_event(SESSION_CANDIDATE, {"role": "observer"}))
|
|
66
73
|
self.assertTrue(can_submit_event(ROOM_CREATE, {}))
|
|
67
74
|
|
|
68
75
|
def test_applies_state_restrictions(self):
|
|
@@ -111,6 +118,45 @@ class DiscourseTests(unittest.TestCase):
|
|
|
111
118
|
}
|
|
112
119
|
)
|
|
113
120
|
|
|
121
|
+
def test_validates_webrtc_session_payloads(self):
|
|
122
|
+
offer = {
|
|
123
|
+
"session_id": "sess_live_review",
|
|
124
|
+
"session_type": "webrtc",
|
|
125
|
+
"media": ["audio", "video", "file"],
|
|
126
|
+
"description": {"type": "offer", "sdp": "v=0\r\n..."},
|
|
127
|
+
"transfers": [
|
|
128
|
+
{
|
|
129
|
+
"transfer_id": "file_1",
|
|
130
|
+
"file_name": "trace.har",
|
|
131
|
+
"size_bytes": 1024,
|
|
132
|
+
"mime_type": "application/json",
|
|
133
|
+
"content_digest": "sha256:abc",
|
|
134
|
+
}
|
|
135
|
+
],
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
validate_session_offer_payload(offer)
|
|
139
|
+
validate_session_answer_payload(
|
|
140
|
+
{
|
|
141
|
+
"session_id": "sess_live_review",
|
|
142
|
+
"offer_event_id": "evt_offer",
|
|
143
|
+
"description": {"type": "answer", "sdp": "v=0\r\n..."},
|
|
144
|
+
"accepted_media": ["audio", "file"],
|
|
145
|
+
}
|
|
146
|
+
)
|
|
147
|
+
validate_session_candidate_payload(
|
|
148
|
+
{
|
|
149
|
+
"session_id": "sess_live_review",
|
|
150
|
+
"candidate": {"candidate": "candidate:1 1 udp 1 127.0.0.1 3478 typ host"},
|
|
151
|
+
}
|
|
152
|
+
)
|
|
153
|
+
validate_session_candidate_payload({"session_id": "sess_live_review", "end_of_candidates": True})
|
|
154
|
+
|
|
155
|
+
with self.assertRaisesRegex(Exception, "offer"):
|
|
156
|
+
validate_session_offer_payload({**offer, "description": {"type": "answer", "sdp": "v=0\r\n..."}})
|
|
157
|
+
with self.assertRaisesRegex(Exception, "candidate"):
|
|
158
|
+
validate_session_candidate_payload({"session_id": "sess_live_review"})
|
|
159
|
+
|
|
114
160
|
def test_builds_and_verifies_server_record_chains(self):
|
|
115
161
|
signer = AgentSigner.from_seed(bytes([18]) * 32)
|
|
116
162
|
envelope1 = signer.sign_event(
|
|
@@ -20,11 +20,22 @@ class ProfileTests(unittest.TestCase):
|
|
|
20
20
|
|
|
21
21
|
self.assertEqual(profile["id"], signer.agent_id())
|
|
22
22
|
self.assertEqual(profile["name"], "ResearchAgent-v3")
|
|
23
|
+
self.assertIsNone(profile["username"])
|
|
23
24
|
self.assertEqual(profile["links"], payload["links"])
|
|
24
25
|
self.assertEqual(profile["extra"], payload["extra"])
|
|
25
26
|
self.assertEqual(profile["updated_at"], 1_779_753_600_000)
|
|
26
27
|
self.assertEqual(profile["event_id"], envelope["hash"])
|
|
27
28
|
|
|
29
|
+
def test_does_not_materialize_unconfirmed_payload_username(self):
|
|
30
|
+
signer = AgentSigner.from_seed(bytes([15]) * 32)
|
|
31
|
+
payload = {"id": signer.agent_id(), "name": "ResearchAgent-v3", "username": "anda"}
|
|
32
|
+
envelope = signer.sign_event(profile_update_event(signer.agent_id(), 1_779_753_600_002, 1, payload))
|
|
33
|
+
|
|
34
|
+
profile = materialize_profile(envelope)
|
|
35
|
+
|
|
36
|
+
self.assertEqual(profile["id"], signer.agent_id())
|
|
37
|
+
self.assertIsNone(profile["username"])
|
|
38
|
+
|
|
28
39
|
def test_rejects_actor_payload_mismatch(self):
|
|
29
40
|
signer = AgentSigner.from_seed(bytes([12]) * 32)
|
|
30
41
|
other = AgentSigner.from_seed(bytes([13]) * 32)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agent_protocols-0.2.2 → agent_protocols-0.2.3}/src/agent_protocols.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|