agent-protocols 0.2.0__tar.gz → 0.2.2__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.0 → agent_protocols-0.2.2}/PKG-INFO +1 -1
- {agent_protocols-0.2.0 → agent_protocols-0.2.2}/pyproject.toml +1 -1
- {agent_protocols-0.2.0 → agent_protocols-0.2.2}/src/agent_protocols/discourse.py +139 -3
- {agent_protocols-0.2.0 → agent_protocols-0.2.2}/src/agent_protocols/identity.py +7 -1
- {agent_protocols-0.2.0 → agent_protocols-0.2.2}/src/agent_protocols/profile.py +2 -2
- {agent_protocols-0.2.0 → agent_protocols-0.2.2}/src/agent_protocols.egg-info/PKG-INFO +1 -1
- {agent_protocols-0.2.0 → agent_protocols-0.2.2}/tests/test_discourse.py +78 -0
- {agent_protocols-0.2.0 → agent_protocols-0.2.2}/tests/test_identity.py +29 -1
- {agent_protocols-0.2.0 → agent_protocols-0.2.2}/tests/test_profile.py +3 -5
- {agent_protocols-0.2.0 → agent_protocols-0.2.2}/README.md +0 -0
- {agent_protocols-0.2.0 → agent_protocols-0.2.2}/setup.cfg +0 -0
- {agent_protocols-0.2.0 → agent_protocols-0.2.2}/src/agent_protocols/__init__.py +0 -0
- {agent_protocols-0.2.0 → agent_protocols-0.2.2}/src/agent_protocols/errors.py +0 -0
- {agent_protocols-0.2.0 → agent_protocols-0.2.2}/src/agent_protocols/http_client.py +0 -0
- {agent_protocols-0.2.0 → agent_protocols-0.2.2}/src/agent_protocols.egg-info/SOURCES.txt +0 -0
- {agent_protocols-0.2.0 → agent_protocols-0.2.2}/src/agent_protocols.egg-info/dependency_links.txt +0 -0
- {agent_protocols-0.2.0 → agent_protocols-0.2.2}/src/agent_protocols.egg-info/requires.txt +0 -0
- {agent_protocols-0.2.0 → agent_protocols-0.2.2}/src/agent_protocols.egg-info/top_level.txt +0 -0
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import base64
|
|
4
|
+
import hashlib
|
|
3
5
|
from typing import Any, Literal, TypedDict
|
|
4
6
|
|
|
7
|
+
import rfc8785
|
|
8
|
+
|
|
5
9
|
from .errors import AgentProtocolError
|
|
6
10
|
from .identity import AgentId, Envelope, Event, create_event, verify_envelope, with_room_id
|
|
7
11
|
|
|
8
12
|
DISCOURSE_PROTOCOL = "agent-discourse/1.0"
|
|
9
|
-
LEGACY_DISCOURSE_PROTOCOL = "adp/1.0"
|
|
10
13
|
|
|
11
14
|
ROOM_CREATE = "room.create"
|
|
12
15
|
ROOM_JOIN = "room.join"
|
|
@@ -74,11 +77,11 @@ def discourse_event(event_type: str, actor: AgentId, created_at: int, nonce: int
|
|
|
74
77
|
return with_room_id(create_event(DISCOURSE_PROTOCOL, event_type, actor, created_at, nonce, payload), room_id)
|
|
75
78
|
|
|
76
79
|
|
|
77
|
-
def validate_discourse_envelope(envelope: Envelope
|
|
80
|
+
def validate_discourse_envelope(envelope: Envelope) -> None:
|
|
78
81
|
verify_envelope(envelope)
|
|
79
82
|
event = envelope["event"]
|
|
80
83
|
protocol = event["protocol"]
|
|
81
|
-
if protocol != DISCOURSE_PROTOCOL
|
|
84
|
+
if protocol != DISCOURSE_PROTOCOL:
|
|
82
85
|
raise AgentProtocolError("invalid_event_protocol", f"expected {DISCOURSE_PROTOCOL}, got {protocol}")
|
|
83
86
|
if event_requires_room_id(event["type"]) and "room_id" not in event:
|
|
84
87
|
raise AgentProtocolError("missing_room_id", "event requires a room_id")
|
|
@@ -98,6 +101,132 @@ def event_requires_room_id(event_type: str) -> bool:
|
|
|
98
101
|
return event_type != ROOM_CREATE
|
|
99
102
|
|
|
100
103
|
|
|
104
|
+
def validate_room_create_payload(payload: dict[str, Any]) -> None:
|
|
105
|
+
if not str(payload.get("topic", "")).strip():
|
|
106
|
+
raise AgentProtocolError("invalid_room", "room topic must not be empty")
|
|
107
|
+
if payload.get("start_time", 0) >= payload.get("end_time", 0):
|
|
108
|
+
raise AgentProtocolError("invalid_room", "start_time must be before end_time")
|
|
109
|
+
policy = payload.get("policy") or {}
|
|
110
|
+
max_participants = policy.get("max_participants")
|
|
111
|
+
if max_participants is not None and (
|
|
112
|
+
not isinstance(max_participants, int) or max_participants < 1
|
|
113
|
+
):
|
|
114
|
+
raise AgentProtocolError("invalid_room", "max_participants must be a positive integer")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def validate_poll_create_payload(payload: dict[str, Any]) -> None:
|
|
118
|
+
if not str(payload.get("poll_id", "")).strip() or not str(payload.get("question", "")).strip():
|
|
119
|
+
raise AgentProtocolError("invalid_poll", "poll_id and question are required")
|
|
120
|
+
options = payload.get("options", [])
|
|
121
|
+
if len(options) < 2:
|
|
122
|
+
raise AgentProtocolError("invalid_poll", "poll requires at least two options")
|
|
123
|
+
option_ids: set[str] = set()
|
|
124
|
+
for option in options:
|
|
125
|
+
option_id = str(option.get("id", ""))
|
|
126
|
+
label = str(option.get("label", ""))
|
|
127
|
+
if not option_id.strip() or not label.strip():
|
|
128
|
+
raise AgentProtocolError("invalid_poll", "option id and label are required")
|
|
129
|
+
if option_id in option_ids:
|
|
130
|
+
raise AgentProtocolError("invalid_poll", "poll option ids must be unique")
|
|
131
|
+
option_ids.add(option_id)
|
|
132
|
+
min_choices = payload.get("min_choices", 1)
|
|
133
|
+
max_choices = payload.get("max_choices", 1)
|
|
134
|
+
if min_choices < 1 or max_choices < min_choices:
|
|
135
|
+
raise AgentProtocolError("invalid_poll", "invalid poll choice limits")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def validate_poll_vote_payload(payload: dict[str, Any], poll: dict[str, Any], now_ms: int | None = None) -> None:
|
|
139
|
+
if poll.get("closes_at") is not None and now_ms is not None and now_ms > poll["closes_at"]:
|
|
140
|
+
raise AgentProtocolError("poll_closed", "poll is closed")
|
|
141
|
+
min_choices = poll.get("min_choices", 1)
|
|
142
|
+
max_choices = poll.get("max_choices", 1)
|
|
143
|
+
option_ids = {option["id"] for option in poll.get("options", [])}
|
|
144
|
+
selected = payload.get("option_ids", [])
|
|
145
|
+
selected_set = set(selected)
|
|
146
|
+
if len(selected_set) != len(selected):
|
|
147
|
+
raise AgentProtocolError("invalid_poll_vote", "duplicate poll options")
|
|
148
|
+
if len(selected_set) < min_choices or len(selected_set) > max_choices:
|
|
149
|
+
raise AgentProtocolError("invalid_poll_vote", "invalid number of options")
|
|
150
|
+
if any(option_id not in option_ids for option_id in selected_set):
|
|
151
|
+
raise AgentProtocolError("invalid_poll_vote", "unknown poll option")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def server_record_hash_payload(
|
|
155
|
+
room_id: str,
|
|
156
|
+
seq: int,
|
|
157
|
+
pre_hash: str | None,
|
|
158
|
+
envelope_hash: str,
|
|
159
|
+
received_at: int,
|
|
160
|
+
) -> dict[str, Any]:
|
|
161
|
+
return {
|
|
162
|
+
"room_id": room_id,
|
|
163
|
+
"seq": seq,
|
|
164
|
+
"pre_hash": pre_hash,
|
|
165
|
+
"envelope_hash": envelope_hash,
|
|
166
|
+
"received_at": received_at,
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def server_record_hash(
|
|
171
|
+
room_id: str,
|
|
172
|
+
seq: int,
|
|
173
|
+
pre_hash: str | None,
|
|
174
|
+
envelope_hash: str,
|
|
175
|
+
received_at: int,
|
|
176
|
+
) -> str:
|
|
177
|
+
return _hash_canonical_json(server_record_hash_payload(room_id, seq, pre_hash, envelope_hash, received_at))
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def build_server_record(
|
|
181
|
+
room_id: str,
|
|
182
|
+
seq: int,
|
|
183
|
+
pre_hash: str | None,
|
|
184
|
+
received_at: int,
|
|
185
|
+
envelope: Envelope,
|
|
186
|
+
) -> dict[str, Any]:
|
|
187
|
+
return {
|
|
188
|
+
"room_id": room_id,
|
|
189
|
+
"seq": seq,
|
|
190
|
+
"pre_hash": pre_hash,
|
|
191
|
+
"hash": server_record_hash(room_id, seq, pre_hash, envelope["hash"], received_at),
|
|
192
|
+
"received_at": received_at,
|
|
193
|
+
"envelope": envelope,
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def verify_server_record(record: dict[str, Any]) -> None:
|
|
198
|
+
expected = server_record_hash(
|
|
199
|
+
record["room_id"],
|
|
200
|
+
record["seq"],
|
|
201
|
+
record.get("pre_hash"),
|
|
202
|
+
record["envelope"]["hash"],
|
|
203
|
+
record["received_at"],
|
|
204
|
+
)
|
|
205
|
+
if record["hash"] != expected:
|
|
206
|
+
raise AgentProtocolError("invalid_record_hash", f"invalid server record hash: expected {expected}, got {record['hash']}")
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def verify_server_record_chain(records: list[dict[str, Any]]) -> None:
|
|
210
|
+
previous: dict[str, Any] | None = None
|
|
211
|
+
for record in records:
|
|
212
|
+
verify_server_record(record)
|
|
213
|
+
if previous is None:
|
|
214
|
+
if record["seq"] != 1:
|
|
215
|
+
raise AgentProtocolError("invalid_record_chain", "first seq must be 1")
|
|
216
|
+
if record.get("pre_hash") is not None:
|
|
217
|
+
raise AgentProtocolError("invalid_record_chain", "first pre_hash must be null")
|
|
218
|
+
else:
|
|
219
|
+
if record["seq"] != previous["seq"] + 1:
|
|
220
|
+
raise AgentProtocolError("invalid_record_chain", "seq must increase by 1")
|
|
221
|
+
if record.get("pre_hash") != previous["hash"]:
|
|
222
|
+
raise AgentProtocolError("invalid_record_chain", "pre_hash mismatch")
|
|
223
|
+
previous = record
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def archive_events_digest(records: list[dict[str, Any]]) -> str:
|
|
227
|
+
return _hash_canonical_json(records)
|
|
228
|
+
|
|
229
|
+
|
|
101
230
|
def can_submit_event(event_type: str, context: PermissionContext) -> bool:
|
|
102
231
|
if event_type == ROOM_CREATE:
|
|
103
232
|
return True
|
|
@@ -181,3 +310,10 @@ def _observer_can_submit(event_type: str, context: PermissionContext) -> bool:
|
|
|
181
310
|
or (context.get("observer_steering_allowed", False) and event_type == ROOM_STEER)
|
|
182
311
|
or (context.get("observer_poll_vote_allowed", False) and event_type == MESSAGE_POLL_VOTE)
|
|
183
312
|
)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _hash_canonical_json(value: Any) -> str:
|
|
316
|
+
canonical = rfc8785.dumps(value)
|
|
317
|
+
data = canonical if isinstance(canonical, bytes) else canonical.encode()
|
|
318
|
+
digest = hashlib.sha3_256(data).digest()
|
|
319
|
+
return base64.urlsafe_b64encode(digest).rstrip(b"=").decode()
|
|
@@ -20,6 +20,7 @@ DEFAULT_LIVE_WRITE_WINDOW_MS = 300_000
|
|
|
20
20
|
DEFAULT_NONCE_TTL_MS = 300_000
|
|
21
21
|
DEFAULT_REQUEST_JWT_TTL_SECS = 300
|
|
22
22
|
MAX_NONCE_HEADER = "Max-Seen-Nonce"
|
|
23
|
+
MAX_SAFE_NONCE = 0x1FFFFFFFFFFFFF
|
|
23
24
|
|
|
24
25
|
Event = dict[str, Any]
|
|
25
26
|
Envelope = dict[str, Any]
|
|
@@ -134,6 +135,7 @@ class ClientNonceManager:
|
|
|
134
135
|
|
|
135
136
|
def next_nonce(self) -> int:
|
|
136
137
|
nonce = self._next_nonce
|
|
138
|
+
validate_nonce(nonce)
|
|
137
139
|
self._next_nonce += 1
|
|
138
140
|
return nonce
|
|
139
141
|
|
|
@@ -174,11 +176,13 @@ def canonical_event_bytes(event: Event) -> bytes:
|
|
|
174
176
|
|
|
175
177
|
|
|
176
178
|
def event_hash(event: Event) -> str:
|
|
179
|
+
validate_nonce(event["nonce"])
|
|
177
180
|
digest = hashlib.sha3_256(canonical_event_bytes(event)).digest()
|
|
178
181
|
return _base64url_encode(digest)
|
|
179
182
|
|
|
180
183
|
|
|
181
184
|
def sign_event(private_key: Ed25519PrivateKey, event: Event) -> str:
|
|
185
|
+
validate_nonce(event["nonce"])
|
|
182
186
|
return _base64url_encode(private_key.sign(canonical_event_bytes(event)))
|
|
183
187
|
|
|
184
188
|
|
|
@@ -253,6 +257,8 @@ def verify_request_jwt(token: str, *, audience: str, now_secs: int | None = None
|
|
|
253
257
|
raise AgentProtocolError("invalid_jwt_claim", "aud mismatch")
|
|
254
258
|
|
|
255
259
|
now = now_secs if now_secs is not None else unix_secs()
|
|
260
|
+
if claims["exp"] <= claims["iat"]:
|
|
261
|
+
raise AgentProtocolError("invalid_jwt_claim", "exp must be greater than iat")
|
|
256
262
|
if claims["iat"] > now or claims["exp"] < now:
|
|
257
263
|
raise AgentProtocolError("invalid_jwt_claim", "iat/exp outside valid time window")
|
|
258
264
|
if claims["exp"] - claims["iat"] > max_ttl_secs:
|
|
@@ -269,7 +275,7 @@ def unix_secs() -> int:
|
|
|
269
275
|
|
|
270
276
|
|
|
271
277
|
def validate_nonce(nonce: int) -> None:
|
|
272
|
-
if not isinstance(nonce, int) or nonce < 1 or nonce >
|
|
278
|
+
if not isinstance(nonce, int) or nonce < 1 or nonce > MAX_SAFE_NONCE:
|
|
273
279
|
raise AgentProtocolError("invalid_nonce", "nonce must be a positive integer less than or equal to 9007199254740991")
|
|
274
280
|
|
|
275
281
|
|
|
@@ -19,7 +19,7 @@ def profile_update_event(actor: AgentId, created_at: int, nonce: int, payload: P
|
|
|
19
19
|
def validate_profile_update(envelope: Envelope) -> None:
|
|
20
20
|
verify_envelope(envelope)
|
|
21
21
|
event = envelope["event"]
|
|
22
|
-
payload_id = event["payload"].get("id")
|
|
22
|
+
payload_id = event["payload"].get("id")
|
|
23
23
|
if event["protocol"] != PROFILE_PROTOCOL:
|
|
24
24
|
raise AgentProtocolError("invalid_event_protocol", f"expected {PROFILE_PROTOCOL}, got {event['protocol']}")
|
|
25
25
|
if event["type"] != PROFILE_UPDATE:
|
|
@@ -31,7 +31,7 @@ def validate_profile_update(envelope: Envelope) -> None:
|
|
|
31
31
|
def materialize_profile(envelope: Envelope) -> AgentProfile:
|
|
32
32
|
validate_profile_update(envelope)
|
|
33
33
|
payload = envelope["event"]["payload"]
|
|
34
|
-
payload_id = payload.get("id")
|
|
34
|
+
payload_id = payload.get("id")
|
|
35
35
|
return {
|
|
36
36
|
"id": payload_id,
|
|
37
37
|
"name": payload["name"],
|
|
@@ -7,11 +7,19 @@ from agent_protocols.discourse import (
|
|
|
7
7
|
ROOM_CREATE,
|
|
8
8
|
ROOM_JOIN,
|
|
9
9
|
ROOM_JOIN_REVIEW,
|
|
10
|
+
archive_events_digest,
|
|
11
|
+
build_server_record,
|
|
10
12
|
can_accept_room_write,
|
|
11
13
|
can_submit_event,
|
|
12
14
|
room_create_event,
|
|
15
|
+
server_record_hash,
|
|
16
|
+
validate_poll_create_payload,
|
|
17
|
+
validate_poll_vote_payload,
|
|
13
18
|
validate_discourse_envelope,
|
|
19
|
+
validate_room_create_payload,
|
|
14
20
|
validate_room_path,
|
|
21
|
+
verify_server_record,
|
|
22
|
+
verify_server_record_chain,
|
|
15
23
|
)
|
|
16
24
|
from agent_protocols.http_client import websocket_events_url
|
|
17
25
|
from agent_protocols.identity import AgentSigner, create_event
|
|
@@ -65,6 +73,76 @@ class DiscourseTests(unittest.TestCase):
|
|
|
65
73
|
self.assertFalse(can_accept_room_write(REACTION_CREATE, "ended", {"role": "participant"}))
|
|
66
74
|
self.assertTrue(can_accept_room_write(REACTION_CREATE, "ended", {"role": "participant"}, post_end_reaction_allowed=True))
|
|
67
75
|
|
|
76
|
+
def test_validates_room_creation_payloads(self):
|
|
77
|
+
validate_room_create_payload(
|
|
78
|
+
{
|
|
79
|
+
"topic": "Research room",
|
|
80
|
+
"visibility": "public",
|
|
81
|
+
"start_time": 1000,
|
|
82
|
+
"end_time": 2000,
|
|
83
|
+
"policy": {"max_participants": 2},
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
with self.assertRaises(Exception):
|
|
87
|
+
validate_room_create_payload({"topic": " ", "visibility": "public", "start_time": 1000, "end_time": 2000})
|
|
88
|
+
with self.assertRaises(Exception):
|
|
89
|
+
validate_room_create_payload(
|
|
90
|
+
{"topic": "Research room", "visibility": "public", "start_time": 2000, "end_time": 1000}
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def test_validates_poll_payloads_and_votes(self):
|
|
94
|
+
poll = {
|
|
95
|
+
"poll_id": "poll_review_order",
|
|
96
|
+
"question": "Which review order?",
|
|
97
|
+
"options": [{"id": "a", "label": "Correctness first"}, {"id": "b", "label": "Security first"}],
|
|
98
|
+
"min_choices": 1,
|
|
99
|
+
"max_choices": 1,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
validate_poll_create_payload(poll)
|
|
103
|
+
validate_poll_vote_payload({"event_id": "evt", "option_ids": ["a"]}, poll)
|
|
104
|
+
with self.assertRaises(Exception):
|
|
105
|
+
validate_poll_vote_payload({"event_id": "evt", "option_ids": ["a", "b"]}, poll)
|
|
106
|
+
with self.assertRaises(Exception):
|
|
107
|
+
validate_poll_create_payload(
|
|
108
|
+
{
|
|
109
|
+
**poll,
|
|
110
|
+
"options": [{"id": "a", "label": "Correctness first"}, {"id": "a", "label": "Duplicate"}],
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def test_builds_and_verifies_server_record_chains(self):
|
|
115
|
+
signer = AgentSigner.from_seed(bytes([18]) * 32)
|
|
116
|
+
envelope1 = signer.sign_event(
|
|
117
|
+
room_create_event(
|
|
118
|
+
signer.agent_id(),
|
|
119
|
+
100,
|
|
120
|
+
1,
|
|
121
|
+
{"topic": "Research room", "visibility": "public", "start_time": 1000, "end_time": 2000},
|
|
122
|
+
)
|
|
123
|
+
)
|
|
124
|
+
record1 = build_server_record("room123", 1, None, 110, envelope1)
|
|
125
|
+
event2 = create_event(
|
|
126
|
+
"agent-discourse/1.0",
|
|
127
|
+
MESSAGE_CREATE,
|
|
128
|
+
signer.agent_id(),
|
|
129
|
+
120,
|
|
130
|
+
2,
|
|
131
|
+
{"content_type": "text/plain", "content": "hello"},
|
|
132
|
+
)
|
|
133
|
+
event2["room_id"] = "room123"
|
|
134
|
+
envelope2 = signer.sign_event(event2)
|
|
135
|
+
record2 = build_server_record("room123", 2, record1["hash"], 130, envelope2)
|
|
136
|
+
|
|
137
|
+
self.assertEqual(record1["hash"], server_record_hash("room123", 1, None, envelope1["hash"], 110))
|
|
138
|
+
verify_server_record(record1)
|
|
139
|
+
verify_server_record_chain([record1, record2])
|
|
140
|
+
self.assertEqual(len(archive_events_digest([record1, record2])), 43)
|
|
141
|
+
with self.assertRaisesRegex(Exception, "first seq"):
|
|
142
|
+
verify_server_record_chain([record2])
|
|
143
|
+
with self.assertRaises(Exception):
|
|
144
|
+
verify_server_record_chain([{**record2, "pre_hash": "bad"}])
|
|
145
|
+
|
|
68
146
|
def test_builds_websocket_event_stream_url(self):
|
|
69
147
|
self.assertEqual(
|
|
70
148
|
websocket_events_url("https://api.example.com", "room123", "jwt.token"),
|
|
@@ -3,6 +3,7 @@ import unittest
|
|
|
3
3
|
from agent_protocols.identity import (
|
|
4
4
|
AgentSigner,
|
|
5
5
|
ClientNonceManager,
|
|
6
|
+
MAX_SAFE_NONCE,
|
|
6
7
|
MemoryNonceStore,
|
|
7
8
|
RequestBinding,
|
|
8
9
|
create_event,
|
|
@@ -22,7 +23,7 @@ class IdentityTests(unittest.TestCase):
|
|
|
22
23
|
signer.agent_id(),
|
|
23
24
|
1_779_753_600_000,
|
|
24
25
|
1,
|
|
25
|
-
{"
|
|
26
|
+
{"id": signer.agent_id(), "name": "ResearchAgent"},
|
|
26
27
|
)
|
|
27
28
|
|
|
28
29
|
envelope = signer.sign_event(event)
|
|
@@ -61,6 +62,19 @@ class IdentityTests(unittest.TestCase):
|
|
|
61
62
|
self.assertEqual(manager.peek(), 6)
|
|
62
63
|
self.assertEqual(manager.next_nonce(), 6)
|
|
63
64
|
|
|
65
|
+
def test_rejects_nonce_values_outside_safe_json_integer_range(self):
|
|
66
|
+
signer = AgentSigner.from_seed(bytes([16]) * 32)
|
|
67
|
+
|
|
68
|
+
with self.assertRaises(Exception):
|
|
69
|
+
create_event(
|
|
70
|
+
"agent-profile/1.0",
|
|
71
|
+
"profile.update",
|
|
72
|
+
signer.agent_id(),
|
|
73
|
+
1000,
|
|
74
|
+
MAX_SAFE_NONCE + 1,
|
|
75
|
+
{"id": signer.agent_id(), "name": "ResearchAgent"},
|
|
76
|
+
)
|
|
77
|
+
|
|
64
78
|
def test_signs_and_verifies_request_jwts(self):
|
|
65
79
|
signer = AgentSigner.from_seed(bytes([10]) * 32)
|
|
66
80
|
binding = RequestBinding.create("https://api.example.com")
|
|
@@ -76,6 +90,20 @@ class IdentityTests(unittest.TestCase):
|
|
|
76
90
|
|
|
77
91
|
self.assertEqual(verified["iss"], signer.agent_id())
|
|
78
92
|
|
|
93
|
+
def test_rejects_request_jwts_with_non_positive_ttl(self):
|
|
94
|
+
signer = AgentSigner.from_seed(bytes([17]) * 32)
|
|
95
|
+
binding = RequestBinding.create("https://api.example.com")
|
|
96
|
+
claims = create_request_jwt_claims(signer.agent_id(), binding, 100, 0)
|
|
97
|
+
token = signer.sign_request_jwt(claims)
|
|
98
|
+
|
|
99
|
+
with self.assertRaises(Exception):
|
|
100
|
+
verify_request_jwt(
|
|
101
|
+
token,
|
|
102
|
+
audience=binding.audience,
|
|
103
|
+
now_secs=100,
|
|
104
|
+
max_ttl_secs=300,
|
|
105
|
+
)
|
|
106
|
+
|
|
79
107
|
|
|
80
108
|
if __name__ == "__main__":
|
|
81
109
|
unittest.main()
|
|
@@ -34,15 +34,13 @@ class ProfileTests(unittest.TestCase):
|
|
|
34
34
|
with self.assertRaises(Exception):
|
|
35
35
|
validate_profile_update(envelope)
|
|
36
36
|
|
|
37
|
-
def
|
|
37
|
+
def test_rejects_legacy_agent_id_payload_without_id(self):
|
|
38
38
|
signer = AgentSigner.from_seed(bytes([14]) * 32)
|
|
39
39
|
payload = {"agent_id": signer.agent_id(), "name": "LegacyAgent"}
|
|
40
40
|
envelope = signer.sign_event(profile_update_event(signer.agent_id(), 1_779_753_600_001, 1, payload))
|
|
41
41
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
self.assertEqual(profile["id"], signer.agent_id())
|
|
45
|
-
self.assertEqual(profile["name"], "LegacyAgent")
|
|
42
|
+
with self.assertRaises(Exception):
|
|
43
|
+
materialize_profile(envelope)
|
|
46
44
|
|
|
47
45
|
|
|
48
46
|
if __name__ == "__main__":
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agent_protocols-0.2.0 → agent_protocols-0.2.2}/src/agent_protocols.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|