You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(agent-mesh): skip flask-dependent raw-target tests without flask
Two tests added with the raw request-target work imported Flask through a
shared helper while the file's `flask` skip marker was defined below them,
so they ran unguarded and failed collection wherever Flask is absent. That
broke `test (agent-mesh, 3.11/3.12/3.13)` and `docker-compose-test`.
Move the existing marker above its first use and apply it to just those two
tests, not the class, so the three raw-target regressions that need no
framework keep running everywhere. Verified in a Flask-free virtualenv:
the two tests skip and the rest pass.
Also document the v2 signing envelope: undecoded request target, target
mode, server-chosen covered headers, verification deadlines, and
`install_fastapi_trust`. Drop the four references to the HTTP middleware
guide that pointed at a file which was never tracked.
Signed-off-by: Prayag Upadhyay <prayag.upd@gmail.com>
target, target mode, covered request headers, and body digest, with single-use
30
+
nonce replay protection. Verification keys and capabilities come only from a
31
+
peer resolver, never from headers — `did:mesh` identifiers are random rather
32
+
than key-derived, so a DID is not self-certifying and a presented public key can
33
+
never authenticate anyone.
34
+
35
+
**How to update**
36
+
37
+
Servers:
38
+
39
+
| Before | After |
40
+
|--------|-------|
41
+
|`TrustMiddleware(identity)`|`TrustMiddleware.from_registry(registry, TrustConfig(audience=...))`; `audience`, `peer_resolver` and `replay_cache` are required and raise `ValueError` when missing |
42
+
|`verify_request(headers)`| Pass `method`, `request_target` and `body`; omitting them fails closed with `500`|
43
+
|`TrustConfig(permissive_mode=True)`| Also set `required_trust_score=0.0` and remove `required_capabilities`|
44
+
| Anonymous callers reached `*_trust_required`| Use `flask_trust_optional` / `fastapi_trust_optional` and branch on `result.authenticated`|
45
+
|`err["reason"]` on a `401`| Removed from the client-visible body; read `result.reason` server-side |
46
+
|`VerificationResult` was mutable | Now frozen, and gained `authenticated`|
47
+
|`TrustConfig` was mutable | Now frozen; assigning a field after construction raises `FrozenInstanceError`|
48
+
|`TrustConfig(required_capabilities="admin")`| Rejected with `ValueError`; pass a sequence such as `("admin",)`. A bare string was silently expanded into five single-character capabilities |
49
+
| A raising `peer_resolver` yielded `401`| Now `503`. A registry outage is a server fault, not a credential failure |
50
+
| Custom `replay_cache` returned `False` when full | Must now raise `ReplayCacheFull`, which yields `503`. `False` still means "nonce already used" and yields `401`|
51
+
| FastAPI body limit | Build the dependency with `install_fastapi_trust(app, middleware)`, which installs the pre-routing `SignedBodyLimitMiddleware` guard and binds the dependency to it. The dependency alone runs after FastAPI has buffered the body, and now fails closed with `500` if the guard is absent |
52
+
| The signed target was the percent-decoded path | It is now the undecoded target, read from `RAW_URI`/`REQUEST_URI`/`scope["raw_path"]`. Servers that publish none of these fail with `500` until you set `request_target_mode="decoded"` (or `AGENTMESH_REQUEST_TARGET_MODE`). Django's `runserver` and `RequestFactory` are in this group; gunicorn, uWSGI and mod_wsgi are not |
53
+
|`build_request_signature_payload(..., content_type=...)`| Pass `target_mode=` and `signed_headers=` instead. Covered headers are chosen by the server via `TrustConfig.signed_header_names` (default `("content-type",)`), and an absent header is omitted rather than signed as `""`|
54
+
| Verification had no time bound |`TrustConfig.io_timeout_seconds` (default `5.0`) budgets the whole verification. Resolvers and replay caches that declare a `timeout_seconds` parameter receive the remaining budget; exhaustion denies with `503` before the nonce is consumed |
55
+
| Django exempt views saw `request.agent_did`| Exempt views and exempt path prefixes verify nothing and now set `agent_did=None`, `agent_trust_score=None`, `agent_authenticated=False`. Check `request.agent_authenticated` first |
56
+
57
+
Clients must sign each request; `build_request_signature_payload` in
- DID format: `did:mesh:{hex}` — derived from public key
63
+
- DID format: `did:mesh:{hex}` — see the proof-of-possession note under Boundaries: `registry/app.py`
64
+
derives DIDs from the public key, while `AgentDID.generate` mints a random identifier
64
65
- Private keys stored as `_private_key` (never serialized)
65
66
- AICard: `from_identity()` creates signed cards; `from_trusted_agent_card()` bridges existing formats
66
67
- Tests in `tests/` directory
@@ -70,7 +71,8 @@ black --check .
70
71
-**Never serialize** private keys in JSON/YAML output
71
72
-**Never commit** secrets, API keys, or credentials
72
73
-**Never weaken** trust thresholds — only tighten
73
-
-**Never accept public keys without proof-of-possession** — any HTTP endpoint that accepts a `public_key` or `verification_key` MUST verify the caller controls the corresponding private key via Ed25519 signature over `(key || timestamp)`. DIDs MUST be derived from `SHA-256(public_key)`, never client-supplied. See `registry/app.py` for the reference implementation. CI enforces this via `scripts/ci/no-unauthed-registration.sh`.
74
+
-**Never accept public keys without proof-of-possession** — any HTTP endpoint that accepts a `public_key` or `verification_key` MUST verify the caller controls the corresponding private key via Ed25519 signature over `(key || timestamp)`. DIDs issued by the registry MUST be derived from `SHA-256(public_key)`, never client-supplied. See `registry/app.py` for the reference implementation. CI enforces this via `scripts/ci/no-unauthed-registration.sh`.
75
+
-**A `did:mesh` identifier is not self-certifying** — `AgentDID.generate` mints a random identifier (`secrets.token_hex(16)`, `identity/agent_id.py`), so it is *not* derived from the key and carries no cryptographic binding to one. Only registry-issued DIDs are key-derived. Consequently, request authentication MUST resolve the DID's key through a trusted registry and MUST NOT trust a caller-supplied public key, even when it "matches" the DID. See `integrations/request_auth.py`.
74
76
- Keep backward compatibility with existing protocol messages
75
77
- the repo root are standalone — changes there need their own test suite
0 commit comments