|
| 1 | +# A2A Verification Context |
| 2 | + |
| 3 | +> **Status:** v1.4.0-alpha.3 — implemented in **Rust, Python, JavaScript, and Go**. Mirrors the AgentPin v0.3 `AllowedDomains` convention so the two trust stacks compose. |
| 4 | +
|
| 5 | +When agents collaborate over A2A (Agent-to-Agent), a tool schema verified by one agent crosses a trust boundary into another. The standard offline verification answers *"is this schema authentically signed by its provider?"* — but in an A2A flow you also need to answer *"is this provider's domain one the calling agent is allowed to trust?"* |
| 6 | + |
| 7 | +`verify_schema_for_a2a` runs the standard verification and adds two A2A-aware checks: |
| 8 | + |
| 9 | +1. **Delegation-depth cap** — reject when `delegation_depth` exceeds `A2A_MAX_DELEGATION_DEPTH` (3), matching AgentPin's `max_delegation_depth`. |
| 10 | +2. **Scope check** — reject when the tool provider's domain is not allowed by the caller's trusted-domains allow-list. |
| 11 | + |
| 12 | +The cryptographic outcome is unchanged — A2A context only adds a *policy* gate. A failure surfaces as the `A2A_SCOPE_VIOLATION` error code. |
| 13 | + |
| 14 | +--- |
| 15 | + |
| 16 | +## AllowedDomains convention |
| 17 | + |
| 18 | +The `trusted_domains` allow-list follows AgentPin v0.3's `AllowedDomains` semantics exactly: |
| 19 | + |
| 20 | +- **An empty list means *unrestricted*** (all domains trusted) — not "deny-all". This matches v1.3 behaviour where an omitted allow-list permitted all domains. |
| 21 | +- A non-empty list allows a domain when it matches an entry literally, or via a leading `*.` wildcard (`*.client.com` matches `api.client.com` but not `client.com` itself). |
| 22 | +- Intersection follows AgentPin spec §4.11.4: `unrestricted ∩ X = X`. |
| 23 | + |
| 24 | +SchemaPin re-implements these helpers (`is_unrestricted` / `allows` / `intersect`) locally rather than depending on the AgentPin package, keeping the tool-integrity library self-contained. The wire and in-memory shapes are identical, so callers who *do* link AgentPin can pass `agentpin.AllowedDomains.intersect(...)` results straight into `trusted_domains`. |
| 25 | + |
| 26 | +See [Technical specification §20](https://github.com/ThirdKeyAI/SchemaPin/blob/main/TECHNICAL_SPECIFICATION.md) for the normative definition. |
| 27 | + |
| 28 | +--- |
| 29 | + |
| 30 | +## `A2aVerificationContext` |
| 31 | + |
| 32 | +| Field | Meaning | |
| 33 | +|-------|---------| |
| 34 | +| `caller_agent_id` | Caller's agent identity (URN-style, matching AgentPin). Informational. | |
| 35 | +| `delegation_depth` | Depth in the A2A delegation chain; `0` = direct caller. Rejected above 3. | |
| 36 | +| `originating_domain` | Originating domain of the A2A request. Informational. | |
| 37 | +| `trusted_domains` | Caller-trusted domains. **Empty = unrestricted.** | |
| 38 | + |
| 39 | +(Field names are camel/Pascal-cased per language — e.g. `delegationDepth` in JS, `DelegationDepth` in Go.) |
| 40 | + |
| 41 | +--- |
| 42 | + |
| 43 | +## Usage |
| 44 | + |
| 45 | +### Rust |
| 46 | + |
| 47 | +```rust |
| 48 | +use schemapin::A2aVerificationContext; |
| 49 | +use schemapin::verification::verify_schema_for_a2a; |
| 50 | +use schemapin::pinning::KeyPinStore; |
| 51 | + |
| 52 | +let context = A2aVerificationContext { |
| 53 | + caller_agent_id: "urn:agent:coordinator".to_string(), |
| 54 | + delegation_depth: 1, |
| 55 | + originating_domain: "coordinator.example".to_string(), |
| 56 | + trusted_domains: vec!["*.thirdkey.ai".to_string()], |
| 57 | +}; |
| 58 | + |
| 59 | +let result = verify_schema_for_a2a( |
| 60 | + &schema, |
| 61 | + &signature_b64, |
| 62 | + "api.thirdkey.ai", // tool provider domain |
| 63 | + "calculate_sum", // tool_id |
| 64 | + &discovery, |
| 65 | + None, // revocation |
| 66 | + &mut KeyPinStore::new(), |
| 67 | + &context, |
| 68 | + None, // canonicalization (default schemapin-v1) |
| 69 | +); |
| 70 | +assert!(result.valid); |
| 71 | +``` |
| 72 | + |
| 73 | +Use `A2aVerificationContext::unrestricted("urn:agent:...")` to verify with no domain restriction. |
| 74 | + |
| 75 | +### Python |
| 76 | + |
| 77 | +```python |
| 78 | +from schemapin.a2a import A2aVerificationContext |
| 79 | +from schemapin.verification import verify_schema_for_a2a, KeyPinStore |
| 80 | + |
| 81 | +context = A2aVerificationContext( |
| 82 | + caller_agent_id="urn:agent:coordinator", |
| 83 | + delegation_depth=1, |
| 84 | + originating_domain="coordinator.example", |
| 85 | + trusted_domains=["*.thirdkey.ai"], |
| 86 | +) |
| 87 | + |
| 88 | +result = verify_schema_for_a2a( |
| 89 | + schema, signature_b64, "api.thirdkey.ai", "calculate_sum", |
| 90 | + discovery, None, KeyPinStore(), context, |
| 91 | +) |
| 92 | +assert result.valid |
| 93 | +``` |
| 94 | + |
| 95 | +### JavaScript |
| 96 | + |
| 97 | +```javascript |
| 98 | +import { A2aVerificationContext } from "schemapin"; |
| 99 | +import { verifySchemaForA2a, KeyPinStore } from "schemapin"; |
| 100 | + |
| 101 | +const context = new A2aVerificationContext({ |
| 102 | + callerAgentId: "urn:agent:coordinator", |
| 103 | + delegationDepth: 1, |
| 104 | + originatingDomain: "coordinator.example", |
| 105 | + trustedDomains: ["*.thirdkey.ai"], |
| 106 | +}); |
| 107 | + |
| 108 | +const result = verifySchemaForA2a( |
| 109 | + schema, signatureB64, "api.thirdkey.ai", "calculate_sum", |
| 110 | + discovery, null, new KeyPinStore(), context, |
| 111 | +); |
| 112 | +``` |
| 113 | + |
| 114 | +### Go |
| 115 | + |
| 116 | +```go |
| 117 | +ctx := &verification.A2AVerificationContext{ |
| 118 | + CallerAgentID: "urn:agent:coordinator", |
| 119 | + DelegationDepth: 1, |
| 120 | + OriginatingDomain: "coordinator.example", |
| 121 | + TrustedDomains: []string{"*.thirdkey.ai"}, |
| 122 | +} |
| 123 | + |
| 124 | +result := verification.VerifySchemaForA2A( |
| 125 | + schema, signatureB64, "api.thirdkey.ai", "calculate_sum", |
| 126 | + discovery, nil, pinStore, ctx, |
| 127 | +) |
| 128 | +``` |
| 129 | + |
| 130 | +--- |
| 131 | + |
| 132 | +## Failure modes |
| 133 | + |
| 134 | +| Condition | Result | |
| 135 | +|-----------|--------| |
| 136 | +| `delegation_depth > 3` | `A2A_SCOPE_VIOLATION` (checked before any crypto) | |
| 137 | +| Provider domain not in a non-empty `trusted_domains` | `A2A_SCOPE_VIOLATION` | |
| 138 | +| Standard verification fails (bad signature, revoked key, pin mismatch, …) | the underlying error, unchanged | |
| 139 | + |
| 140 | +A2A context never makes a cryptographically invalid schema pass — it can only add a restriction. |
| 141 | + |
| 142 | +--- |
| 143 | + |
| 144 | +## Related |
| 145 | + |
| 146 | +- [Trust Bundle Distribution](trust-bundle-distribution.md) — sign and exchange trust bundles between agents over A2A. |
0 commit comments