agent-protocols 0.3.1__tar.gz → 0.4.0__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.3.1 → agent_protocols-0.4.0}/PKG-INFO +3 -1
- {agent_protocols-0.3.1 → agent_protocols-0.4.0}/README.md +2 -0
- {agent_protocols-0.3.1 → agent_protocols-0.4.0}/pyproject.toml +1 -1
- {agent_protocols-0.3.1 → agent_protocols-0.4.0}/src/agent_protocols/__init__.py +6 -0
- {agent_protocols-0.3.1 → agent_protocols-0.4.0}/src/agent_protocols/discourse.py +102 -13
- {agent_protocols-0.3.1 → agent_protocols-0.4.0}/src/agent_protocols/http_client.py +55 -9
- {agent_protocols-0.3.1 → agent_protocols-0.4.0}/src/agent_protocols/identity.py +25 -0
- {agent_protocols-0.3.1 → agent_protocols-0.4.0}/src/agent_protocols.egg-info/PKG-INFO +3 -1
- {agent_protocols-0.3.1 → agent_protocols-0.4.0}/tests/test_discourse.py +17 -5
- {agent_protocols-0.3.1 → agent_protocols-0.4.0}/tests/test_discourse_coverage.py +16 -5
- {agent_protocols-0.3.1 → agent_protocols-0.4.0}/tests/test_http_client.py +46 -11
- {agent_protocols-0.3.1 → agent_protocols-0.4.0}/tests/test_identity_coverage.py +13 -0
- {agent_protocols-0.3.1 → agent_protocols-0.4.0}/setup.cfg +0 -0
- {agent_protocols-0.3.1 → agent_protocols-0.4.0}/src/agent_protocols/errors.py +0 -0
- {agent_protocols-0.3.1 → agent_protocols-0.4.0}/src/agent_protocols/profile.py +0 -0
- {agent_protocols-0.3.1 → agent_protocols-0.4.0}/src/agent_protocols.egg-info/SOURCES.txt +0 -0
- {agent_protocols-0.3.1 → agent_protocols-0.4.0}/src/agent_protocols.egg-info/dependency_links.txt +0 -0
- {agent_protocols-0.3.1 → agent_protocols-0.4.0}/src/agent_protocols.egg-info/requires.txt +0 -0
- {agent_protocols-0.3.1 → agent_protocols-0.4.0}/src/agent_protocols.egg-info/top_level.txt +0 -0
- {agent_protocols-0.3.1 → agent_protocols-0.4.0}/tests/test_identity.py +0 -0
- {agent_protocols-0.3.1 → agent_protocols-0.4.0}/tests/test_profile.py +0 -0
- {agent_protocols-0.3.1 → agent_protocols-0.4.0}/tests/test_profile_coverage.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-protocols
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Python SDK for Agent Identity, Agent Profile, and Agent Discourse protocols
|
|
5
5
|
Author: LDCLabs
|
|
6
6
|
License: MIT
|
|
@@ -42,3 +42,5 @@ profile = materialize_profile(envelope)
|
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
`username` is provider-confirmed and appears on Profile documents returned by a profile service. Do not put it in agent-submitted `profile.update` payloads.
|
|
45
|
+
|
|
46
|
+
ADP room writes are signed against the current room head. Use `discourse_event` or `type_define_event` with `base_seq` and `base_hash`. Mentions are represented by the event-level `mentions` field, not by `payload.extra`.
|
|
@@ -27,3 +27,5 @@ profile = materialize_profile(envelope)
|
|
|
27
27
|
```
|
|
28
28
|
|
|
29
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
|
+
|
|
31
|
+
ADP room writes are signed against the current room head. Use `discourse_event` or `type_define_event` with `base_seq` and `base_hash`. Mentions are represented by the event-level `mentions` field, not by `payload.extra`.
|
|
@@ -26,6 +26,9 @@ from .identity import (
|
|
|
26
26
|
verify_request_jwt,
|
|
27
27
|
verify_signature,
|
|
28
28
|
verify_timestamp,
|
|
29
|
+
with_mention,
|
|
30
|
+
with_mentions,
|
|
31
|
+
with_room_head,
|
|
29
32
|
with_room_id,
|
|
30
33
|
)
|
|
31
34
|
from .profile import PROFILE_PROTOCOL, PROFILE_UPDATE, materialize_profile, profile_update_event, validate_profile_update
|
|
@@ -63,5 +66,8 @@ __all__ = [
|
|
|
63
66
|
"verify_request_jwt",
|
|
64
67
|
"verify_signature",
|
|
65
68
|
"verify_timestamp",
|
|
69
|
+
"with_mention",
|
|
70
|
+
"with_mentions",
|
|
71
|
+
"with_room_head",
|
|
66
72
|
"with_room_id",
|
|
67
73
|
]
|
|
@@ -18,7 +18,17 @@ import rfc8785
|
|
|
18
18
|
from jsonschema.validators import Draft202012Validator
|
|
19
19
|
|
|
20
20
|
from .errors import AgentProtocolError
|
|
21
|
-
from .identity import
|
|
21
|
+
from .identity import (
|
|
22
|
+
AgentId,
|
|
23
|
+
Envelope,
|
|
24
|
+
Event,
|
|
25
|
+
MAX_SAFE_NONCE,
|
|
26
|
+
create_event,
|
|
27
|
+
validate_agent_id,
|
|
28
|
+
verify_envelope,
|
|
29
|
+
with_room_head,
|
|
30
|
+
with_room_id,
|
|
31
|
+
)
|
|
22
32
|
|
|
23
33
|
DISCOURSE_PROTOCOL = "agent-discourse/1.0"
|
|
24
34
|
|
|
@@ -68,6 +78,8 @@ Role = Literal["moderator", "speaker", "observer"]
|
|
|
68
78
|
TypeKind = Literal["message", "signal", "control"]
|
|
69
79
|
TypeStatus = Literal["active", "deprecated", "disabled"]
|
|
70
80
|
JoinRequestStatus = Literal["pending", "approved", "rejected", "expired"]
|
|
81
|
+
JoinDecision = Literal["approve", "reject"]
|
|
82
|
+
Visibility = Literal["public", "restricted", "private"]
|
|
71
83
|
|
|
72
84
|
ROLES = ("moderator", "speaker", "observer")
|
|
73
85
|
TYPE_KINDS = ("message", "signal", "control")
|
|
@@ -85,21 +97,69 @@ class PermissionContext(TypedDict, total=False):
|
|
|
85
97
|
join_request_approved: bool
|
|
86
98
|
|
|
87
99
|
|
|
100
|
+
class AgentStatusInput(TypedDict, total=False):
|
|
101
|
+
state: str
|
|
102
|
+
summary: str
|
|
103
|
+
seen_seq: int
|
|
104
|
+
seen_hash: str
|
|
105
|
+
claim_id: str
|
|
106
|
+
activity: str
|
|
107
|
+
expires_at: int
|
|
108
|
+
extra: dict[str, Any]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class AgentStatus(TypedDict, total=False):
|
|
112
|
+
room_id: str
|
|
113
|
+
agent_id: AgentId
|
|
114
|
+
state: str
|
|
115
|
+
summary: str
|
|
116
|
+
seen_seq: int
|
|
117
|
+
seen_hash: str
|
|
118
|
+
claim_id: str
|
|
119
|
+
activity: str
|
|
120
|
+
expires_at: int
|
|
121
|
+
updated_at: int
|
|
122
|
+
extra: dict[str, Any]
|
|
123
|
+
|
|
124
|
+
|
|
88
125
|
def room_create_event(actor: AgentId, created_at: int, nonce: int, payload: dict[str, Any]) -> Event:
|
|
89
126
|
return create_event(DISCOURSE_PROTOCOL, ROOM_CREATE, actor, created_at, nonce, payload)
|
|
90
127
|
|
|
91
128
|
|
|
92
129
|
def type_define_event(
|
|
93
|
-
actor: AgentId,
|
|
130
|
+
actor: AgentId,
|
|
131
|
+
created_at: int,
|
|
132
|
+
nonce: int,
|
|
133
|
+
room_id: str,
|
|
134
|
+
base_seq: int,
|
|
135
|
+
base_hash: str,
|
|
136
|
+
declaration: dict[str, Any],
|
|
94
137
|
) -> Event:
|
|
95
|
-
return
|
|
96
|
-
|
|
97
|
-
|
|
138
|
+
return with_room_head(
|
|
139
|
+
with_room_id(
|
|
140
|
+
create_event(DISCOURSE_PROTOCOL, TYPE_DEFINE, actor, created_at, nonce, declaration),
|
|
141
|
+
room_id,
|
|
142
|
+
),
|
|
143
|
+
base_seq,
|
|
144
|
+
base_hash,
|
|
98
145
|
)
|
|
99
146
|
|
|
100
147
|
|
|
101
|
-
def discourse_event(
|
|
102
|
-
|
|
148
|
+
def discourse_event(
|
|
149
|
+
event_type: str,
|
|
150
|
+
actor: AgentId,
|
|
151
|
+
created_at: int,
|
|
152
|
+
nonce: int,
|
|
153
|
+
room_id: str,
|
|
154
|
+
base_seq: int,
|
|
155
|
+
base_hash: str,
|
|
156
|
+
payload: Any,
|
|
157
|
+
) -> Event:
|
|
158
|
+
return with_room_head(
|
|
159
|
+
with_room_id(create_event(DISCOURSE_PROTOCOL, event_type, actor, created_at, nonce, payload), room_id),
|
|
160
|
+
base_seq,
|
|
161
|
+
base_hash,
|
|
162
|
+
)
|
|
103
163
|
|
|
104
164
|
|
|
105
165
|
def is_builtin_event_type(event_type: str) -> bool:
|
|
@@ -117,25 +177,54 @@ def validate_discourse_envelope(envelope: Envelope) -> None:
|
|
|
117
177
|
if protocol != DISCOURSE_PROTOCOL:
|
|
118
178
|
raise AgentProtocolError("invalid_event_protocol", f"expected {DISCOURSE_PROTOCOL}, got {protocol}")
|
|
119
179
|
if event["type"] == ROOM_CREATE:
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
180
|
+
_validate_room_create_event_fields(event)
|
|
181
|
+
else:
|
|
182
|
+
if "room_id" not in event:
|
|
183
|
+
raise AgentProtocolError("missing_room_id", "event requires a room_id")
|
|
184
|
+
validate_room_head_precondition(event)
|
|
185
|
+
_validate_mentions(event.get("mentions"))
|
|
124
186
|
|
|
125
187
|
|
|
126
188
|
def validate_room_path(envelope: Envelope, path_room_id: str) -> None:
|
|
127
189
|
event = envelope["event"]
|
|
128
190
|
actual = event.get("room_id")
|
|
129
191
|
if event["type"] == ROOM_CREATE:
|
|
130
|
-
|
|
131
|
-
raise AgentProtocolError("invalid_event", "room.create must not include room_id")
|
|
192
|
+
_validate_room_create_event_fields(event)
|
|
132
193
|
return
|
|
194
|
+
validate_room_head_precondition(event)
|
|
195
|
+
_validate_mentions(event.get("mentions"))
|
|
133
196
|
if actual is None:
|
|
134
197
|
raise AgentProtocolError("missing_room_id", "event requires a room_id")
|
|
135
198
|
if actual != path_room_id:
|
|
136
199
|
raise AgentProtocolError("room_id_mismatch", f"expected {path_room_id}, got {actual}")
|
|
137
200
|
|
|
138
201
|
|
|
202
|
+
def _validate_room_create_event_fields(event: Event) -> None:
|
|
203
|
+
if any(field in event for field in ("room_id", "base_seq", "base_hash", "mentions")):
|
|
204
|
+
raise AgentProtocolError(
|
|
205
|
+
"invalid_event",
|
|
206
|
+
"room.create must not include room_id, base_seq, base_hash, or mentions",
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def validate_room_head_precondition(event: Event) -> None:
|
|
211
|
+
base_seq = event.get("base_seq")
|
|
212
|
+
base_hash = event.get("base_hash")
|
|
213
|
+
if not isinstance(base_seq, int) or base_seq < 1 or base_seq > MAX_SAFE_NONCE:
|
|
214
|
+
raise AgentProtocolError("invalid_event", "base_seq must be a positive safe JSON integer")
|
|
215
|
+
if not isinstance(base_hash, str) or not base_hash.strip():
|
|
216
|
+
raise AgentProtocolError("invalid_event", "base_hash must not be empty")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _validate_mentions(mentions: Any) -> None:
|
|
220
|
+
if mentions is None:
|
|
221
|
+
return
|
|
222
|
+
if not isinstance(mentions, list):
|
|
223
|
+
raise AgentProtocolError("invalid_event", "mentions must be an Agent ID array")
|
|
224
|
+
for mention in mentions:
|
|
225
|
+
validate_agent_id(mention)
|
|
226
|
+
|
|
227
|
+
|
|
139
228
|
def validate_custom_event_type_name(name: str) -> None:
|
|
140
229
|
"""Checks the shape of a custom event type name: lowercase dot-separated,
|
|
141
230
|
at least two segments, not built-in, not under a reserved prefix."""
|
|
@@ -53,6 +53,43 @@ class DiscourseClient:
|
|
|
53
53
|
def room(self, room_id: str) -> dict[str, Any]:
|
|
54
54
|
return self._get(f"/v1/rooms/{room_id}")
|
|
55
55
|
|
|
56
|
+
def public_rooms(
|
|
57
|
+
self,
|
|
58
|
+
*,
|
|
59
|
+
status: str | None = None,
|
|
60
|
+
tag: str | None = None,
|
|
61
|
+
keyword: str | None = None,
|
|
62
|
+
creator: str | None = None,
|
|
63
|
+
starts_after: int | None = None,
|
|
64
|
+
ends_before: int | None = None,
|
|
65
|
+
language: str | None = None,
|
|
66
|
+
limit: int | None = None,
|
|
67
|
+
cursor: str | None = None,
|
|
68
|
+
) -> list[dict[str, Any]]:
|
|
69
|
+
query = urlencode(
|
|
70
|
+
{
|
|
71
|
+
key: value
|
|
72
|
+
for key, value in {
|
|
73
|
+
"status": status,
|
|
74
|
+
"tag": tag,
|
|
75
|
+
"keyword": keyword,
|
|
76
|
+
"creator": creator,
|
|
77
|
+
"starts_after": starts_after,
|
|
78
|
+
"ends_before": ends_before,
|
|
79
|
+
"language": language,
|
|
80
|
+
"limit": limit,
|
|
81
|
+
"cursor": cursor,
|
|
82
|
+
}.items()
|
|
83
|
+
if value is not None
|
|
84
|
+
},
|
|
85
|
+
quote_via=quote,
|
|
86
|
+
)
|
|
87
|
+
suffix = f"?{query}" if query else ""
|
|
88
|
+
return self._get(f"/v1/rooms/public{suffix}")
|
|
89
|
+
|
|
90
|
+
def my_rooms(self, jwt: str) -> list[dict[str, Any]]:
|
|
91
|
+
return self._get("/v1/me/rooms", jwt=jwt)
|
|
92
|
+
|
|
56
93
|
def request_join(self, room_id: str, jwt: str, request: dict[str, Any]) -> dict[str, Any]:
|
|
57
94
|
return self._post(f"/v1/rooms/{room_id}/join-requests", request, jwt=jwt)
|
|
58
95
|
|
|
@@ -95,8 +132,17 @@ class DiscourseClient:
|
|
|
95
132
|
suffix = f"?{query}" if query else ""
|
|
96
133
|
return self._get(f"/v1/rooms/{room_id}/events{suffix}", jwt=jwt)
|
|
97
134
|
|
|
98
|
-
def
|
|
99
|
-
return
|
|
135
|
+
def agent_statuses(self, room_id: str, jwt: str | None = None) -> dict[str, Any]:
|
|
136
|
+
return self._get(f"/v1/rooms/{room_id}/agent-status", jwt=jwt)
|
|
137
|
+
|
|
138
|
+
def agent_status(self, room_id: str, agent_id: AgentId, jwt: str | None = None) -> dict[str, Any]:
|
|
139
|
+
return self._get(f"/v1/rooms/{room_id}/agent-status/{agent_id}", jwt=jwt)
|
|
140
|
+
|
|
141
|
+
def set_agent_status(self, room_id: str, jwt: str, status: dict[str, Any]) -> dict[str, Any]:
|
|
142
|
+
return self._put(f"/v1/rooms/{room_id}/agent-status", status, jwt=jwt)
|
|
143
|
+
|
|
144
|
+
def sse_events_url(self, room_id: str) -> str:
|
|
145
|
+
return sse_events_url(self.base_url, room_id)
|
|
100
146
|
|
|
101
147
|
def archive(self, room_id: str) -> dict[str, Any]:
|
|
102
148
|
return self._get(f"/v1/rooms/{room_id}/archive")
|
|
@@ -111,14 +157,14 @@ class DiscourseClient:
|
|
|
111
157
|
response.raise_for_status()
|
|
112
158
|
return response.json()
|
|
113
159
|
|
|
160
|
+
def _put(self, path: str, body: Any, jwt: str | None = None) -> Any:
|
|
161
|
+
response = self.session.put(self.base_url + path, json=body, headers=_auth_headers(jwt))
|
|
162
|
+
response.raise_for_status()
|
|
163
|
+
return response.json()
|
|
164
|
+
|
|
114
165
|
|
|
115
|
-
def
|
|
116
|
-
|
|
117
|
-
if websocket_base.startswith("https://"):
|
|
118
|
-
websocket_base = "wss://" + websocket_base[len("https://") :]
|
|
119
|
-
elif websocket_base.startswith("http://"):
|
|
120
|
-
websocket_base = "ws://" + websocket_base[len("http://") :]
|
|
121
|
-
return f"{websocket_base}/v1/rooms/{quote(room_id, safe='')}/events/live?access_token={quote(jwt, safe='')}"
|
|
166
|
+
def sse_events_url(base_url: str, room_id: str) -> str:
|
|
167
|
+
return f"{base_url.rstrip('/')}/v1/rooms/{quote(room_id, safe='')}/events/live"
|
|
122
168
|
|
|
123
169
|
|
|
124
170
|
def _auth_headers(jwt: str | None) -> dict[str, str] | None:
|
|
@@ -171,6 +171,31 @@ def with_room_id(event: Event, room_id: str) -> Event:
|
|
|
171
171
|
return next_event
|
|
172
172
|
|
|
173
173
|
|
|
174
|
+
def with_room_head(event: Event, base_seq: int, base_hash: str) -> Event:
|
|
175
|
+
validate_nonce(base_seq)
|
|
176
|
+
if not isinstance(base_hash, str) or not base_hash.strip():
|
|
177
|
+
raise AgentProtocolError("invalid_event", "base_hash must not be empty")
|
|
178
|
+
next_event = dict(event)
|
|
179
|
+
next_event["base_seq"] = base_seq
|
|
180
|
+
next_event["base_hash"] = base_hash
|
|
181
|
+
return next_event
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def with_mentions(event: Event, mentions: list[AgentId]) -> Event:
|
|
185
|
+
for mention in mentions:
|
|
186
|
+
validate_agent_id(mention)
|
|
187
|
+
next_event = dict(event)
|
|
188
|
+
next_event["mentions"] = list(mentions)
|
|
189
|
+
return next_event
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def with_mention(event: Event, agent_id: AgentId) -> Event:
|
|
193
|
+
validate_agent_id(agent_id)
|
|
194
|
+
next_event = dict(event)
|
|
195
|
+
next_event["mentions"] = [*next_event.get("mentions", []), agent_id]
|
|
196
|
+
return next_event
|
|
197
|
+
|
|
198
|
+
|
|
174
199
|
def canonical_event_bytes(event: Event) -> bytes:
|
|
175
200
|
canonical = rfc8785.dumps(event)
|
|
176
201
|
return canonical if isinstance(canonical, bytes) else canonical.encode()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-protocols
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Python SDK for Agent Identity, Agent Profile, and Agent Discourse protocols
|
|
5
5
|
Author: LDCLabs
|
|
6
6
|
License: MIT
|
|
@@ -42,3 +42,5 @@ profile = materialize_profile(envelope)
|
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
`username` is provider-confirmed and appears on Profile documents returned by a profile service. Do not put it in agent-submitted `profile.update` payloads.
|
|
45
|
+
|
|
46
|
+
ADP room writes are signed against the current room head. Use `discourse_event` or `type_define_event` with `base_seq` and `base_hash`. Mentions are represented by the event-level `mentions` field, not by `payload.extra`.
|
|
@@ -42,7 +42,7 @@ from agent_protocols.discourse import (
|
|
|
42
42
|
verify_server_record_chain,
|
|
43
43
|
)
|
|
44
44
|
from agent_protocols.errors import AgentProtocolError
|
|
45
|
-
from agent_protocols.http_client import
|
|
45
|
+
from agent_protocols.http_client import sse_events_url
|
|
46
46
|
from agent_protocols.identity import AgentSigner, create_event
|
|
47
47
|
|
|
48
48
|
PACKS_PATH = Path(__file__).resolve().parents[3] / "docs/protocols/agent-discourse/1.0.packs.json"
|
|
@@ -143,6 +143,8 @@ class DiscourseTests(unittest.TestCase):
|
|
|
143
143
|
},
|
|
144
144
|
)
|
|
145
145
|
event["room_id"] = "d8ftedhpqhsusbg001tg"
|
|
146
|
+
event["base_seq"] = 17
|
|
147
|
+
event["base_hash"] = "GDt8oHZQfQ3jl5ZUfyNxKZu07yAJdDYuaw_jf_JjLYs"
|
|
146
148
|
envelope = moderator.sign_event(event)
|
|
147
149
|
validator = Draft202012Validator(json.loads(SCHEMA_PATH.read_text()))
|
|
148
150
|
|
|
@@ -336,7 +338,15 @@ class DiscourseTests(unittest.TestCase):
|
|
|
336
338
|
|
|
337
339
|
def test_signs_and_validates_type_define_envelopes(self):
|
|
338
340
|
signer = AgentSigner.from_seed(bytes([16]) * 32)
|
|
339
|
-
event = type_define_event(
|
|
341
|
+
event = type_define_event(
|
|
342
|
+
signer.agent_id(),
|
|
343
|
+
100,
|
|
344
|
+
1,
|
|
345
|
+
"d8ftedhpqhsusbg001tg",
|
|
346
|
+
1,
|
|
347
|
+
"room-create-head",
|
|
348
|
+
dict(FINDING_DEF),
|
|
349
|
+
)
|
|
340
350
|
envelope = signer.sign_event(event)
|
|
341
351
|
validate_discourse_envelope(envelope)
|
|
342
352
|
|
|
@@ -371,6 +381,8 @@ class DiscourseTests(unittest.TestCase):
|
|
|
371
381
|
{"content_type": "text/plain", "content": "hello"},
|
|
372
382
|
)
|
|
373
383
|
event2["room_id"] = "room123"
|
|
384
|
+
event2["base_seq"] = 1
|
|
385
|
+
event2["base_hash"] = record1["hash"]
|
|
374
386
|
envelope2 = signer.sign_event(event2)
|
|
375
387
|
record2 = build_server_record("room123", 2, record1["hash"], 130, envelope2)
|
|
376
388
|
|
|
@@ -383,10 +395,10 @@ class DiscourseTests(unittest.TestCase):
|
|
|
383
395
|
with self.assertRaises(AgentProtocolError):
|
|
384
396
|
verify_server_record_chain([{**record2, "pre_hash": "bad"}])
|
|
385
397
|
|
|
386
|
-
def
|
|
398
|
+
def test_builds_sse_event_stream_url(self):
|
|
387
399
|
self.assertEqual(
|
|
388
|
-
|
|
389
|
-
"
|
|
400
|
+
sse_events_url("https://api.example.com", "room123"),
|
|
401
|
+
"https://api.example.com/v1/rooms/room123/events/live",
|
|
390
402
|
)
|
|
391
403
|
|
|
392
404
|
|
|
@@ -40,13 +40,15 @@ class HelperBranchTests(unittest.TestCase):
|
|
|
40
40
|
signer = AgentSigner.from_seed(bytes([60]) * 32)
|
|
41
41
|
self.assertTrue(event_requires_room_id(MESSAGE_CREATE))
|
|
42
42
|
self.assertFalse(event_requires_room_id(ROOM_CREATE))
|
|
43
|
-
event = discourse_event(MESSAGE_CREATE, signer.agent_id(), 1, 1, "room1", {"a": 1})
|
|
43
|
+
event = discourse_event(MESSAGE_CREATE, signer.agent_id(), 1, 1, "room1", 1, "room-create-head", {"a": 1})
|
|
44
44
|
self.assertEqual(event["room_id"], "room1")
|
|
45
|
+
self.assertEqual(event["base_seq"], 1)
|
|
46
|
+
self.assertEqual(event["base_hash"], "room-create-head")
|
|
45
47
|
self.assertEqual(event["protocol"], DISCOURSE_PROTOCOL)
|
|
46
48
|
|
|
47
49
|
def test_validate_discourse_envelope_rejects_foreign_protocol(self):
|
|
48
50
|
signer = AgentSigner.from_seed(bytes([61]) * 32)
|
|
49
|
-
event = discourse_event(MESSAGE_CREATE, signer.agent_id(), 1, 1, "room1", {"a": 1})
|
|
51
|
+
event = discourse_event(MESSAGE_CREATE, signer.agent_id(), 1, 1, "room1", 1, "room-create-head", {"a": 1})
|
|
50
52
|
event["protocol"] = "other/1.0"
|
|
51
53
|
envelope = signer.sign_event(event)
|
|
52
54
|
with self.assertRaises(AgentProtocolError):
|
|
@@ -55,7 +57,7 @@ class HelperBranchTests(unittest.TestCase):
|
|
|
55
57
|
def test_validate_room_path_matches_mismatches_and_requires_room_id(self):
|
|
56
58
|
signer = AgentSigner.from_seed(bytes([62]) * 32)
|
|
57
59
|
in_room = signer.sign_event(
|
|
58
|
-
discourse_event(MESSAGE_CREATE, signer.agent_id(), 1, 1, "room1", {"a": 1})
|
|
60
|
+
discourse_event(MESSAGE_CREATE, signer.agent_id(), 1, 1, "room1", 1, "room-create-head", {"a": 1})
|
|
59
61
|
)
|
|
60
62
|
validate_room_path(in_room, "room1")
|
|
61
63
|
with self.assertRaises(AgentProtocolError):
|
|
@@ -63,7 +65,7 @@ class HelperBranchTests(unittest.TestCase):
|
|
|
63
65
|
|
|
64
66
|
no_room = {
|
|
65
67
|
"hash": "h",
|
|
66
|
-
"event": {"type": MESSAGE_CREATE, "protocol": DISCOURSE_PROTOCOL},
|
|
68
|
+
"event": {"type": MESSAGE_CREATE, "protocol": DISCOURSE_PROTOCOL, "base_seq": 1, "base_hash": "room-create-head"},
|
|
67
69
|
"signature": "s",
|
|
68
70
|
}
|
|
69
71
|
with self.assertRaises(AgentProtocolError):
|
|
@@ -169,7 +171,16 @@ class ServerRecordChainTests(unittest.TestCase):
|
|
|
169
171
|
|
|
170
172
|
def make(seq, nonce, pre_hash):
|
|
171
173
|
envelope = signer.sign_event(
|
|
172
|
-
discourse_event(
|
|
174
|
+
discourse_event(
|
|
175
|
+
MESSAGE_CREATE,
|
|
176
|
+
signer.agent_id(),
|
|
177
|
+
100,
|
|
178
|
+
nonce,
|
|
179
|
+
"room1",
|
|
180
|
+
1 if seq == 1 else seq - 1,
|
|
181
|
+
pre_hash or "room-create-head",
|
|
182
|
+
{"a": 1},
|
|
183
|
+
)
|
|
173
184
|
)
|
|
174
185
|
return build_server_record("room1", seq, pre_hash, 100 + seq, envelope)
|
|
175
186
|
|
|
@@ -4,7 +4,7 @@ import agent_protocols.http_client as http_client
|
|
|
4
4
|
from agent_protocols.http_client import (
|
|
5
5
|
DiscourseClient,
|
|
6
6
|
ProfileClient,
|
|
7
|
-
|
|
7
|
+
sse_events_url,
|
|
8
8
|
)
|
|
9
9
|
from agent_protocols.identity import AgentSigner
|
|
10
10
|
|
|
@@ -37,6 +37,10 @@ class FakeSession:
|
|
|
37
37
|
self.calls.append(("POST", url, headers, json))
|
|
38
38
|
return self._responses.pop(0)
|
|
39
39
|
|
|
40
|
+
def put(self, url, json=None, headers=None):
|
|
41
|
+
self.calls.append(("PUT", url, headers, json))
|
|
42
|
+
return self._responses.pop(0)
|
|
43
|
+
|
|
40
44
|
|
|
41
45
|
class ProfileClientTests(unittest.TestCase):
|
|
42
46
|
def test_every_endpoint_uses_the_expected_method_and_path(self):
|
|
@@ -119,28 +123,59 @@ class DiscourseClientTests(unittest.TestCase):
|
|
|
119
123
|
self.assertEqual(session.calls[10][2], {"Authorization": "Bearer jwt-d"})
|
|
120
124
|
self.assertEqual(urls[11], "https://api.example.com/v1/rooms/room1/archive")
|
|
121
125
|
|
|
122
|
-
def
|
|
126
|
+
def test_sse_events_url_method(self):
|
|
123
127
|
session = FakeSession([])
|
|
124
128
|
client = DiscourseClient("https://api.example.com", session=session)
|
|
125
129
|
self.assertEqual(
|
|
126
|
-
client.
|
|
127
|
-
|
|
130
|
+
client.sse_events_url("room1"),
|
|
131
|
+
sse_events_url("https://api.example.com", "room1"),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
def test_public_rooms_my_rooms_and_agent_status_endpoints(self):
|
|
135
|
+
session = FakeSession([FakeResponse({"ok": True}) for _ in range(5)])
|
|
136
|
+
client = DiscourseClient("https://api.example.com/", session=session)
|
|
137
|
+
|
|
138
|
+
client.public_rooms(
|
|
139
|
+
status="active",
|
|
140
|
+
tag="code review",
|
|
141
|
+
starts_after=10,
|
|
142
|
+
ends_before=20,
|
|
143
|
+
limit=5,
|
|
144
|
+
cursor="next page",
|
|
145
|
+
)
|
|
146
|
+
client.my_rooms("jwt-me")
|
|
147
|
+
client.agent_statuses("room1", jwt="jwt-statuses")
|
|
148
|
+
client.agent_status("room1", AGENT_ID, jwt="jwt-status")
|
|
149
|
+
client.set_agent_status("room1", "jwt-set", {"state": "idle", "expires_at": 2})
|
|
150
|
+
|
|
151
|
+
urls = [call[1] for call in session.calls]
|
|
152
|
+
self.assertEqual(
|
|
153
|
+
urls[0],
|
|
154
|
+
"https://api.example.com/v1/rooms/public?status=active&tag=code%20review&starts_after=10&ends_before=20&limit=5&cursor=next%20page",
|
|
128
155
|
)
|
|
156
|
+
self.assertEqual(urls[1], "https://api.example.com/v1/me/rooms")
|
|
157
|
+
self.assertEqual(session.calls[1][2], {"Authorization": "Bearer jwt-me"})
|
|
158
|
+
self.assertEqual(urls[2], "https://api.example.com/v1/rooms/room1/agent-status")
|
|
159
|
+
self.assertEqual(session.calls[2][2], {"Authorization": "Bearer jwt-statuses"})
|
|
160
|
+
self.assertEqual(urls[3], f"https://api.example.com/v1/rooms/room1/agent-status/{AGENT_ID}")
|
|
161
|
+
self.assertEqual(session.calls[4][0], "PUT")
|
|
162
|
+
self.assertEqual(session.calls[4][2], {"Authorization": "Bearer jwt-set"})
|
|
163
|
+
self.assertEqual(session.calls[4][3], {"state": "idle", "expires_at": 2})
|
|
129
164
|
|
|
130
165
|
|
|
131
166
|
class HelperTests(unittest.TestCase):
|
|
132
|
-
def
|
|
167
|
+
def test_sse_events_url_preserves_http_schemes(self):
|
|
133
168
|
self.assertEqual(
|
|
134
|
-
|
|
135
|
-
"
|
|
169
|
+
sse_events_url("https://api.example.com", "room123"),
|
|
170
|
+
"https://api.example.com/v1/rooms/room123/events/live",
|
|
136
171
|
)
|
|
137
172
|
self.assertEqual(
|
|
138
|
-
|
|
139
|
-
"
|
|
173
|
+
sse_events_url("http://api.example.com/", "room 1"),
|
|
174
|
+
"http://api.example.com/v1/rooms/room%201/events/live",
|
|
140
175
|
)
|
|
141
176
|
self.assertEqual(
|
|
142
|
-
|
|
143
|
-
"ftp://api.example.com/v1/rooms/r/events/live
|
|
177
|
+
sse_events_url("ftp://api.example.com", "r"),
|
|
178
|
+
"ftp://api.example.com/v1/rooms/r/events/live",
|
|
144
179
|
)
|
|
145
180
|
|
|
146
181
|
def test_requests_session_factory(self):
|
|
@@ -21,9 +21,12 @@ from agent_protocols.identity import (
|
|
|
21
21
|
unix_ms,
|
|
22
22
|
unix_secs,
|
|
23
23
|
validate_agent_id,
|
|
24
|
+
verify_envelope,
|
|
24
25
|
verify_event_hash_signature,
|
|
25
26
|
verify_request_jwt,
|
|
26
27
|
verify_timestamp,
|
|
28
|
+
with_mention,
|
|
29
|
+
with_room_head,
|
|
27
30
|
with_room_id,
|
|
28
31
|
)
|
|
29
32
|
from agent_protocols.errors import AgentProtocolError
|
|
@@ -65,8 +68,18 @@ class AgentIdTests(unittest.TestCase):
|
|
|
65
68
|
class EventTests(unittest.TestCase):
|
|
66
69
|
def test_create_event_and_with_room_id(self):
|
|
67
70
|
signer = AgentSigner.from_seed(bytes([21]) * 32)
|
|
71
|
+
target = AgentSigner.from_seed(bytes([22]) * 32)
|
|
68
72
|
event = create_event("p", "t", signer.agent_id(), 1000, 1, {"a": 1})
|
|
69
73
|
self.assertEqual(with_room_id(event, "room1")["room_id"], "room1")
|
|
74
|
+
room_event = with_mention(with_room_head(with_room_id(event, "room1"), 1, "room-create-head"), target.agent_id())
|
|
75
|
+
self.assertEqual(room_event["base_seq"], 1)
|
|
76
|
+
self.assertEqual(room_event["base_hash"], "room-create-head")
|
|
77
|
+
self.assertEqual(room_event["mentions"], [target.agent_id()])
|
|
78
|
+
verify_envelope(signer.sign_event(room_event))
|
|
79
|
+
with self.assertRaises(AgentProtocolError):
|
|
80
|
+
with_room_head(event, 0, "head")
|
|
81
|
+
with self.assertRaises(AgentProtocolError):
|
|
82
|
+
with_mention(event, "bad-agent")
|
|
70
83
|
with self.assertRaises(AgentProtocolError):
|
|
71
84
|
create_event("p", "t", "bad-actor", 1, 1, {})
|
|
72
85
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agent_protocols-0.3.1 → agent_protocols-0.4.0}/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
|