Architecture
A connector for Claude that a bank issues to one customer. Its identity is three hardware keys; the customer binds it with a passkey; the bank attests it, gives it a mandate, and makes it a member of the bank's OpenID Federation - so a partner of the bank can trust it without ever having met it.
The pieces
This Mac The bank (local)
┌───────────────────────────────────────┐ ┌────────────────────────────────────────────────┐
│ Claude Desktop / Claude Code │ │ PingFederate 13.1.3 + pf-agentic-identity :9131 │
│ └─ connector (MCP, stdio) │── PAR ─────▶│ authorization server (PAR, PKCE, DPoP) │
│ ├─ SecureEnclaveSigner.app ──┐ │── token ───▶│ attestation filter: attest_jwt_client_auth │
│ └─ yk_signer.py (YubiKey) ───┤ │ │ federation intermediate + hosted agents │
│ keys: fed, inst, dpop │ │── enrol ───▶│ device-enrolment (Java) the Client Attester :7200│
│ browser: passkey, consent ◀─────────┘ │── sign in ─▶│ identity provider (passkeys) :7500 │
│ │── publish ─▶│ Trust Mark Issuer :7400 │
└───────────────────────────────────────┘── call ────▶│ the bank's API :7100 │
│ └────────────────────────────────────────────────┘
│ ┌──────────────────────────┐ ┌──────────────────┐
└── call ────────▶│ a partner (partner-rs) │ │ Trust Anchor │
│ :7300, trusts only the │─▶│ go-oidfed │
│ anchor's pinned keys │ │ lighthouse :8180 │
└──────────────────────────┘ └──────────────────┘
| Component | Where | What it is |
|---|---|---|
packages/connector |
this Mac, spawned by Claude | the MCP server: the agent's state machine, OAuth client, federation publisher. stdout is the MCP channel; everything else goes to stderr |
helpers/se-signer |
this Mac | SecureEnclaveSigner.app, a signed bundle: CryptoKit enclave keys, and App Attest on macOS 27. make bundle seals the connector and Node.js's own build inside it as ClaudeBankConnector.app, and the helper then serves only that connector |
helpers/yk-signer |
this Mac | a Python helper over yubikey-manager: keys in PIV slots 82, 83, 84 |
| PingFederate | Docker, https://localhost:9131 |
the bank's authorization server, federation intermediate and host of agent entities. Its RAR processor (au.idp.rar.FedRar) asks the PDP about every authorization_details entry of a mandate. Built from a worktree of pf-agentic-identity (exp/mac-connector) |
| device-enrolment | the Mac, :7200 |
the bank's Client Attester: verifies the passkey sign-in and the device evidence, mints attestations, registers the agent at PingFederate |
| bank-idp | :7500 |
the bank's identity provider for enrolment: passkeys (WebAuthn, rpId localhost), OIDC with PKCE. Stands in for PingOne |
| trust-mark-issuer | :7400 |
issues the bank's customer-agent Trust Mark, and answers its status |
| bank-api | :7100 |
the bank's own API: DPoP-bound tokens from its AS |
| partner-rs | :7300 |
a different member of the federation, and a federation entity in its own right. Its enforcement point configures itself by resolving its own entity - the anchor sets its PDPs, the federation's layer, the Trust Marks it requires, the purposes it accepts. Its only trust input is the anchor's public keys |
| pdp | :7600, :7650 |
a federation-aware AuthZEN PDP, and the federation's own policy layer. It resolves the agent and the resource itself and compares mission, purpose and grant (MISSION.md) |
| lighthouse | Docker, :8180 |
the Trust Anchor. PingFederate and the Trust Mark Issuer are its subordinates; it lists the issuer in trust_mark_issuers |
Three identities, never conflated
| Identity | Who | On the wire |
|---|---|---|
| Principal | the customer | the access token's sub |
| Agent instance | this connector, on this Mac | agent_id in the attestation and the token; act.sub = its federation entity, https://localhost:9131/federation/agents/<agent_id> |
| Agent type | the connector software the bank registered | OAuth client_id = claude-bank-connector; the agent's software_id in federation metadata |
One registered client serves every instance: the attestation says which instance is calling. Delegation, never impersonation - the token always names the customer as sub and the agent as the actor.
Three keys, never cross-used
| Key | Signs | Published as | Secure Enclave | YubiKey |
|---|---|---|---|---|
fed |
the agent's own Entity Configuration | jwks of that Entity Configuration, and of the bank's Subordinate Statement about it |
no gate, usable after first unlock | slot 82, PIN once |
inst |
every attestation PoP: client authentication | cnf.jwk of the bank's attestation |
Touch ID, only while unlocked | slot 83, PIN once, touch every time |
dpop |
DPoP proofs | nowhere; tokens carry its thumbprint in cnf.jkt |
no gate, usable after first unlock | slot 84, PIN once |
A software backend exists for automated tests only (CONNECTOR_ALLOW_SOFTWARE_KEYS=1) and is never selected by default.
Who trusts what
- The attester trusts the IdP's ID token (issuer, audience,
acr,auth_time, and anonceit recomputes over the challenge and both key thumbprints) and the device evidence it can verify. What it cannot verify, it does not claim. - PingFederate trusts attestations from the attester (a static attester key for now; the filter can resolve attesters through the federation instead) and binds each client to the attesters allowed to vouch for it. It checks the agent's standing as a hosted entity on every request.
- A partner trusts the anchor's pinned keys and nothing else: the bank's AS keys come from the bank's chain-validated metadata, its own enforcement configuration from its own resolved metadata, and every decision about the agent from PDPs that resolve the agent's chain, mission and Trust Mark themselves.
- The PDP trusts the anchor's pinned keys too: it takes nobody's word for what the federation says, including the enforcement point's.
- The connector trusts the bank's issuer and the anchor it was configured with;
bank_explain_trustresolves its own chain exactly as a partner would.
Revocation
| The bank does | Partners | The bank's AS | The bank's API |
|---|---|---|---|
| withdraws the Trust Mark | refuse on the next call: the federation's layer checks status every time | still accept | still accept |
| revokes the hosted entity | refuse on the next call: the layer finds no chain | refuse the next PAR, token or refresh | accept already-issued tokens until they expire (ten minutes) |
Choices worth knowing
- Attestation is client authentication, the mandate is an authorisation code. The attestation proves which software instance is calling; it is not a grant. The customer's approval is a FAPI 2.0-shaped PAR + PKCE + DPoP flow, and refresh tokens do not rotate.
- The agent signs its own Entity Configuration, the bank hosts it. A laptop has no public URL, so the bank serves the agent's Entity Configuration at
<entity>/.well-known/openid-federation- but the bytes are signed by the agent'sfedkey, and the bank checks that signature against the key it registered before it will publish them. - One mandate per audience. PingFederate ties a grant to one token manager, so the bank's API and the partner are separate approvals, each with its own refresh token and RFC 8707 resource.
- Mission and purpose are federation data. The agent declares its RAR types and DPV purposes, the bank vouches for it and caps both, each mandate states its purpose, and the PDP compares them - at the bank when a mandate is asked for, at the partner on every call (MISSION.md).
- Nothing that waits on a person runs inside a tool call. Enrolment and authorisation return a browser URL at once;
bank_statusreports progress.