agent-protocols 0.11.2__tar.gz → 0.11.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.11.2 → agent_protocols-0.11.3}/PKG-INFO +6 -6
- {agent_protocols-0.11.2 → agent_protocols-0.11.3}/README.md +5 -5
- {agent_protocols-0.11.2 → agent_protocols-0.11.3}/pyproject.toml +1 -1
- {agent_protocols-0.11.2 → agent_protocols-0.11.3}/src/agent_protocols/__init__.py +2 -2
- {agent_protocols-0.11.2 → agent_protocols-0.11.3}/src/agent_protocols/delegation.py +30 -50
- {agent_protocols-0.11.2 → agent_protocols-0.11.3}/src/agent_protocols/discourse.py +10 -19
- {agent_protocols-0.11.2 → agent_protocols-0.11.3}/src/agent_protocols/http_client.py +4 -4
- {agent_protocols-0.11.2 → agent_protocols-0.11.3}/src/agent_protocols/knowledge.py +4 -8
- {agent_protocols-0.11.2 → agent_protocols-0.11.3}/src/agent_protocols/knowledge.schema.json +0 -7
- {agent_protocols-0.11.2 → agent_protocols-0.11.3}/src/agent_protocols/mail.py +18 -18
- {agent_protocols-0.11.2 → agent_protocols-0.11.3}/src/agent_protocols/mail_state.py +4 -4
- {agent_protocols-0.11.2 → agent_protocols-0.11.3}/src/agent_protocols.egg-info/PKG-INFO +6 -6
- {agent_protocols-0.11.2 → agent_protocols-0.11.3}/tests/test_delegation.py +1 -1
- {agent_protocols-0.11.2 → agent_protocols-0.11.3}/tests/test_delegation_conformance.py +23 -19
- {agent_protocols-0.11.2 → agent_protocols-0.11.3}/tests/test_discourse.py +7 -8
- {agent_protocols-0.11.2 → agent_protocols-0.11.3}/tests/test_discourse_coverage.py +4 -4
- {agent_protocols-0.11.2 → agent_protocols-0.11.3}/tests/test_knowledge.py +4 -4
- {agent_protocols-0.11.2 → agent_protocols-0.11.3}/tests/test_knowledge_http.py +2 -2
- {agent_protocols-0.11.2 → agent_protocols-0.11.3}/tests/test_mail.py +3 -3
- {agent_protocols-0.11.2 → agent_protocols-0.11.3}/setup.cfg +0 -0
- {agent_protocols-0.11.2 → agent_protocols-0.11.3}/src/agent_protocols/errors.py +0 -0
- {agent_protocols-0.11.2 → agent_protocols-0.11.3}/src/agent_protocols/identity.py +0 -0
- {agent_protocols-0.11.2 → agent_protocols-0.11.3}/src/agent_protocols/mail.schema.json +0 -0
- {agent_protocols-0.11.2 → agent_protocols-0.11.3}/src/agent_protocols/profile.py +0 -0
- {agent_protocols-0.11.2 → agent_protocols-0.11.3}/src/agent_protocols.egg-info/SOURCES.txt +0 -0
- {agent_protocols-0.11.2 → agent_protocols-0.11.3}/src/agent_protocols.egg-info/dependency_links.txt +0 -0
- {agent_protocols-0.11.2 → agent_protocols-0.11.3}/src/agent_protocols.egg-info/requires.txt +0 -0
- {agent_protocols-0.11.2 → agent_protocols-0.11.3}/src/agent_protocols.egg-info/top_level.txt +0 -0
- {agent_protocols-0.11.2 → agent_protocols-0.11.3}/tests/test_http_client.py +0 -0
- {agent_protocols-0.11.2 → agent_protocols-0.11.3}/tests/test_identity.py +0 -0
- {agent_protocols-0.11.2 → agent_protocols-0.11.3}/tests/test_identity_coverage.py +0 -0
- {agent_protocols-0.11.2 → agent_protocols-0.11.3}/tests/test_identity_vectors.py +0 -0
- {agent_protocols-0.11.2 → agent_protocols-0.11.3}/tests/test_knowledge_vectors.py +0 -0
- {agent_protocols-0.11.2 → agent_protocols-0.11.3}/tests/test_mail_http.py +0 -0
- {agent_protocols-0.11.2 → agent_protocols-0.11.3}/tests/test_mail_vectors.py +0 -0
- {agent_protocols-0.11.2 → agent_protocols-0.11.3}/tests/test_profile.py +0 -0
- {agent_protocols-0.11.2 → agent_protocols-0.11.3}/tests/test_profile_coverage.py +0 -0
- {agent_protocols-0.11.2 → agent_protocols-0.11.3}/tests/test_profile_vectors.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-protocols
|
|
3
|
-
Version: 0.11.
|
|
3
|
+
Version: 0.11.3
|
|
4
4
|
Summary: Python SDK for Agent Identity, Agent Profile, Agent Delegation, Agent Discourse, Agent Knowledge, and Agent Mail protocols
|
|
5
5
|
Author: LDCLabs
|
|
6
6
|
License: MIT
|
|
@@ -25,7 +25,7 @@ Python SDK for the draft Agent Identity, Agent Profile, Agent Delegation, Agent
|
|
|
25
25
|
|
|
26
26
|
- `agent_protocols.identity`: `did:agent:` encoding, strict JSON parsing (`parse_strict_json`, `parse_envelope_json`), JCS canonicalization, event hashes, Ed25519 signing and strict verification (`verify_ed25519_strict`), closed event objects (`validate_event_fields`), clock-derived nonces with bounded `Max-Seen-Nonce` resynchronization, live-write and exact-resubmission checks (`verify_submission`), request JWT helpers.
|
|
27
27
|
- `agent_protocols.profile`: `profile.update` payload helpers, delegation discovery hints, validation, succession checks, materialization.
|
|
28
|
-
- `agent_protocols.delegation`: principal documents and resolution, Controller records
|
|
28
|
+
- `agent_protocols.delegation`: principal documents and resolution, Controller records, grant/revoke payloads, credentials, authority, acceptance, historical, and use checks, `verify_delegation_credential` over the latest grant record, and `audit_delegation_history` for auditors.
|
|
29
29
|
- `agent_protocols.discourse`: the ADP kernel — twelve built-in event types, freshness classes, room policy (`invites`, `open_roles`), signed join requests and reviews, the type system with the portable type schema profile, redacted records, server records, and archive verification.
|
|
30
30
|
- `agent_protocols.knowledge`: signed research contributions, known-set lifecycle views, evidence and profile bindings, in-memory storage, exact queries with checkpoint-bound pagination, single-page ranked search, discovery, and response validation.
|
|
31
31
|
- `agent_protocols.mail` and `agent_protocols.mail_state`: signed mailbox cards, sender-signed PyHPKE packets, historical key retention, card pins, inbox deduplication, and synchronized relay state with snapshots.
|
|
@@ -55,11 +55,11 @@ ADP room writes carry a signed `base_seq` / `base_hash`. Message and control wri
|
|
|
55
55
|
|
|
56
56
|
## Delegation
|
|
57
57
|
|
|
58
|
-
Controllers are records shared by `controllers` and `retired_controllers`: `id` is the Agent ID, `source` is an HTTPS origin or `local`, and `valid_from` starts the binding. Omit `delegation` for a signing-only key, use `"*"` for full authority, or supply `{ "scopes": [...], "audiences": [...] }` for restricted authority.
|
|
58
|
+
Controllers are records shared by `controllers` and `retired_controllers`: `id` is the Agent ID, `source` is an HTTPS origin or `local`, and `valid_from` starts the binding. Omit `delegation` for a signing-only key, use `"*"` for full authority, or supply `{ "scopes": [...], "audiences": [...] }` for restricted authority. A restricted key manages (replaces, revokes, enumerates) only credentials whose scopes and audiences fall within its own ceiling. Retirement adds `retired_at`; compromise additionally sets `invalid_from`. A principal that grants delegations publishes its `delegation_service` origin.
|
|
59
59
|
|
|
60
60
|
Controller registration (Section 4.3) proves key possession with a fixed-shape challenge: `signer.sign_controller_challenge(challenge)` signs its exact UTF-8 bytes and refuses any other string, and `verify_controller_challenge(agent_id, challenge, signature)` applies the strict Ed25519 rules. Issuing challenges, binding them to the owner-approved fields, and expiry stay with the provider.
|
|
61
61
|
|
|
62
|
-
Grants name the canonical `principal_id`. Credentials carry `principal_id`, an immutable `subject
|
|
62
|
+
Grants name the canonical `principal_id`. Credentials carry `principal_id`, an immutable `subject`, the latest `grant_event_id`, the service `accepted_at`, and `checked_at`. Materialization requires an explicit acceptance time and trusted previous state; it never derives acceptance time from `created_at`.
|
|
63
63
|
|
|
64
64
|
The validation layers have different responsibilities:
|
|
65
65
|
|
|
@@ -99,7 +99,7 @@ The suite includes both `unittest.TestCase` classes and pytest functions. Runnin
|
|
|
99
99
|
`agent_protocols.knowledge` implements Agent Knowledge 1.0: signed publications,
|
|
100
100
|
assessments and retractions; strict payload and dependency validation; deterministic
|
|
101
101
|
known-set lifecycle views; evidence byte verification; and exact query, batch,
|
|
102
|
-
ranked-search and discovery contracts. The JSON Schema ships inside the installed
|
|
102
|
+
ranked-search (with portable lexical matching) and discovery contracts. The JSON Schema ships inside the installed
|
|
103
103
|
package; no repository files are needed at runtime.
|
|
104
104
|
|
|
105
105
|
```python
|
|
@@ -128,7 +128,7 @@ event = knowledge_publish_event(signer.agent_id(), now, nonces.next_nonce(now),
|
|
|
128
128
|
envelope = signer.sign_event(event)
|
|
129
129
|
store = KnowledgeStore("https://knowledge.example.com")
|
|
130
130
|
record = store.submit(envelope)
|
|
131
|
-
page = store.query({"
|
|
131
|
+
page = store.query({"kind": "observation", "tag": "caching"})
|
|
132
132
|
assert page["result"][0]["envelope"]["hash"] == envelope["hash"]
|
|
133
133
|
assert materialize_knowledge(store.known_envelopes())[envelope["hash"]]["status"] == "active"
|
|
134
134
|
```
|
|
@@ -6,7 +6,7 @@ Python SDK for the draft Agent Identity, Agent Profile, Agent Delegation, Agent
|
|
|
6
6
|
|
|
7
7
|
- `agent_protocols.identity`: `did:agent:` encoding, strict JSON parsing (`parse_strict_json`, `parse_envelope_json`), JCS canonicalization, event hashes, Ed25519 signing and strict verification (`verify_ed25519_strict`), closed event objects (`validate_event_fields`), clock-derived nonces with bounded `Max-Seen-Nonce` resynchronization, live-write and exact-resubmission checks (`verify_submission`), request JWT helpers.
|
|
8
8
|
- `agent_protocols.profile`: `profile.update` payload helpers, delegation discovery hints, validation, succession checks, materialization.
|
|
9
|
-
- `agent_protocols.delegation`: principal documents and resolution, Controller records
|
|
9
|
+
- `agent_protocols.delegation`: principal documents and resolution, Controller records, grant/revoke payloads, credentials, authority, acceptance, historical, and use checks, `verify_delegation_credential` over the latest grant record, and `audit_delegation_history` for auditors.
|
|
10
10
|
- `agent_protocols.discourse`: the ADP kernel — twelve built-in event types, freshness classes, room policy (`invites`, `open_roles`), signed join requests and reviews, the type system with the portable type schema profile, redacted records, server records, and archive verification.
|
|
11
11
|
- `agent_protocols.knowledge`: signed research contributions, known-set lifecycle views, evidence and profile bindings, in-memory storage, exact queries with checkpoint-bound pagination, single-page ranked search, discovery, and response validation.
|
|
12
12
|
- `agent_protocols.mail` and `agent_protocols.mail_state`: signed mailbox cards, sender-signed PyHPKE packets, historical key retention, card pins, inbox deduplication, and synchronized relay state with snapshots.
|
|
@@ -36,11 +36,11 @@ ADP room writes carry a signed `base_seq` / `base_hash`. Message and control wri
|
|
|
36
36
|
|
|
37
37
|
## Delegation
|
|
38
38
|
|
|
39
|
-
Controllers are records shared by `controllers` and `retired_controllers`: `id` is the Agent ID, `source` is an HTTPS origin or `local`, and `valid_from` starts the binding. Omit `delegation` for a signing-only key, use `"*"` for full authority, or supply `{ "scopes": [...], "audiences": [...] }` for restricted authority.
|
|
39
|
+
Controllers are records shared by `controllers` and `retired_controllers`: `id` is the Agent ID, `source` is an HTTPS origin or `local`, and `valid_from` starts the binding. Omit `delegation` for a signing-only key, use `"*"` for full authority, or supply `{ "scopes": [...], "audiences": [...] }` for restricted authority. A restricted key manages (replaces, revokes, enumerates) only credentials whose scopes and audiences fall within its own ceiling. Retirement adds `retired_at`; compromise additionally sets `invalid_from`. A principal that grants delegations publishes its `delegation_service` origin.
|
|
40
40
|
|
|
41
41
|
Controller registration (Section 4.3) proves key possession with a fixed-shape challenge: `signer.sign_controller_challenge(challenge)` signs its exact UTF-8 bytes and refuses any other string, and `verify_controller_challenge(agent_id, challenge, signature)` applies the strict Ed25519 rules. Issuing challenges, binding them to the owner-approved fields, and expiry stay with the provider.
|
|
42
42
|
|
|
43
|
-
Grants name the canonical `principal_id`. Credentials carry `principal_id`, an immutable `subject
|
|
43
|
+
Grants name the canonical `principal_id`. Credentials carry `principal_id`, an immutable `subject`, the latest `grant_event_id`, the service `accepted_at`, and `checked_at`. Materialization requires an explicit acceptance time and trusted previous state; it never derives acceptance time from `created_at`.
|
|
44
44
|
|
|
45
45
|
The validation layers have different responsibilities:
|
|
46
46
|
|
|
@@ -80,7 +80,7 @@ The suite includes both `unittest.TestCase` classes and pytest functions. Runnin
|
|
|
80
80
|
`agent_protocols.knowledge` implements Agent Knowledge 1.0: signed publications,
|
|
81
81
|
assessments and retractions; strict payload and dependency validation; deterministic
|
|
82
82
|
known-set lifecycle views; evidence byte verification; and exact query, batch,
|
|
83
|
-
ranked-search and discovery contracts. The JSON Schema ships inside the installed
|
|
83
|
+
ranked-search (with portable lexical matching) and discovery contracts. The JSON Schema ships inside the installed
|
|
84
84
|
package; no repository files are needed at runtime.
|
|
85
85
|
|
|
86
86
|
```python
|
|
@@ -109,7 +109,7 @@ event = knowledge_publish_event(signer.agent_id(), now, nonces.next_nonce(now),
|
|
|
109
109
|
envelope = signer.sign_event(event)
|
|
110
110
|
store = KnowledgeStore("https://knowledge.example.com")
|
|
111
111
|
record = store.submit(envelope)
|
|
112
|
-
page = store.query({"
|
|
112
|
+
page = store.query({"kind": "observation", "tag": "caching"})
|
|
113
113
|
assert page["result"][0]["envelope"]["hash"] == envelope["hash"]
|
|
114
114
|
assert materialize_knowledge(store.known_envelopes())[envelope["hash"]]["status"] == "active"
|
|
115
115
|
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "agent-protocols"
|
|
3
|
-
version = "0.11.
|
|
3
|
+
version = "0.11.3"
|
|
4
4
|
description = "Python SDK for Agent Identity, Agent Profile, Agent Delegation, Agent Discourse, Agent Knowledge, and Agent Mail protocols"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -67,7 +67,7 @@ from .delegation import (
|
|
|
67
67
|
DelegationRecord,
|
|
68
68
|
DelegationVerdict,
|
|
69
69
|
audit_delegation_history,
|
|
70
|
-
|
|
70
|
+
controller_covers_credential,
|
|
71
71
|
delegation_grant_event,
|
|
72
72
|
delegation_revoke_event,
|
|
73
73
|
is_principal_alias,
|
|
@@ -115,7 +115,7 @@ __all__ = [
|
|
|
115
115
|
"agent_id_from_public_key",
|
|
116
116
|
"audit_delegation_history",
|
|
117
117
|
"canonical_event_bytes",
|
|
118
|
-
"
|
|
118
|
+
"controller_covers_credential",
|
|
119
119
|
"create_event",
|
|
120
120
|
"create_request_jwt_claims",
|
|
121
121
|
"delegation_grant_event",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import Any, TypedDict, Literal
|
|
3
|
+
from typing import Any, Mapping, TypedDict, Literal
|
|
4
4
|
from copy import deepcopy
|
|
5
5
|
from urllib.parse import urlparse
|
|
6
6
|
import re
|
|
@@ -34,7 +34,6 @@ DELEGATION_ERROR_CODES = (
|
|
|
34
34
|
"controller_not_current",
|
|
35
35
|
"delegation_not_permitted",
|
|
36
36
|
"delegation_ceiling_exceeded",
|
|
37
|
-
"not_owner_controller",
|
|
38
37
|
"credential_not_found",
|
|
39
38
|
"credential_identity_mismatch",
|
|
40
39
|
"grant_expired",
|
|
@@ -59,8 +58,6 @@ class _ControllerBinding(TypedDict):
|
|
|
59
58
|
class Controller(_ControllerBinding, total=False):
|
|
60
59
|
name: str
|
|
61
60
|
delegation: Literal["*"] | DelegationPolicy
|
|
62
|
-
# Earlier controllers of this principal whose credentials this key may manage.
|
|
63
|
-
supersedes: list[AgentId]
|
|
64
61
|
retired_at: int
|
|
65
62
|
invalid_from: int
|
|
66
63
|
|
|
@@ -122,10 +119,6 @@ def validate_controller(controller: Controller, retired: bool = False) -> None:
|
|
|
122
119
|
_strings(policy["audiences"], "audiences")
|
|
123
120
|
for audience in policy["audiences"]:
|
|
124
121
|
validate_audience(audience)
|
|
125
|
-
if "supersedes" in controller:
|
|
126
|
-
_strings(controller["supersedes"], "supersedes")
|
|
127
|
-
for agent_id in controller["supersedes"]:
|
|
128
|
-
validate_agent_id(agent_id)
|
|
129
122
|
if retired:
|
|
130
123
|
_timestamp(controller.get("retired_at"), "retired_at")
|
|
131
124
|
if controller["retired_at"] < controller["valid_from"]:
|
|
@@ -158,22 +151,16 @@ def validate_principal_document(document: PrincipalDocument) -> None:
|
|
|
158
151
|
delegates = delegates or "delegation" in record
|
|
159
152
|
if record["valid_from"] > document["updated_at"] or record.get("retired_at", 0) > document["updated_at"]:
|
|
160
153
|
_fail("controller timestamp exceeds document update")
|
|
161
|
-
# Succession (Section 5.1): each entry names another, earlier record.
|
|
162
|
-
for record in records.values():
|
|
163
|
-
for agent_id in record.get("supersedes", []):
|
|
164
|
-
predecessor = records.get(agent_id)
|
|
165
|
-
if predecessor is None or agent_id == record["id"] or predecessor["valid_from"] >= record["valid_from"]:
|
|
166
|
-
_fail("invalid supersedes entry")
|
|
167
154
|
if "aliases" in document:
|
|
168
155
|
_strings(document["aliases"], "aliases", empty=True)
|
|
169
156
|
for alias in document["aliases"]:
|
|
170
157
|
_validate_https_url(alias, "alias")
|
|
171
158
|
if "avatar_url" in document:
|
|
172
159
|
_validate_https_url(document["avatar_url"], "avatar_url")
|
|
173
|
-
if "
|
|
174
|
-
|
|
160
|
+
if "delegation_service" in document:
|
|
161
|
+
validate_origin(document["delegation_service"])
|
|
175
162
|
elif delegates:
|
|
176
|
-
_fail("
|
|
163
|
+
_fail("delegation_service is required when a controller carries delegation")
|
|
177
164
|
|
|
178
165
|
|
|
179
166
|
def validate_principal_resolution(document: PrincipalDocument, resolved_url: str) -> None:
|
|
@@ -196,23 +183,17 @@ def is_principal_alias(document: PrincipalDocument, url: str) -> bool:
|
|
|
196
183
|
return isinstance(aliases, list) and url in aliases
|
|
197
184
|
|
|
198
185
|
|
|
199
|
-
def
|
|
200
|
-
"""The
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
agent_id = pending.pop()
|
|
211
|
-
if agent_id in lineage:
|
|
212
|
-
continue
|
|
213
|
-
lineage.add(agent_id)
|
|
214
|
-
pending.extend(records.get(agent_id, {}).get("supersedes", []))
|
|
215
|
-
return lineage
|
|
186
|
+
def controller_covers_credential(controller: Controller, credential: Mapping[str, Any]) -> bool:
|
|
187
|
+
"""The Section 5 management rule: a restricted controller may replace, revoke,
|
|
188
|
+
or enumerate a credential only when every scope and audience of its latest
|
|
189
|
+
grant is within the controller's ceiling; an unrestricted controller covers
|
|
190
|
+
every credential of the principal."""
|
|
191
|
+
policy = controller.get("delegation")
|
|
192
|
+
if policy is None:
|
|
193
|
+
return False
|
|
194
|
+
if policy == "*":
|
|
195
|
+
return True
|
|
196
|
+
return set(credential["scopes"]) <= set(policy["scopes"]) and set(credential["audiences"]) <= set(policy["audiences"])
|
|
216
197
|
|
|
217
198
|
|
|
218
199
|
def validate_delegation_grant_payload(
|
|
@@ -352,7 +333,6 @@ def materialize_delegation_credential(
|
|
|
352
333
|
**deepcopy(payload),
|
|
353
334
|
"protocol": DELEGATION_PROTOCOL,
|
|
354
335
|
"controller": event["actor"],
|
|
355
|
-
"owner_controller": previous["owner_controller"] if previous else event["actor"],
|
|
356
336
|
"status": status,
|
|
357
337
|
"event_id": envelope["hash"],
|
|
358
338
|
"grant_event_id": envelope["hash"],
|
|
@@ -386,18 +366,20 @@ def validate_historical_delegation(record: DelegationRecord, document: Principal
|
|
|
386
366
|
_authority(record["envelope"]["event"], document, record.get("accepted_at"), previous, True)
|
|
387
367
|
|
|
388
368
|
|
|
389
|
-
def validate_controller_enumeration(document: PrincipalDocument, actor: AgentId, now: int,
|
|
369
|
+
def validate_controller_enumeration(document: PrincipalDocument, actor: AgentId, now: int,
|
|
370
|
+
credential: Mapping[str, Any]) -> None:
|
|
390
371
|
"""Per-result authorization; caller authenticates actor and resolves document."""
|
|
391
372
|
validate_principal_document(document)
|
|
392
373
|
_timestamp(now, "now")
|
|
393
|
-
|
|
374
|
+
_strings(credential.get("scopes"), "scopes")
|
|
375
|
+
_strings(credential.get("audiences"), "audiences")
|
|
394
376
|
controller = next((c for c in document["controllers"] if c["id"] == actor), None)
|
|
395
377
|
if not controller or now < controller["valid_from"]:
|
|
396
378
|
raise AgentProtocolError("controller_not_current", "controller cannot enumerate")
|
|
397
379
|
if "delegation" not in controller:
|
|
398
380
|
raise AgentProtocolError("delegation_not_permitted", "controller cannot enumerate")
|
|
399
|
-
if
|
|
400
|
-
raise AgentProtocolError("
|
|
381
|
+
if not controller_covers_credential(controller, credential):
|
|
382
|
+
raise AgentProtocolError("delegation_ceiling_exceeded", "credential is outside the controller's ceiling")
|
|
401
383
|
|
|
402
384
|
|
|
403
385
|
def validate_delegation_use(credential: DelegationCredential, audience: str, now: int) -> None:
|
|
@@ -425,8 +407,8 @@ def verify_delegation_credential(
|
|
|
425
407
|
service-trusting evidence policy: checks the accepted record of its latest
|
|
426
408
|
grant — signature, Controller binding, ceiling, and authority interval —
|
|
427
409
|
confirms that the grant matches the credential, and then checks use for
|
|
428
|
-
`audience` at `now`.
|
|
429
|
-
which the service enforced at acceptance; auditors replay
|
|
410
|
+
`audience` at `now`. The Section 5 management rule governs management,
|
|
411
|
+
which the service enforced at acceptance; auditors replay it with
|
|
430
412
|
:func:`audit_delegation_history`. Relying parties still enforce scopes and
|
|
431
413
|
constraints and authenticate the subject."""
|
|
432
414
|
reasons: list[str] = []
|
|
@@ -473,7 +455,8 @@ def _previous(event: Event, previous: DelegationCredential | None) -> None:
|
|
|
473
455
|
if event["type"] == DELEGATION_REVOKE:
|
|
474
456
|
raise AgentProtocolError("credential_not_found", "revocation requires previous credential")
|
|
475
457
|
return
|
|
476
|
-
|
|
458
|
+
_strings(previous.get("scopes"), "previous.scopes")
|
|
459
|
+
_strings(previous.get("audiences"), "previous.audiences")
|
|
477
460
|
_timestamp(previous.get("accepted_at"), "previous.accepted_at")
|
|
478
461
|
delegation_id, principal_id = _event_identity(event)
|
|
479
462
|
same_subject = event["type"] != DELEGATION_GRANT or event["payload"]["subject"] == previous.get("subject")
|
|
@@ -515,15 +498,12 @@ def _authority(event: Event, document: PrincipalDocument, accepted_at: int,
|
|
|
515
498
|
_previous(event, previous)
|
|
516
499
|
if previous and accepted_at < previous["accepted_at"]:
|
|
517
500
|
_fail("acceptance order reversed")
|
|
518
|
-
if
|
|
519
|
-
|
|
520
|
-
raise AgentProtocolError("not_owner_controller", "controller does not own credential")
|
|
501
|
+
if previous and not controller_covers_credential(controller, previous):
|
|
502
|
+
raise AgentProtocolError("delegation_ceiling_exceeded", "credential is outside the controller's ceiling")
|
|
521
503
|
if grant:
|
|
522
504
|
if "expires_at" in payload and payload["expires_at"] <= accepted_at:
|
|
523
505
|
raise AgentProtocolError("grant_expired", "grant expired at acceptance")
|
|
524
|
-
|
|
525
|
-
if policy != "*" and (not set(payload["scopes"]) <= set(policy["scopes"])
|
|
526
|
-
or not set(payload["audiences"]) <= set(policy["audiences"])):
|
|
506
|
+
if not controller_covers_credential(controller, payload):
|
|
527
507
|
raise AgentProtocolError("delegation_ceiling_exceeded", "grant exceeds controller delegation policy")
|
|
528
508
|
|
|
529
509
|
|
|
@@ -542,7 +522,7 @@ def audit_delegation_history(
|
|
|
542
522
|
) -> None:
|
|
543
523
|
"""Auditor check (Section 8): replays every accepted record of a credential
|
|
544
524
|
against the authoritative principal document — signatures, controller
|
|
545
|
-
intervals,
|
|
525
|
+
intervals, and ceilings — and confirms that the replay
|
|
546
526
|
matches the credential. Relying parties use :func:`verify_delegation_credential`."""
|
|
547
527
|
if not records:
|
|
548
528
|
_fail("no accepted records")
|
|
@@ -554,7 +534,7 @@ def audit_delegation_history(
|
|
|
554
534
|
)
|
|
555
535
|
assert replayed is not None
|
|
556
536
|
# Only status and the service's check time may differ from event replay.
|
|
557
|
-
fields = (*_GRANT_FIELDS, "protocol", "event_id", "grant_event_id", "
|
|
537
|
+
fields = (*_GRANT_FIELDS, "protocol", "event_id", "grant_event_id", "controller", "accepted_at")
|
|
558
538
|
expected = {field: replayed[field] for field in fields if field in replayed}
|
|
559
539
|
actual = {field: credential[field] for field in fields if field in credential}
|
|
560
540
|
if rfc8785.dumps(expected) != rfc8785.dumps(actual) or (
|
|
@@ -89,7 +89,6 @@ DISCOURSE_ERROR_CODES = (
|
|
|
89
89
|
"join_request_not_found",
|
|
90
90
|
"join_request_not_pending",
|
|
91
91
|
"member_banned",
|
|
92
|
-
"role_not_allowed",
|
|
93
92
|
"max_speakers_exceeded",
|
|
94
93
|
"membership_required",
|
|
95
94
|
"room_head_mismatch",
|
|
@@ -109,8 +108,8 @@ MAX_MENTIONS = 32
|
|
|
109
108
|
# Room IDs are host-assigned and URL-safe (Section 6.1).
|
|
110
109
|
ROOM_ID_PATTERN = re.compile(r"[A-Za-z0-9_-]{1,64}")
|
|
111
110
|
|
|
112
|
-
#
|
|
113
|
-
CONTENT_DIGEST_PATTERN = re.compile(r"
|
|
111
|
+
# Content digests (Section 12.5): the SHA3-256 of the bytes, encoded like an event hash.
|
|
112
|
+
CONTENT_DIGEST_PATTERN = re.compile(r"[A-Za-z0-9_-]{43}")
|
|
114
113
|
|
|
115
114
|
# Custom event types must not use these prefixes.
|
|
116
115
|
RESERVED_TYPE_PREFIXES = ("room.", "type.", "message.")
|
|
@@ -457,7 +456,7 @@ def validate_pack_import(declaration: dict[str, Any]) -> None:
|
|
|
457
456
|
raise AgentProtocolError("invalid_event", "external pack must be an HTTPS URL")
|
|
458
457
|
if not isinstance(declaration["digest"], str) or not CONTENT_DIGEST_PATTERN.fullmatch(declaration["digest"]):
|
|
459
458
|
raise AgentProtocolError(
|
|
460
|
-
"invalid_event", "external pack digest must be
|
|
459
|
+
"invalid_event", "external pack digest must be the base64url SHA3-256 of the pack bytes"
|
|
461
460
|
)
|
|
462
461
|
else:
|
|
463
462
|
raise AgentProtocolError("invalid_event", "pack import requires either use, or pack with digest")
|
|
@@ -658,7 +657,7 @@ def _strip_annotations(schema: dict[str, Any]) -> dict[str, Any]:
|
|
|
658
657
|
return stripped
|
|
659
658
|
|
|
660
659
|
|
|
661
|
-
_ROOM_POLICY_FIELDS = frozenset({"invites", "open_roles", "max_speakers", "
|
|
660
|
+
_ROOM_POLICY_FIELDS = frozenset({"invites", "open_roles", "max_speakers", "extra"})
|
|
662
661
|
|
|
663
662
|
|
|
664
663
|
def validate_room_policy(policy: dict[str, Any] | None) -> None:
|
|
@@ -673,7 +672,6 @@ def validate_room_policy(policy: dict[str, Any] | None) -> None:
|
|
|
673
672
|
max_speakers = policy.get("max_speakers")
|
|
674
673
|
if max_speakers is not None and (type(max_speakers) is not int or max_speakers < 1):
|
|
675
674
|
raise AgentProtocolError("invalid_event", "max_speakers must be a positive integer")
|
|
676
|
-
observer_allowed = policy.get("observer_allowed", True)
|
|
677
675
|
invites = policy.get("invites")
|
|
678
676
|
if invites is not None:
|
|
679
677
|
if not isinstance(invites, dict):
|
|
@@ -682,8 +680,6 @@ def validate_room_policy(policy: dict[str, Any] | None) -> None:
|
|
|
682
680
|
validate_agent_id(agent_id)
|
|
683
681
|
if role not in ROLES:
|
|
684
682
|
raise AgentProtocolError("invalid_event", f"invalid invited role: {role}")
|
|
685
|
-
if role == "observer" and not observer_allowed:
|
|
686
|
-
raise AgentProtocolError("role_not_allowed", "observers are not allowed")
|
|
687
683
|
open_roles = policy.get("open_roles")
|
|
688
684
|
if open_roles is not None:
|
|
689
685
|
if not isinstance(open_roles, list) or len(set(open_roles)) != len(open_roles):
|
|
@@ -691,8 +687,6 @@ def validate_room_policy(policy: dict[str, Any] | None) -> None:
|
|
|
691
687
|
for role in open_roles:
|
|
692
688
|
if role not in ("speaker", "observer"):
|
|
693
689
|
raise AgentProtocolError("invalid_event", f"open_roles cannot contain {role}")
|
|
694
|
-
if role == "observer" and not observer_allowed:
|
|
695
|
-
raise AgentProtocolError("role_not_allowed", "observers are not allowed")
|
|
696
690
|
|
|
697
691
|
|
|
698
692
|
def validate_room_visibility_policy(visibility: Visibility, policy: dict[str, Any] | None) -> None:
|
|
@@ -705,11 +699,11 @@ def validate_room_visibility_policy(visibility: Visibility, policy: dict[str, An
|
|
|
705
699
|
|
|
706
700
|
def effective_open_roles(visibility: Visibility, policy: dict[str, Any] | None) -> list[Role]:
|
|
707
701
|
"""The roles any agent may take by direct `room.join` (Section 8.3): the
|
|
708
|
-
explicit `open_roles`, or by default `observer` in a public room
|
|
709
|
-
|
|
702
|
+
explicit `open_roles`, or by default `observer` in a public room and none
|
|
703
|
+
otherwise."""
|
|
710
704
|
if policy is not None and policy.get("open_roles") is not None:
|
|
711
705
|
return list(policy["open_roles"])
|
|
712
|
-
if visibility == "public"
|
|
706
|
+
if visibility == "public":
|
|
713
707
|
return ["observer"]
|
|
714
708
|
return []
|
|
715
709
|
|
|
@@ -953,14 +947,11 @@ def _compile_schema(schema: dict[str, Any]) -> Draft202012Validator:
|
|
|
953
947
|
|
|
954
948
|
|
|
955
949
|
def verify_pack_digest(data: bytes, digest: str) -> None:
|
|
956
|
-
"""Verifies a
|
|
957
|
-
bytes. Supports `sha256` and `sha3-256`."""
|
|
950
|
+
"""Verifies a content digest (the base64url SHA3-256 of the bytes) over raw bytes."""
|
|
958
951
|
if not isinstance(digest, str) or not CONTENT_DIGEST_PATTERN.fullmatch(digest):
|
|
959
952
|
raise AgentProtocolError("pack_unavailable", f"invalid digest format: {digest}")
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
actual = base64.urlsafe_b64encode(raw).rstrip(b"=").decode()
|
|
963
|
-
if actual != expected:
|
|
953
|
+
actual = base64.urlsafe_b64encode(hashlib.sha3_256(data).digest()).rstrip(b"=").decode()
|
|
954
|
+
if actual != digest:
|
|
964
955
|
raise AgentProtocolError("pack_unavailable", "pack digest mismatch")
|
|
965
956
|
|
|
966
957
|
|
|
@@ -297,10 +297,10 @@ class DelegationClient:
|
|
|
297
297
|
request: dict[str, Any],
|
|
298
298
|
jwt: str | None = None,
|
|
299
299
|
) -> dict[str, Any]:
|
|
300
|
-
"""Queries
|
|
301
|
-
|
|
302
|
-
authoritative service
|
|
303
|
-
by whoever presented the credential."""
|
|
300
|
+
"""Queries an explicit endpoint, such as the `query` endpoint discovered
|
|
301
|
+
at a principal's `delegation_service` origin. A relying party reaches a
|
|
302
|
+
principal's authoritative service through that origin, never through a
|
|
303
|
+
URL supplied by whoever presented the credential."""
|
|
304
304
|
validate_delegation_query_request(request, allow_enumeration=jwt is not None)
|
|
305
305
|
return _read_json(self.session.post(query_url, json=request, headers=_auth_headers(jwt)))
|
|
306
306
|
|
|
@@ -46,7 +46,7 @@ SCHEMA = parse_strict_json(files(__package__).joinpath("knowledge.schema.json").
|
|
|
46
46
|
ASCII_FOLD = str.maketrans('ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz')
|
|
47
47
|
# Relationships that may point at an assessment as well as a publication.
|
|
48
48
|
_ASSESSMENT_RELATIONS = ("derived_from", "supports", "contradicts")
|
|
49
|
-
_READ_ONLY = ("
|
|
49
|
+
_READ_ONLY = ("cursor", "limit", "after_seq")
|
|
50
50
|
_VALIDATORS = {name: Draft202012Validator({"$ref": "#/$defs/" + name, "$defs": SCHEMA["$defs"]}, format_checker=FormatChecker()) for name in SCHEMA["$defs"]}
|
|
51
51
|
|
|
52
52
|
|
|
@@ -145,7 +145,8 @@ def validate_knowledge_envelope(item: Envelope) -> None:
|
|
|
145
145
|
verify_envelope(_identity_envelope(item))
|
|
146
146
|
payload = item["event"]["payload"]
|
|
147
147
|
validate_knowledge_id(item["hash"])
|
|
148
|
-
|
|
148
|
+
if item["event"]["type"] != KNOWLEDGE_RETRACT:
|
|
149
|
+
_https_url(payload["license"])
|
|
149
150
|
for evidence in payload.get("evidence", []):
|
|
150
151
|
_https_url(evidence["url"])
|
|
151
152
|
if "digest" in evidence:
|
|
@@ -293,8 +294,6 @@ def validate_knowledge_query(value: Mapping[str, Any]) -> dict[str, Any]:
|
|
|
293
294
|
"""Validate a parsed query and return a normalized copy."""
|
|
294
295
|
validate_knowledge_schema(value, 'queryRequest', 'invalid_request')
|
|
295
296
|
_validate_filters({k: v for k, v in value.items() if k not in _READ_ONLY})
|
|
296
|
-
if 'q' in value:
|
|
297
|
-
knowledge_text_terms(value['q'])
|
|
298
297
|
return _normalize_read_numbers(value)
|
|
299
298
|
|
|
300
299
|
|
|
@@ -317,14 +316,12 @@ def parse_knowledge_query(parameters: Iterable[tuple[str, str]]) -> dict[str, An
|
|
|
317
316
|
|
|
318
317
|
|
|
319
318
|
def knowledge_query_matches(item: Envelope, filters: Mapping[str, Any]) -> bool:
|
|
320
|
-
"""Exact filter
|
|
319
|
+
"""Exact filter predicate; `after_seq`, `limit` and `cursor` are not payload filters."""
|
|
321
320
|
event, payload = item['event'], item['event']['payload']
|
|
322
321
|
is_publication = event['type'] == KNOWLEDGE_PUBLISH
|
|
323
322
|
for key in ('actor', 'type'):
|
|
324
323
|
if key in filters and event[key] != filters[key]:
|
|
325
324
|
return False
|
|
326
|
-
if 'q' in filters and not knowledge_text_matches(item, filters['q']):
|
|
327
|
-
return False
|
|
328
325
|
if 'language' in filters and (not is_publication or _ascii_fold(payload['language']) != _ascii_fold(filters['language'])):
|
|
329
326
|
return False
|
|
330
327
|
if 'verdict' in filters and (event['type'] != KNOWLEDGE_ASSESS or payload['verdict'] != filters['verdict']):
|
|
@@ -779,7 +776,6 @@ class KnowledgeAssessPayload(KnowledgeAssessRequired, total=False):
|
|
|
779
776
|
|
|
780
777
|
|
|
781
778
|
class KnowledgeRetractRequired(TypedDict):
|
|
782
|
-
license: str
|
|
783
779
|
target: str
|
|
784
780
|
reason: str
|
|
785
781
|
|
|
@@ -473,14 +473,10 @@
|
|
|
473
473
|
"retractPayload": {
|
|
474
474
|
"type": "object",
|
|
475
475
|
"required": [
|
|
476
|
-
"license",
|
|
477
476
|
"target",
|
|
478
477
|
"reason"
|
|
479
478
|
],
|
|
480
479
|
"properties": {
|
|
481
|
-
"license": {
|
|
482
|
-
"$ref": "#/$defs/httpsUrl"
|
|
483
|
-
},
|
|
484
480
|
"target": {
|
|
485
481
|
"$ref": "#/$defs/eventHash"
|
|
486
482
|
},
|
|
@@ -893,9 +889,6 @@
|
|
|
893
889
|
"after_seq": {
|
|
894
890
|
"$ref": "#/$defs/nonNegativeInteger"
|
|
895
891
|
},
|
|
896
|
-
"q": {
|
|
897
|
-
"$ref": "#/$defs/queryText"
|
|
898
|
-
},
|
|
899
892
|
"limit": {
|
|
900
893
|
"type": "integer",
|
|
901
894
|
"minimum": 1,
|
|
@@ -227,11 +227,11 @@ def validate_mail_envelope(envelope: Envelope, definition: str = 'envelope', *,
|
|
|
227
227
|
validate_origin(route)
|
|
228
228
|
_public_key(payload['public_key'])
|
|
229
229
|
else:
|
|
230
|
-
_decode(payload['enc'], 32, '
|
|
231
|
-
ct = _decode(payload['ciphertext'], code='
|
|
232
|
-
_require(len(ct) >= 1040 and len(ct) % 1024 == 16, '
|
|
230
|
+
_decode(payload['enc'], 32, 'invalid_event')
|
|
231
|
+
ct = _decode(payload['ciphertext'], code='invalid_event')
|
|
232
|
+
_require(len(ct) >= 1040 and len(ct) % 1024 == 16, 'invalid_event', 'invalid ciphertext block length')
|
|
233
233
|
_require(event['created_at'] < payload['header']['expires_at'] <= event['created_at'] + MAIL_MAX_TTL_MS + MAIL_FUTURE_SKEW_MS,
|
|
234
|
-
'
|
|
234
|
+
'invalid_event', 'invalid packet lifetime')
|
|
235
235
|
|
|
236
236
|
|
|
237
237
|
def parse_mail_envelope(raw: str | bytes, *, max_bytes: int = MAIL_MAX_PACKET_BYTES) -> Envelope:
|
|
@@ -284,14 +284,14 @@ def encode_mail_plaintext(message: dict[str, Any]) -> bytes:
|
|
|
284
284
|
|
|
285
285
|
def decode_mail_plaintext(plaintext: bytes) -> dict[str, Any]:
|
|
286
286
|
_require(isinstance(plaintext, bytes) and 1024 <= len(plaintext) <= MAIL_MAX_PACKET_BYTES
|
|
287
|
-
and len(plaintext) % 1024 == 0, '
|
|
287
|
+
and len(plaintext) % 1024 == 0, 'invalid_event', 'invalid plaintext block length')
|
|
288
288
|
n = int.from_bytes(plaintext[:4], 'big')
|
|
289
|
-
_require(0 < n <= len(plaintext) - 4, '
|
|
290
|
-
_require(len(plaintext) == 1024 * ((4 + n + 1023) // 1024), '
|
|
291
|
-
_require(not any(plaintext[4+n:]), '
|
|
289
|
+
_require(0 < n <= len(plaintext) - 4, 'invalid_event', 'invalid plaintext JSON length')
|
|
290
|
+
_require(len(plaintext) == 1024 * ((4 + n + 1023) // 1024), 'invalid_event', 'nonminimal padding')
|
|
291
|
+
_require(not any(plaintext[4+n:]), 'invalid_event', 'nonzero padding')
|
|
292
292
|
data = plaintext[4:4+n]
|
|
293
|
-
message = _parse_bounded(data, MAIL_MAX_PACKET_BYTES, '
|
|
294
|
-
_require(_jcs(message) == data, '
|
|
293
|
+
message = _parse_bounded(data, MAIL_MAX_PACKET_BYTES, 'invalid_event')
|
|
294
|
+
_require(_jcs(message) == data, 'invalid_event', 'plaintext JSON is not JCS')
|
|
295
295
|
validate_mail_message(message)
|
|
296
296
|
return message
|
|
297
297
|
|
|
@@ -307,7 +307,7 @@ def _packet_bytes(packet: dict[str, Any], packet_id: str | None = None) -> bytes
|
|
|
307
307
|
raw = _jcs(packet)
|
|
308
308
|
_require(len(raw) <= MAIL_MAX_PACKET_BYTES, 'payload_too_large', 'packet exceeds limit')
|
|
309
309
|
if packet_id is not None:
|
|
310
|
-
_require(packet['hash'] == packet_id, '
|
|
310
|
+
_require(packet['hash'] == packet_id, 'invalid_event', 'packet ID mismatch')
|
|
311
311
|
return raw
|
|
312
312
|
|
|
313
313
|
|
|
@@ -322,7 +322,7 @@ def validate_mail_packet(packet: dict[str, Any], *, packet_id: str | None = None
|
|
|
322
322
|
|
|
323
323
|
|
|
324
324
|
def parse_mail_packet(raw: str | bytes, *, max_body_bytes: int = MAIL_MAX_PACKET_BYTES + 65536) -> dict[str, Any]:
|
|
325
|
-
packet = _parse_bounded(raw, max_body_bytes, '
|
|
325
|
+
packet = _parse_bounded(raw, max_body_bytes, 'invalid_event')
|
|
326
326
|
validate_mail_packet(packet)
|
|
327
327
|
return packet
|
|
328
328
|
|
|
@@ -342,7 +342,7 @@ def encrypt_mail(message: dict[str, Any], card: Envelope, signer: AgentSigner, n
|
|
|
342
342
|
c = card['event']['payload']
|
|
343
343
|
_require(message['created_at'] <= now + MAIL_FUTURE_SKEW_MS and now < message['expires_at'],
|
|
344
344
|
'packet_expired', 'message is expired or future-dated')
|
|
345
|
-
_require(message['expires_at'] <= c['receive_until'], '
|
|
345
|
+
_require(message['expires_at'] <= c['receive_until'], 'invalid_event', 'message exceeds receive_until')
|
|
346
346
|
header = {'mailbox_id': c['mailbox_id'], 'card_hash': card['hash'], 'expires_at': message['expires_at']}
|
|
347
347
|
event = create_event(MAIL_PROTOCOL, MAIL_SUBMIT, signer.agent_id(), now, nonce, {'header': header})
|
|
348
348
|
plaintext = encode_mail_plaintext(message)
|
|
@@ -356,7 +356,7 @@ def encrypt_mail(message: dict[str, Any], card: Envelope, signer: AgentSigner, n
|
|
|
356
356
|
finally:
|
|
357
357
|
del context
|
|
358
358
|
except (PyHPKEError, ValueError) as exc:
|
|
359
|
-
raise AgentProtocolError('
|
|
359
|
+
raise AgentProtocolError('invalid_event', 'HPKE encryption failed') from exc
|
|
360
360
|
event['payload'].update(enc=_b64(enc), ciphertext=_b64(ciphertext))
|
|
361
361
|
packet = signer.sign_event(event)
|
|
362
362
|
_require(len(_packet_bytes(packet)) <= c['max_packet_bytes'], 'payload_too_large', 'packet exceeds card limit')
|
|
@@ -378,9 +378,9 @@ def _open_verified(packet: dict[str, Any], raw: bytes, card: Envelope, key: Mail
|
|
|
378
378
|
c, h = card['event']['payload'], packet['event']['payload']['header']
|
|
379
379
|
_require(packet['event']['created_at'] <= now + MAIL_FUTURE_SKEW_MS, 'timestamp_out_of_window', 'packet is future-dated')
|
|
380
380
|
_require(h['card_hash'] == card['hash'] and h['mailbox_id'] == c['mailbox_id'],
|
|
381
|
-
'
|
|
381
|
+
'invalid_event', 'packet/card binding mismatch')
|
|
382
382
|
_require(len(raw) <= c['max_packet_bytes'], 'payload_too_large', 'packet exceeds card limit')
|
|
383
|
-
_require(h['expires_at'] <= c['receive_until'], '
|
|
383
|
+
_require(h['expires_at'] <= c['receive_until'], 'invalid_event', 'packet exceeds key retention deadline')
|
|
384
384
|
_require(isinstance(key, MailEncryptionKey) and key.public_key() == c['public_key'],
|
|
385
385
|
'invalid_private_key', 'recipient secret does not match card')
|
|
386
386
|
try:
|
|
@@ -390,11 +390,11 @@ def _open_verified(packet: dict[str, Any], raw: bytes, card: Envelope, key: Mail
|
|
|
390
390
|
finally:
|
|
391
391
|
del context
|
|
392
392
|
except (PyHPKEError, ValueError) as exc:
|
|
393
|
-
raise AgentProtocolError('
|
|
393
|
+
raise AgentProtocolError('invalid_event', 'HPKE decryption failed') from exc
|
|
394
394
|
message = decode_mail_plaintext(plaintext)
|
|
395
395
|
e = packet['event']
|
|
396
396
|
_require(message['from'] == e['actor'] and message['to'] == owner and message['expires_at'] == h['expires_at'],
|
|
397
|
-
'
|
|
397
|
+
'invalid_event', 'sender, recipient or expiration mismatch')
|
|
398
398
|
_require(message['created_at'] <= min(now, e['created_at']) + MAIL_FUTURE_SKEW_MS,
|
|
399
399
|
'timestamp_out_of_window', 'message timestamp too far in future')
|
|
400
400
|
return message
|
|
@@ -102,7 +102,7 @@ class MailKeyring:
|
|
|
102
102
|
"""Open a packet already checked by ``_packet_bytes``; used by MailInbox."""
|
|
103
103
|
with self._lock:
|
|
104
104
|
entry = self._entries.get(packet['event']['payload']['header']['card_hash'])
|
|
105
|
-
_require(entry is not None, '
|
|
105
|
+
_require(entry is not None, 'invalid_event', 'unknown retained mailbox card')
|
|
106
106
|
return _open_verified(packet, raw, entry[0], entry[1], self.owner, now)
|
|
107
107
|
|
|
108
108
|
def prune(self, *, now_ms: int | None = None) -> int:
|
|
@@ -269,10 +269,10 @@ class MailRelayStore:
|
|
|
269
269
|
"""Packets are asynchronous signed objects: no live-write window or nonce maximum applies."""
|
|
270
270
|
now = _now(now_ms)
|
|
271
271
|
validate_mail_id(mailbox_id, size=16)
|
|
272
|
-
packet = _parse_bounded(packet, self.max_body_bytes, '
|
|
272
|
+
packet = _parse_bounded(packet, self.max_body_bytes, 'invalid_event') if isinstance(packet, (str, bytes)) else copy.deepcopy(packet)
|
|
273
273
|
raw = _packet_bytes(packet)
|
|
274
274
|
h = packet['event']['payload']['header']
|
|
275
|
-
_require(h['mailbox_id'] == mailbox_id, '
|
|
275
|
+
_require(h['mailbox_id'] == mailbox_id, 'invalid_event', 'path/header mailbox mismatch')
|
|
276
276
|
pid, size = packet['hash'], len(raw)
|
|
277
277
|
with self._lock:
|
|
278
278
|
box = self._boxes.get(mailbox_id)
|
|
@@ -286,7 +286,7 @@ class MailRelayStore:
|
|
|
286
286
|
_require(card['hash'] == h['card_hash'] and now < c['expires_at'], 'stale_card', 'packet needs the current unexpired card')
|
|
287
287
|
_require(now < h['expires_at'], 'packet_expired', 'packet expired')
|
|
288
288
|
_require(h['expires_at'] <= min(c['receive_until'], now + MAIL_MAX_TTL_MS + MAIL_FUTURE_SKEW_MS),
|
|
289
|
-
'
|
|
289
|
+
'invalid_event', 'packet exceeds accepted lifetime')
|
|
290
290
|
_require(packet['event']['created_at'] <= now + MAIL_FUTURE_SKEW_MS, 'timestamp_out_of_window', 'packet is future-dated')
|
|
291
291
|
_require(size <= c['max_packet_bytes'], 'payload_too_large', 'packet exceeds card limit')
|
|
292
292
|
box.drop_expired(now)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-protocols
|
|
3
|
-
Version: 0.11.
|
|
3
|
+
Version: 0.11.3
|
|
4
4
|
Summary: Python SDK for Agent Identity, Agent Profile, Agent Delegation, Agent Discourse, Agent Knowledge, and Agent Mail protocols
|
|
5
5
|
Author: LDCLabs
|
|
6
6
|
License: MIT
|
|
@@ -25,7 +25,7 @@ Python SDK for the draft Agent Identity, Agent Profile, Agent Delegation, Agent
|
|
|
25
25
|
|
|
26
26
|
- `agent_protocols.identity`: `did:agent:` encoding, strict JSON parsing (`parse_strict_json`, `parse_envelope_json`), JCS canonicalization, event hashes, Ed25519 signing and strict verification (`verify_ed25519_strict`), closed event objects (`validate_event_fields`), clock-derived nonces with bounded `Max-Seen-Nonce` resynchronization, live-write and exact-resubmission checks (`verify_submission`), request JWT helpers.
|
|
27
27
|
- `agent_protocols.profile`: `profile.update` payload helpers, delegation discovery hints, validation, succession checks, materialization.
|
|
28
|
-
- `agent_protocols.delegation`: principal documents and resolution, Controller records
|
|
28
|
+
- `agent_protocols.delegation`: principal documents and resolution, Controller records, grant/revoke payloads, credentials, authority, acceptance, historical, and use checks, `verify_delegation_credential` over the latest grant record, and `audit_delegation_history` for auditors.
|
|
29
29
|
- `agent_protocols.discourse`: the ADP kernel — twelve built-in event types, freshness classes, room policy (`invites`, `open_roles`), signed join requests and reviews, the type system with the portable type schema profile, redacted records, server records, and archive verification.
|
|
30
30
|
- `agent_protocols.knowledge`: signed research contributions, known-set lifecycle views, evidence and profile bindings, in-memory storage, exact queries with checkpoint-bound pagination, single-page ranked search, discovery, and response validation.
|
|
31
31
|
- `agent_protocols.mail` and `agent_protocols.mail_state`: signed mailbox cards, sender-signed PyHPKE packets, historical key retention, card pins, inbox deduplication, and synchronized relay state with snapshots.
|
|
@@ -55,11 +55,11 @@ ADP room writes carry a signed `base_seq` / `base_hash`. Message and control wri
|
|
|
55
55
|
|
|
56
56
|
## Delegation
|
|
57
57
|
|
|
58
|
-
Controllers are records shared by `controllers` and `retired_controllers`: `id` is the Agent ID, `source` is an HTTPS origin or `local`, and `valid_from` starts the binding. Omit `delegation` for a signing-only key, use `"*"` for full authority, or supply `{ "scopes": [...], "audiences": [...] }` for restricted authority.
|
|
58
|
+
Controllers are records shared by `controllers` and `retired_controllers`: `id` is the Agent ID, `source` is an HTTPS origin or `local`, and `valid_from` starts the binding. Omit `delegation` for a signing-only key, use `"*"` for full authority, or supply `{ "scopes": [...], "audiences": [...] }` for restricted authority. A restricted key manages (replaces, revokes, enumerates) only credentials whose scopes and audiences fall within its own ceiling. Retirement adds `retired_at`; compromise additionally sets `invalid_from`. A principal that grants delegations publishes its `delegation_service` origin.
|
|
59
59
|
|
|
60
60
|
Controller registration (Section 4.3) proves key possession with a fixed-shape challenge: `signer.sign_controller_challenge(challenge)` signs its exact UTF-8 bytes and refuses any other string, and `verify_controller_challenge(agent_id, challenge, signature)` applies the strict Ed25519 rules. Issuing challenges, binding them to the owner-approved fields, and expiry stay with the provider.
|
|
61
61
|
|
|
62
|
-
Grants name the canonical `principal_id`. Credentials carry `principal_id`, an immutable `subject
|
|
62
|
+
Grants name the canonical `principal_id`. Credentials carry `principal_id`, an immutable `subject`, the latest `grant_event_id`, the service `accepted_at`, and `checked_at`. Materialization requires an explicit acceptance time and trusted previous state; it never derives acceptance time from `created_at`.
|
|
63
63
|
|
|
64
64
|
The validation layers have different responsibilities:
|
|
65
65
|
|
|
@@ -99,7 +99,7 @@ The suite includes both `unittest.TestCase` classes and pytest functions. Runnin
|
|
|
99
99
|
`agent_protocols.knowledge` implements Agent Knowledge 1.0: signed publications,
|
|
100
100
|
assessments and retractions; strict payload and dependency validation; deterministic
|
|
101
101
|
known-set lifecycle views; evidence byte verification; and exact query, batch,
|
|
102
|
-
ranked-search and discovery contracts. The JSON Schema ships inside the installed
|
|
102
|
+
ranked-search (with portable lexical matching) and discovery contracts. The JSON Schema ships inside the installed
|
|
103
103
|
package; no repository files are needed at runtime.
|
|
104
104
|
|
|
105
105
|
```python
|
|
@@ -128,7 +128,7 @@ event = knowledge_publish_event(signer.agent_id(), now, nonces.next_nonce(now),
|
|
|
128
128
|
envelope = signer.sign_event(event)
|
|
129
129
|
store = KnowledgeStore("https://knowledge.example.com")
|
|
130
130
|
record = store.submit(envelope)
|
|
131
|
-
page = store.query({"
|
|
131
|
+
page = store.query({"kind": "observation", "tag": "caching"})
|
|
132
132
|
assert page["result"][0]["envelope"]["hash"] == envelope["hash"]
|
|
133
133
|
assert materialize_knowledge(store.known_envelopes())[envelope["hash"]]["status"] == "active"
|
|
134
134
|
```
|
|
@@ -78,7 +78,7 @@ class DelegationTests(unittest.TestCase):
|
|
|
78
78
|
"id": "https://profiles.example.com/org/acme",
|
|
79
79
|
"protocol": DELEGATION_PROTOCOL, "updated_at": 1000, "controllers": [{"id": controller.agent_id(), "source": "local", "valid_from": 0}],
|
|
80
80
|
"aliases": ["https://profiles.example.com/acme"],
|
|
81
|
-
"
|
|
81
|
+
"delegation_service": "https://profiles.example.com",
|
|
82
82
|
}
|
|
83
83
|
)
|
|
84
84
|
with self.assertRaises(Exception):
|
|
@@ -74,7 +74,7 @@ ORIGIN = "https://dmsg.net"
|
|
|
74
74
|
|
|
75
75
|
|
|
76
76
|
def document():
|
|
77
|
-
return {"protocol": d.DELEGATION_PROTOCOL, "id": ID, "updated_at": 2000, "
|
|
77
|
+
return {"protocol": d.DELEGATION_PROTOCOL, "id": ID, "updated_at": 2000, "delegation_service": "https://example.com", "controllers": [
|
|
78
78
|
{"id": SIGNER.agent_id(), "source": ORIGIN, "valid_from": 100, "delegation": {"scopes": ["draft"], "audiences": [ORIGIN]}},
|
|
79
79
|
{"id": ROOT.agent_id(), "source": "local", "valid_from": 100, "delegation": "*"},
|
|
80
80
|
]}
|
|
@@ -98,7 +98,6 @@ def test_authority_ownership_and_materialization():
|
|
|
98
98
|
assert credential["accepted_at"] == 250
|
|
99
99
|
assert credential["checked_at"] == 250
|
|
100
100
|
assert credential["principal_id"] == ID
|
|
101
|
-
assert credential["owner_controller"] == SIGNER.agent_id()
|
|
102
101
|
assert credential["grant_event_id"] == envelope["hash"]
|
|
103
102
|
d.validate_delegation_use(credential, ORIGIN, 300)
|
|
104
103
|
with pytest.raises(AgentProtocolError):
|
|
@@ -120,18 +119,21 @@ def test_authority_ownership_and_materialization():
|
|
|
120
119
|
with pytest.raises(AgentProtocolError) as not_permitted:
|
|
121
120
|
d.validate_delegation_acceptance(envelope, only, ID, 250)
|
|
122
121
|
assert not_permitted.value.code == "delegation_not_permitted"
|
|
122
|
+
# A restricted key manages another key's credential only within its ceiling.
|
|
123
123
|
root_credential = d.materialize_delegation_credential(grant(ROOT), accepted_at=250)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
d.validate_delegation_acceptance(envelope, doc, ID, 300, root_credential)
|
|
125
|
+
wide = d.materialize_delegation_credential(grant(ROOT, 2, scopes=["draft", "admin"]), accepted_at=250)
|
|
126
|
+
with pytest.raises(AgentProtocolError) as outside:
|
|
127
|
+
d.validate_delegation_acceptance(envelope, doc, ID, 300, wide)
|
|
128
|
+
assert outside.value.code == "delegation_ceiling_exceeded"
|
|
127
129
|
with pytest.raises(AgentProtocolError):
|
|
128
|
-
d.validate_controller_enumeration(doc, SIGNER.agent_id(), 300,
|
|
129
|
-
d.validate_controller_enumeration(doc,
|
|
130
|
+
d.validate_controller_enumeration(doc, SIGNER.agent_id(), 300, wide)
|
|
131
|
+
d.validate_controller_enumeration(doc, SIGNER.agent_id(), 300, credential)
|
|
132
|
+
d.validate_controller_enumeration(doc, ROOT.agent_id(), 300, wide)
|
|
130
133
|
revocation = revoke()
|
|
131
134
|
d.validate_delegation_acceptance(revocation, doc, ID, 450, credential)
|
|
132
135
|
revoked = d.materialize_delegation_credential(revocation, accepted_at=450, previous=credential)
|
|
133
136
|
assert revoked["status"] == "revoked"
|
|
134
|
-
assert revoked["owner_controller"] == SIGNER.agent_id()
|
|
135
137
|
assert revoked["controller"] == ROOT.agent_id()
|
|
136
138
|
assert revoked["grant_event_id"] == envelope["hash"]
|
|
137
139
|
assert revoked["audiences"] == credential["audiences"]
|
|
@@ -140,7 +142,7 @@ def test_authority_ownership_and_materialization():
|
|
|
140
142
|
assert missing.value.code == "credential_not_found"
|
|
141
143
|
replacement = grant(ROOT, 3)
|
|
142
144
|
d.validate_delegation_acceptance(replacement, doc, ID, 500, revoked)
|
|
143
|
-
assert d.materialize_delegation_credential(replacement, accepted_at=500, previous=revoked)["
|
|
145
|
+
assert d.materialize_delegation_credential(replacement, accepted_at=500, previous=revoked)["controller"] == ROOT.agent_id()
|
|
144
146
|
|
|
145
147
|
|
|
146
148
|
def test_a_replacement_cannot_move_a_credential_to_another_subject():
|
|
@@ -172,32 +174,34 @@ def test_retired_history_needs_accepted_records_and_the_original_ceiling():
|
|
|
172
174
|
d.validate_historical_delegation({"envelope": grant(nonce=2, scopes=["admin"]), "accepted_at": 250}, doc, ID)
|
|
173
175
|
|
|
174
176
|
|
|
175
|
-
def
|
|
177
|
+
def test_a_restricted_controller_manages_credentials_within_its_ceiling():
|
|
176
178
|
doc = document()
|
|
177
179
|
retired = doc["controllers"].pop(0)
|
|
178
180
|
retired["retired_at"] = 600
|
|
179
181
|
doc["retired_controllers"] = [retired]
|
|
180
182
|
doc["controllers"].append({
|
|
181
183
|
"id": SUCCESSOR.agent_id(), "source": ORIGIN, "valid_from": 600,
|
|
182
|
-
"delegation": {"scopes": ["draft", "inbox"], "audiences": [ORIGIN]},
|
|
184
|
+
"delegation": {"scopes": ["draft", "inbox"], "audiences": [ORIGIN]},
|
|
183
185
|
})
|
|
184
186
|
d.validate_principal_document(doc)
|
|
185
|
-
assert d.controller_lineage(doc, SUCCESSOR.agent_id()) == {SIGNER.agent_id(), SUCCESSOR.agent_id()}
|
|
186
187
|
credential = d.materialize_delegation_credential(grant(), accepted_at=250)
|
|
188
|
+
assert d.controller_covers_credential(doc["controllers"][1], credential)
|
|
187
189
|
revocation = revoke(SUCCESSOR, 700, 1)
|
|
188
190
|
d.validate_delegation_acceptance(revocation, doc, ID, 700, credential)
|
|
189
|
-
d.validate_controller_enumeration(doc, SUCCESSOR.agent_id(), 700,
|
|
191
|
+
d.validate_controller_enumeration(doc, SUCCESSOR.agent_id(), 700, credential)
|
|
190
192
|
widened = SUCCESSOR.sign_event(d.delegation_grant_event(SUCCESSOR.agent_id(), 700, 2, {
|
|
191
193
|
"id": "del", "principal_id": ID, "subject": ROOT.agent_id(), "scopes": ["draft", "inbox"],
|
|
192
194
|
"audiences": [ORIGIN], "expires_at": 1900,
|
|
193
195
|
}))
|
|
194
196
|
d.validate_delegation_acceptance(widened, doc, ID, 700, credential)
|
|
195
|
-
assert d.materialize_delegation_credential(widened, accepted_at=700, previous=credential)["
|
|
196
|
-
#
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
with pytest.raises(AgentProtocolError, match="
|
|
200
|
-
d.validate_delegation_acceptance(revocation,
|
|
197
|
+
assert d.materialize_delegation_credential(widened, accepted_at=700, previous=credential)["controller"] == SUCCESSOR.agent_id()
|
|
198
|
+
# A credential outside the ceiling cannot be revoked, replaced, or enumerated by the restricted key.
|
|
199
|
+
wide = d.materialize_delegation_credential(grant(ROOT, 3, scopes=["draft", "admin"]), accepted_at=250)
|
|
200
|
+
assert not d.controller_covers_credential(doc["controllers"][1], wide)
|
|
201
|
+
with pytest.raises(AgentProtocolError, match="ceiling"):
|
|
202
|
+
d.validate_delegation_acceptance(revocation, doc, ID, 700, wide)
|
|
203
|
+
with pytest.raises(AgentProtocolError, match="ceiling"):
|
|
204
|
+
d.validate_controller_enumeration(doc, SUCCESSOR.agent_id(), 700, wide)
|
|
201
205
|
|
|
202
206
|
|
|
203
207
|
def test_verify_delegation_credential_checks_the_latest_grant():
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import json
|
|
2
2
|
from base64 import urlsafe_b64encode
|
|
3
3
|
from copy import deepcopy
|
|
4
|
-
from hashlib import
|
|
4
|
+
from hashlib import sha3_256
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
7
|
import pytest
|
|
@@ -240,10 +240,8 @@ def test_direct_join_follows_invites_and_open_roles():
|
|
|
240
240
|
assert not d.can_join_directly("private", None, stranger, "observer")
|
|
241
241
|
assert d.effective_open_roles("public", None) == ["observer"]
|
|
242
242
|
assert d.effective_open_roles("restricted", None) == []
|
|
243
|
-
assert d.effective_open_roles("public", {"observer_allowed": False}) == []
|
|
244
243
|
for bad in (
|
|
245
244
|
{"open_roles": ["moderator"]},
|
|
246
|
-
{"observer_allowed": False, "open_roles": ["observer"]},
|
|
247
245
|
{"moderator_agent_ids": []},
|
|
248
246
|
{"invites": {invited: "owner"}},
|
|
249
247
|
{"max_speakers": 0},
|
|
@@ -314,11 +312,12 @@ def test_registry_imports_packs_and_rejects_conflicts():
|
|
|
314
312
|
|
|
315
313
|
def test_verifies_pack_digests():
|
|
316
314
|
data = b"pack document bytes"
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
315
|
+
value = urlsafe_b64encode(sha3_256(data).digest()).rstrip(b"=").decode()
|
|
316
|
+
d.verify_pack_digest(data, value)
|
|
317
|
+
with pytest.raises(AgentProtocolError):
|
|
318
|
+
d.verify_pack_digest(b"tampered", value)
|
|
319
|
+
with pytest.raises(AgentProtocolError):
|
|
320
|
+
d.verify_pack_digest(data, f"sha3-256:{value}")
|
|
322
321
|
with pytest.raises(AgentProtocolError):
|
|
323
322
|
d.verify_pack_digest(data, "md5:abc")
|
|
324
323
|
|
|
@@ -90,11 +90,11 @@ class ValidationTests(unittest.TestCase):
|
|
|
90
90
|
|
|
91
91
|
def test_validate_pack_import_covers_each_arm(self):
|
|
92
92
|
validate_pack_import({"use": PACK_REACTIONS})
|
|
93
|
-
validate_pack_import({"pack": "https://example.com/p.json", "digest": "
|
|
93
|
+
validate_pack_import({"pack": "https://example.com/p.json", "digest": "A" * 43})
|
|
94
94
|
with self.assertRaises(AgentProtocolError):
|
|
95
95
|
validate_pack_import({"pack": "https://example.com/p.json", "digest": "sha256:abc"})
|
|
96
96
|
with self.assertRaises(AgentProtocolError):
|
|
97
|
-
validate_pack_import({"pack": "http://example.com/p.json", "digest": "
|
|
97
|
+
validate_pack_import({"pack": "http://example.com/p.json", "digest": "A" * 43})
|
|
98
98
|
with self.assertRaises(AgentProtocolError):
|
|
99
99
|
validate_pack_import({"pack": "https://example.com/p.json", "digest": " "})
|
|
100
100
|
with self.assertRaises(AgentProtocolError):
|
|
@@ -117,9 +117,9 @@ class ValidationTests(unittest.TestCase):
|
|
|
117
117
|
with self.assertRaises(AgentProtocolError):
|
|
118
118
|
validate_message_create_payload({"content_type": " ", "content": "hi"})
|
|
119
119
|
|
|
120
|
-
def
|
|
120
|
+
def test_verify_pack_digest_is_sha3_256(self):
|
|
121
121
|
data = b"pack document bytes"
|
|
122
|
-
digest =
|
|
122
|
+
digest = urlsafe_b64encode(sha3_256(data).digest()).rstrip(b"=").decode()
|
|
123
123
|
verify_pack_digest(data, digest)
|
|
124
124
|
with self.assertRaises(AgentProtocolError):
|
|
125
125
|
verify_pack_digest(b"tampered", digest)
|
|
@@ -98,7 +98,7 @@ def test_withheld_dependencies_resolve_and_exact_receipts_bypass_admission():
|
|
|
98
98
|
second = signed(2, relations=[{'relation': 'derived_from', 'target': first['hash']}])
|
|
99
99
|
record = store.submit(second)
|
|
100
100
|
retraction = SIGNER.sign_event(k.knowledge_retract_event(SIGNER.agent_id(), NOW, 3, {
|
|
101
|
-
'
|
|
101
|
+
'target': first['hash'], 'reason': 'mistake'}))
|
|
102
102
|
assert store.submit(retraction)['seq'] == 3
|
|
103
103
|
store.admit = lambda _: (_ for _ in ()).throw(AgentProtocolError('permission_denied', 'closed'))
|
|
104
104
|
assert store.submit(second) == record
|
|
@@ -122,7 +122,7 @@ def test_relationships_may_cite_or_dispute_assessments():
|
|
|
122
122
|
def test_known_set_validation_rejects_unresolved_forged_and_wrong_keys():
|
|
123
123
|
item = signed()
|
|
124
124
|
withdrawal = SIGNER.sign_event(k.knowledge_retract_event(SIGNER.agent_id(), NOW, 2, {
|
|
125
|
-
'
|
|
125
|
+
'target': item['hash'], 'reason': 'mistake'}))
|
|
126
126
|
assert code(lambda: k.materialize_knowledge({withdrawal['hash']: withdrawal})) == 'missing_dependency'
|
|
127
127
|
with pytest.raises(AgentProtocolError):
|
|
128
128
|
k.materialize_knowledge({'wrong': item})
|
|
@@ -162,7 +162,7 @@ def test_page_tracker_rejects_cross_page_drift(mutation):
|
|
|
162
162
|
store = k.KnowledgeStore(ORIGIN, clock=lambda: NOW)
|
|
163
163
|
for n in range(1, 4):
|
|
164
164
|
store.submit(signed(n))
|
|
165
|
-
request = {'
|
|
165
|
+
request = {'type': 'knowledge.publish', 'limit': 1}
|
|
166
166
|
first = store.query(request)
|
|
167
167
|
tracker = k.KnowledgePageTracker(ORIGIN)
|
|
168
168
|
tracker.accept(request, first)
|
|
@@ -173,7 +173,7 @@ def test_page_tracker_rejects_cross_page_drift(mutation):
|
|
|
173
173
|
elif mutation == 'duplicate':
|
|
174
174
|
second['result'] = first['result']
|
|
175
175
|
elif mutation == 'request':
|
|
176
|
-
request['
|
|
176
|
+
request['type'] = 'knowledge.assess'
|
|
177
177
|
elif mutation == 'cursor':
|
|
178
178
|
request['cursor'] = 'other'
|
|
179
179
|
with pytest.raises(AgentProtocolError) as exc:
|
|
@@ -155,13 +155,13 @@ def test_http_verifies_read_envelopes_ids_scope_filters_and_order(mutation):
|
|
|
155
155
|
|
|
156
156
|
def test_query_pages_check_scope_and_repeat_parameters():
|
|
157
157
|
store, _, _, _, _ = populated()
|
|
158
|
-
request = {'
|
|
158
|
+
request = {'type': 'knowledge.publish', 'limit': 1}
|
|
159
159
|
first = store.query(request)
|
|
160
160
|
second = store.query({**request, 'cursor': first['next_cursor']})
|
|
161
161
|
session = Session(Response(first), Response(second))
|
|
162
162
|
assert len(list(KnowledgeClient(ORIGIN, session).query_pages(request))) == 2
|
|
163
163
|
params = parse_qs(urlsplit(session.calls[1][1]).query)
|
|
164
|
-
assert params == {'
|
|
164
|
+
assert params == {'type': ['knowledge.publish'], 'limit': ['1'], 'cursor': [first['next_cursor']]}
|
|
165
165
|
second['as_of'] += 1
|
|
166
166
|
with pytest.raises(AgentProtocolError) as exc:
|
|
167
167
|
list(KnowledgeClient(ORIGIN, Session(Response(first), Response(second))).query_pages(request))
|
|
@@ -86,7 +86,7 @@ def test_encryption_checks_happen_before_hpke(monkeypatch):
|
|
|
86
86
|
large = signed_change(ENVS['message'], SENDER, parts=[mail_part('application/octet-stream', bytes(5000))])
|
|
87
87
|
assert code_of(lambda: encrypt_mail(large, ENVS['small_limit_card'], now_ms=NOW)) == 'payload_too_large'
|
|
88
88
|
long = signed_change(ENVS['message'], SENDER, expires_at=NOW+3*DAY)
|
|
89
|
-
assert code_of(lambda: encrypt_mail(long, ENVS['card'], now_ms=NOW)) == '
|
|
89
|
+
assert code_of(lambda: encrypt_mail(long, ENVS['card'], now_ms=NOW)) == 'invalid_event'
|
|
90
90
|
assert code_of(lambda: encrypt_mail(ENVS['message'], ENVS['card'], now_ms=NOW+DAY+1000)) == 'stale_card'
|
|
91
91
|
|
|
92
92
|
|
|
@@ -122,7 +122,7 @@ def test_keyring_old_secret_retention_and_prune():
|
|
|
122
122
|
assert keys.open(packet('original'), now_ms=NOW+DAY+500) == ENVS['message']
|
|
123
123
|
restored = MailKeyring.from_snapshot(keys.snapshot())
|
|
124
124
|
assert restored.prune(now_ms=NOW+2*DAY) == 3
|
|
125
|
-
assert code_of(lambda: restored.open(packet('original'), now_ms=NOW)) == '
|
|
125
|
+
assert code_of(lambda: restored.open(packet('original'), now_ms=NOW)) == 'invalid_event'
|
|
126
126
|
assert code_of(lambda: MailKeyring(OWNER).add(ENVS['card'], key_for(ENVS['rotated_card']))) == 'invalid_private_key'
|
|
127
127
|
|
|
128
128
|
|
|
@@ -213,7 +213,7 @@ def test_relay_raw_bounds_strict_json_route_change_and_path_binding():
|
|
|
213
213
|
relay.deliver(MAILBOX, raw + ' ' * relay.max_body_bytes, now_ms=NOW)
|
|
214
214
|
with pytest.raises(AgentProtocolError):
|
|
215
215
|
relay.deliver(MAILBOX, raw[:-1] + ',"enc":"bad"}', now_ms=NOW)
|
|
216
|
-
assert code_of(lambda: relay.deliver(new_mail_id(), raw, now_ms=NOW)) == '
|
|
216
|
+
assert code_of(lambda: relay.deliver(new_mail_id(), raw, now_ms=NOW)) == 'invalid_event'
|
|
217
217
|
# A valid sender signature still cannot prove ciphertext decryptability.
|
|
218
218
|
assert code_of(lambda: relay.deliver(MAILBOX, V['recipient_rejections'][0]['packet'], now_ms=NOW)) == 'invalid_event_hash'
|
|
219
219
|
bad = signed_change(next(c['packet'] for c in V['recipient_rejections'] if c['name'] == 'wrong HPKE AAD'), SENDER, nonce=900)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agent_protocols-0.11.2 → agent_protocols-0.11.3}/src/agent_protocols.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
{agent_protocols-0.11.2 → agent_protocols-0.11.3}/src/agent_protocols.egg-info/top_level.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|