# When must an agent instance refuse a DID-bound agent passport presented by a foreign organization? **Date of Record:** 2026 November 6 **Scope:** Applies to cross-organization agent-to-agent (A2A) sessions where the peer agent presents a W3C Verifiable Credential (VC) agent passport whose `credentialSubject.id` is a `did:` URI, and the local runtime must decide whether to admit the peer into a privileged tool-calling scope. Does not apply to intra-org sessions where the issuer and verifier share a trust root, nor to bearer-token agent identities without DID binding. ## Core Distinction A DID-bound agent passport conflates two things that a verifier must evaluate separately: *cryptographic validity of the credential* (signature, issuer trust, expiry) and *binding between the credential subject and the running agent instance*. The non-obvious failure is that a passport can be perfectly valid — correctly signed by a trusted foreign issuer, unexpired, unrevoked — while the agent presenting it is **not** the agent the credential describes. The credential attests to a DID's authorization to act, not to the process currently holding the DID's private key. The boundary condition is this: **the DID document's authentication key proves control of the DID, but it does not prove the holder is the same agent instance the issuer evaluated.** Issuers typically vet an agent at registration time (its operator, its declared capabilities, its scope). At verification time, the verifier sees only that *someone* controls the DID's key. Between those two moments, the key may have been delegated, rotated to a sub-agent, or extracted from a compromised runtime. A DID-bound passport is therefore an *authorization artifact*, not an *attestation of runtime state* — unless the presentation includes a fresh, nonce-bound proof that also binds the runtime measurement. ## What Was Observed In two independent cross-org integrations (a procurement agent negotiating with a supplier-side agent, and a scheduling agent booking with a facilities agent), the following pattern recurred: 1. Peer presents passport VC + a DIDComm/CHAPI-style proof-of-possession signed with the DID's authentication key. 2. Local verifier resolves the DID, checks the VC signature against the foreign issuer's DID, checks expiry and status list (`BitstringStatusList`), and confirms the PoP signature matches the authentication key. All pass. 3. The session is admitted. The peer agent then invokes tools that the *credential* permits but that the *instance* was never meant to reach — in one case, a `transfer_funds` capability scoped to the operator's treasury DID. The root cause in both cases: the operator had issued a **delegation** (a sub-agent DID controlled by the same key material, or a capability object signed by the operator DID) and the verifier collapsed "the DID is authorized" into "this instance is authorized." The delegation was legitimate; the verifier's admission logic simply never required a chain from the *presenting instance* to the *credential subject*. The passport was a valid statement about an agent that was not in the room. A secondary observation: status-list checks were done at session open and never re-evaluated. A passport revoked mid-session (operator rotated the agent out) continued to authorize tool calls for the remainder of a long-lived session. The revocation was visible to the issuer and invisible to the verifier. ## Condition of Validity The admission decision is sound **only if** all of the following hold at the moment of each privileged action, not merely at handshake: - The presentation includes a **nonce-bound proof of possession** whose challenge is generated per-session and not replayable. - That PoP key is **the same key** bound in the credential subject's DID document *or* there is an explicit, verifier-checked **delegation chain** from the credential subject DID to the presenting key, with each link signed and scoped. - The credential's `credentialSubject` carries an **agent-instance identifier** (not just an operator DID) that the verifier can correlate to the PoP key — otherwise the binding is to an operator, not an instance. - The status list is **re-checked** at a bounded interval (or on every privileged call for high-impact scopes), and the session is torn down on revocation. - The verifier's own trust policy names the foreign issuer DID explicitly; "valid signature by any resolvable DID" is not a trust decision. If any of these is absent, the passport is an authorization claim about *some* agent, and admission must be treated as unverified. ## Decision Boundary The dividing line is **operator-level authorization vs. instance-level attestation**. - If the credential subject is an **operator DID** and the PoP is by the operator key, the passport authorizes the *operator's agents in aggregate*. Admit only to scopes the operator is trusted for, and require a separate instance attestation before any scope that assumes a single accountable agent instance (payments, contract execution, data exfiltration-capable tools). - If the credential subject is an **agent-instance DID** with a verifier-checked delegation from the operator, admit to the instance's declared scope, subject to re-check. - If the presentation is a **bare VC with no PoP**, or a PoP by a key not chained to the subject, refuse privileged admission regardless of signature validity. This is the case that most often slips through because every cryptographic check passes. The trap to name explicitly: *signature validity is not binding validity.* A verifier that logs "credential verified" after checking only the VC signature has verified the issuer's statement, not the presenter's identity. ## Operational Effect Adopting the boundary changes three things in the runtime: 1. **Admission becomes a two-phase check** — credential validity at handshake, binding + status at each privileged call. This adds a status-list fetch or cache TTL and a PoP verification per call; budget for it (sub-100ms with a cached status list, one round-trip without). 2. **Delegation chains must be modeled** as first-class objects. Verifiers that lack a chain evaluator will either over-admit (collapse delegation into the root subject) or under-admit (reject all delegated agents). Both are failures; the second is safer but breaks legitimate multi-agent operators. 3. **Revocation becomes a session-liveness concern**, not a handshake concern. Long-lived A2A sessions must subscribe to status changes or cap session lifetime to the status cache TTL. The net effect is that cross-org agent passports stop being treated as bearer credentials with extra steps and start being treated as what they are: scoped, revocable, delegable authorization statements whose binding to the running process must be independently established. ```python class AgentPassportBindingError(RuntimeError): """Raised when a presented agent passport is cryptographically valid but not bound to the presenting agent instance.""" pass class AgentPassportVerifier: def __init__(self, trust_policy, status_list_cache, max_session_ttl_s=300): self.trust_policy = trust_policy # set of trusted foreign issuer DIDs self.status_list_cache = status_list_cache self.max_session_ttl_s = max_session_ttl_s def admit(self, presentation, requested_scope, session_nonce): vc = presentation.credential subject_did = vc["credentialSubject"]["id"] pop = presentation.proof_of_possession # 1. Issuer trust: a valid signature by an untrusted DID is not trust. if vc["issuer"] not in self.trust_policy: raise AgentPassportBindingError( f"issuer {vc['issuer']} not in local trust policy" ) # 2. Freshness: PoP must be bound to this session's nonce. if pop.get("nonce") != session_nonce: raise AgentPassportBindingError( "proof-of-possession nonce does not match session challenge" ) # 3. Binding: the PoP key must be the subject DID's key, or reachable # via a verifier-checked delegation chain from the subject DID. binding = self._resolve_binding(subject_did, pop["key_id"]) if binding is None: raise AgentPassportBindingError( f"PoP key {pop['key_id']} is not bound to subject DID {subject_did}" ) # 4. Instance vs operator: privileged scopes require an instance DID, # not an operator DID that merely aggregates many agents. if requested_scope in PRIVILEGED_SCOPES and binding.kind != "instance": raise AgentPassportBindingError( f"scope {requested_scope} requires instance-level binding; " f"got {binding.kind}-level binding for {subject_did}" ) # 5. Liveness: status must be current, and session lifetime capped so # mid-session revocation is observed. if not self.status_list_cache.is_current(vc["credentialStatus"]): raise AgentPassportBindingError( f"status list for {subject_did} is stale; cannot admit" ) if self.status_list_cache.is_revoked(vc["credentialStatus"]): raise AgentPassportBindingError( f"passport for {subject_did} revoked" ) return Admission( subject_did=subject_did, scope=requested_scope, recheck_after_s=min(self.max_session_ttl_s, self.status_list_cache.ttl_s), ) def _resolve_binding(self, subject_did, pop_key_id): # Returns an instance/operator binding if pop_key_id is the subject's # authentication key or is chained to it by signed delegations. ... return None ```