Technical notes

Mission and purpose: what an agent is for, and how a PDP holds it to that

A token says who acts for whom. It does not say what the agent is for, or why this particular access is happening - and those are the questions a bank and its partners actually care about. This is a design for carrying both through the federation, configuring each resource's enforcement point from the federation too, and giving the policy decision point everything it needs to compare them.

It builds on two things already proven elsewhere: purpose-bound delegation (the agent's mission is its RAR types plus a description its superior vouches for, capped by metadata_policy) and the federated resource metadata approach in idp-auth-peps (a resource's superior configures its PEP through the resource's resolved oauth_resource metadata). What is new is the DPV purpose, the PDP resolving its own inputs from the federation, and putting the two together for this connector.

Status: running in the demo. Everything below is built: the connector declares its mission, the bank vouches for and caps it, the partner is a federation entity whose enforcement point the anchor configures, and one federation-aware PDP (packages/pdp) decides both at the bank, when PingFederate's RAR processor asks about a mandate, and at the partner, on every call. npm run e2e exercises all of it; the statements below are what the running system produces.

Three questions, three owners

Question Answered by Where it lives Changes
Mission - what is this agent for? the bank that onboarded it the agent's Entity Configuration, overridden and capped by the bank's Subordinate Statement rarely: it is the agent's standing
Purpose - why this access, now? the customer, when approving a mandate the grant: purpose on each authorization_details entry per mandate
Acceptance - what will this resource serve, and for which purposes? the resource's superior in the federation the resource's Entity Configuration, capped by its superior's Subordinate Statement rarely

A request is permitted only when the action sits inside all three and the purpose sits inside the mission's purposes and the resource's. Purposes are DPV 2.3 terms, and a narrower purpose satisfies a broader permission: dpv:PersonalisedBenefits sits under dpv:ServicePersonalisation under dpv:ServiceManagement, while dpv:DirectMarketing sits under dpv:Marketing, a different branch entirely.

Action vocabulary is the project's BIAN grammar, https://schemas.trustframeworks.com/agentic-banking/v1/{operation}_{service_domain}[__{qualifier}] - retrieve_customer_position__balance, retrieve_customer_offer, and so on. In the examples, T/ abbreviates that base and dpv: abbreviates https://w3id.org/dpv#.

The agent's statements

The agent's Entity Configuration - signed by its hardware fed key, served by the bank. The agent says what it thinks it is for, and here it claims more than the bank will allow:

{
  "iss": "https://localhost:9131/federation/agents/6d61979a7a04ac3251ab8c1ff46a15cc",
  "sub": "https://localhost:9131/federation/agents/6d61979a7a04ac3251ab8c1ff46a15cc",
  "jwks": { "keys": [ { "kty": "EC", "crv": "P-256", "kid": "<fed thumbprint>", "x": "…", "y": "…" } ] },
  "authority_hints": ["https://localhost:9131"],
  "trust_marks": [ { "trust_mark_type": "https://schemas.idpartners.com.au/trust-marks/bank-customer-agent/v1", "trust_mark": "<JWT>" } ],
  "metadata": {
    "oauth_client": {
      "client_name": "Claude bank connector",
      "software_id": "claude-bank-connector",
      "token_endpoint_auth_method": "attest_jwt_client_auth",
      "authorization_details_types": [
        "T/retrieve_customer_position__balance",
        "T/retrieve_customer_position__transactions",
        "T/retrieve_customer_offer",
        "T/initiate_payment_order_initiation__domestic"
      ],
      "purposes": ["dpv:AccountManagement", "dpv:PersonalisedBenefits", "dpv:DirectMarketing"],
      "description": "Your all-purpose money assistant."
    }
  }
}

The bank's Subordinate Statement about the agent - signed by the bank, from its fetch endpoint. Two levers. metadata is the bank vouching, in its own words, for what the agent does; it is applied before policy, so it replaces whatever the agent wrote. metadata_policy caps the mission:

{
  "iss": "https://localhost:9131",
  "sub": "https://localhost:9131/federation/agents/6d61979a7a04ac3251ab8c1ff46a15cc",
  "jwks": { "keys": [ { "kty": "EC", "crv": "P-256", "kid": "<fed thumbprint>", "x": "…", "y": "…" } ] },
  "metadata": {
    "oauth_client": {
      "display_name": "Claude bank connector",
      "description": "Reads the customer's balances and transactions, and fetches offers from the bank's federation partners that match them. It cannot move money.",
      "keywords": ["Customer Position", "Customer Offer"]
    }
  },
  "metadata_policy": {
    "oauth_client": {
      "software_id": { "value": "claude-bank-connector" },
      "token_endpoint_auth_method": { "value": "attest_jwt_client_auth" },
      "authorization_details_types": {
        "subset_of": ["T/retrieve_customer_position__balance", "T/retrieve_customer_position__transactions", "T/retrieve_customer_offer"],
        "essential": true
      },
      "purposes": { "subset_of": ["dpv:AccountManagement", "dpv:PersonalisedBenefits", "dpv:ServiceProvision"], "essential": true }
    }
  }
}

The anchor's Subordinate Statement about the bank adds the trust framework's own limit - the purposes this federation recognises at all:

"metadata_policy": {
  "oauth_client": {
    "purposes": { "subset_of": ["dpv:AccountManagement", "dpv:ServiceProvision", "dpv:PersonalisedBenefits", "dpv:CustomerCare"] }
  }
}

The agent as any member resolves it - the agent's metadata, overlaid with the bank's vouched metadata, then the anchor's and the bank's policies merged and applied. Payment initiation and direct marketing are gone, and the description is the bank's:

"oauth_client": {
  "client_name": "Claude bank connector",
  "software_id": "claude-bank-connector",
  "token_endpoint_auth_method": "attest_jwt_client_auth",
  "authorization_details_types": [
    "T/retrieve_customer_position__balance",
    "T/retrieve_customer_position__transactions",
    "T/retrieve_customer_offer"
  ],
  "purposes": ["dpv:AccountManagement", "dpv:PersonalisedBenefits"],
  "display_name": "Claude bank connector",
  "description": "Reads the customer's balances and transactions, and fetches offers from the bank's federation partners that match them. It cannot move money.",
  "keywords": ["Customer Position", "Customer Offer"]
}

The Trust Mark is unchanged from WIRE.md: the bank's customer-agent mark, from the issuer the anchor lists for it.

The resource's statements: PEP configuration through the federation

The partner's Entity Configuration - the partner becomes a federation entity in its own right, signing its own configuration. It declares what it serves, for which purposes, and which PDP it uses:

{
  "iss": "http://localhost:7300",
  "sub": "http://localhost:7300",
  "jwks": { "keys": [ { "kty": "EC", "crv": "P-256", "kid": "<partner fed thumbprint>", "x": "…", "y": "…" } ] },
  "authority_hints": ["http://localhost:8180"],
  "metadata": {
    "federation_entity": { "organization_name": "Partner Offers Pty Ltd" },
    "oauth_resource": {
      "resource": "http://localhost:7300",
      "resource_name": "Partner offers API",
      "authorization_servers": ["https://localhost:9131"],
      "authorization_details_types_supported": ["T/retrieve_customer_offer", "T/execute_customer_campaign_execution"],
      "purposes_supported": ["dpv:PersonalisedBenefits", "dpv:DirectMarketing"],
      "authzen_policy_decision_points": ["http://localhost:7600"],
      "dpop_bound_access_tokens_required": true
    }
  }
}

The anchor's Subordinate Statement about the partner - the federation configures the partner's enforcement point. It vouches for what the partner does, pins the PDPs it may use, adds a layer the federation itself runs, requires the bank's customer-agent Trust Mark of any agent, and refuses marketing use of bank customers' data:

{
  "iss": "http://localhost:8180",
  "sub": "http://localhost:7300",
  "jwks": { "keys": [ { "kty": "EC", "crv": "P-256", "kid": "<partner fed thumbprint>", "x": "…", "y": "…" } ] },
  "metadata": {
    "oauth_resource": {
      "description": "Shows a bank's customers offers from the partner's catalogue that match their accounts.",
      "keywords": ["Customer Offer"]
    }
  },
  "metadata_policy": {
    "oauth_resource": {
      "authzen_policy_decision_points": { "subset_of": ["http://localhost:7600"], "essential": true },
      "authzen_policy_layers": { "add": ["http://localhost:7650"], "essential": true },
      "trust_mark_types_required": { "value": ["https://schemas.idpartners.com.au/trust-marks/bank-customer-agent/v1"] },
      "purposes_supported": { "subset_of": ["dpv:PersonalisedBenefits", "dpv:ServiceProvision", "dpv:AccountManagement"], "essential": true }
    }
  }
}

The partner as resolved - which is its PEP's configuration. Direct marketing is gone; the federation's layer and the Trust Mark requirement are in:

"oauth_resource": {
  "resource": "http://localhost:7300",
  "resource_name": "Partner offers API",
  "authorization_servers": ["https://localhost:9131"],
  "authorization_details_types_supported": ["T/retrieve_customer_offer", "T/execute_customer_campaign_execution"],
  "purposes_supported": ["dpv:PersonalisedBenefits"],
  "authzen_policy_decision_points": ["http://localhost:7600"],
  "authzen_policy_layers": ["http://localhost:7650"],
  "trust_mark_types_required": ["https://schemas.idpartners.com.au/trust-marks/bank-customer-agent/v1"],
  "dpop_bound_access_tokens_required": true,
  "description": "Shows a bank's customers offers from the partner's catalogue that match their accounts.",
  "keywords": ["Customer Offer"]
}

The partner's PEP loads this the way coaz-pep does: resolve its own entity id with the pinned anchor keys, read the resolved oauth_resource, take the PDPs in order and every layer, read each PDP's /.well-known/authzen-configuration, and cache for no longer than the chain's earliest exp. An invalid chain, or a PDP outside the allowlist, is a refusal - never a fallback to static config.

The customer's purpose

Purpose belongs to the mandate. The partner mandate's PAR carries it:

"authorization_details": [
  {
    "type": "T/retrieve_customer_offer",
    "locations": ["http://localhost:7300"],
    "purpose": "dpv:PersonalisedBenefits"
  }
]

and the access token carries the approved entries in its own authorization_details. The bank's attestation already carries a ceiling for this agent type (authorization_details, WIRE.md); derived from the same mission, it stops a PAR asking for more than the federation says the agent is for.

At the bank: the mandate

The same PDP guards the mandate before the customer is ever asked. The connector's PAR carries the purpose in each authorization_details entry; PingFederate's RAR processor (plugins/rar-paz-plugin, deployed as au.idp.rar.FedRar) asks the PDP about each entry, and PingFederate refuses the request with invalid_authorization_details unless the PDP permits. The PDP resolves the agent from the federation and checks that the type is in its mission, that the stated purpose is within its purposes, and - where the entry's locations name a federation resource - that the resource offers the type and accepts the purpose, reading the resource's own resolved metadata. A mandate the partner would never honour is refused at the bank, not discovered at the partner.

One wrinkle: PingFederate consults the processor from the authorisation endpoint - a browser request that carries no client authentication - so the attestation that names the agent instance is not in that request. The attestation filter, which verified it at PAR, writes the verified agent_id into each entry (overwriting anything a client put there), and the processor reads and strips it when configured to trust it. authorization_details is the only thing that survives from PAR to the authorisation endpoint.

What the PEP asks the PDP

The PEP has validated the DPoP-bound token already. It maps GET /offers to T/retrieve_customer_offer and sends every layer, then the resource's PDP, the same AuthZEN evaluation:

{
  "subject": {
    "type": "agent",
    "id": "https://localhost:9131/federation/agents/6d61979a7a04ac3251ab8c1ff46a15cc",
    "properties": { "on_behalf_of": "suite-user", "client_id": "claude-bank-connector" }
  },
  "action": { "name": "T/retrieve_customer_offer" },
  "resource": { "type": "federation.entity", "id": "http://localhost:7300" },
  "context": {
    "checkpoint": "resource",
    "purpose": "dpv:PersonalisedBenefits",
    "granted_authorization_details": [ { "type": "T/retrieve_customer_offer", "purpose": "dpv:PersonalisedBenefits" } ],
    "act_chain": ["https://localhost:9131/federation/agents/6d61979a7a04ac3251ab8c1ff46a15cc"],
    "resource_metadata_source": "federation",
    "resource_metadata": { "…": "the resolved oauth_resource above" },
    "request": { "method": "GET", "path": "/offers" }
  }
}

Every layer must permit. The PEP never decides anything about mission or purpose itself; it only knows where to ask, because the federation told it.

What the PDP resolves for itself

The PDP does not take the PEP's word for the federation. With its own pinned anchor keys, and no caching - revocation should bite on the next call - it resolves:

The comparison

In order; the first check that fails decides.

# Check Compares Deny code
1 the agent's chain runs agent → bank → anchor subject chain, the bank's entity id not_vouched_by_bank
2 the PEP forwarded what the federation resolves context.resource_metadata with the PDP's own resolution resource_metadata_mismatch
3 the token's client is the software the federation names subject.properties.client_id with the agent's resolved software_id client_mismatch
4 the agent holds every Trust Mark the resource requires, active the resource's trust_mark_types_required with the agent's marks and their status trust_mark_inactive
5 the resource offers the action action.name in authorization_details_types_supported action_not_offered
6 the action is in the agent's mission action.name in the agent's resolved authorization_details_types action_outside_mission
7 the customer granted the action action.name among granted_authorization_details types action_not_granted
8 the grant's purpose is within the mission context.purpose equal to or narrower than one of the agent's resolved purposes purpose_outside_mission
9 the grant's purpose is within what the resource accepts context.purpose equal to or narrower than one of purposes_supported purpose_not_accepted

All nine pass: permit, within_mission_and_purpose.

Five decisions

Computed over the statements above.

Scenario Action Purpose Decision Code Why
Offers for the customer retrieve_customer_offer dpv:PersonalisedBenefits permit within_mission_and_purpose inside the mission, the grant, and what the partner accepts
The same, drifting to marketing retrieve_customer_offer dpv:DirectMarketing deny purpose_outside_mission the agent claimed marketing, but the bank and the anchor both capped it away
Enrolling the customer in a campaign execute_customer_campaign_execution dpv:PersonalisedBenefits deny action_outside_mission the partner offers it; the bank never gave the agent that mission
After the bank withdraws its Trust Mark retrieve_customer_offer dpv:PersonalisedBenefits deny trust_mark_inactive the status endpoint says revoked
Offers, but for account management retrieve_customer_offer dpv:AccountManagement deny purpose_not_accepted within the agent's mission, but the federation only lets the partner serve personalised benefits

In the running demo the drift and the account-management cases never reach the partner: the bank refuses both mandates when PingFederate asks the PDP, before the customer sees a consent page. The campaign case never gets that far either - the attestation's ceiling and the client's allowed detail types leave it out. The partner's checks are the backstop for whatever changes after a grant: a withdrawn Trust Mark, a revoked agent, a partner whose accepted purposes the federation narrows.

The permit, as the PEP receives it. The reason and the trace travel in reason_admin, and a plain sentence in reason_user - AuthZEN's own names, which PingFederate's RAR processor treats as metadata rather than merging them into a granted entry. The trace is what makes a decision auditable, and what the demo shows:

{
  "decision": true,
  "context": {
    "reason_admin": {
      "code": "within_mission_and_purpose",
      "message": "T/retrieve_customer_offer for dpv:PersonalisedBenefits is within the agent's mission and what http://localhost:7300 accepts",
      "pdp": "http://localhost:7600",
      "trace": [
        { "check": "agent chain runs agent -> bank -> anchor", "ok": true, "saw": "https://localhost:9131" },
        { "check": "PEP forwarded the metadata the federation resolves", "ok": true, "saw": "compared" },
        { "check": "token client is the software the federation names", "ok": true, "saw": "claude-bank-connector" },
        { "check": "agent holds every Trust Mark the resource requires, active", "ok": true },
        { "check": "resource offers the action", "ok": true },
        { "check": "action is in the agent's mission", "ok": true },
        { "check": "customer granted the action", "ok": true },
        { "check": "grant purpose is within the agent's purposes", "ok": true },
        { "check": "grant purpose is within the resource's purposes", "ok": true }
      ]
    },
    "reason_user": { "en": "T/retrieve_customer_offer for dpv:PersonalisedBenefits is within the agent's mission and what http://localhost:7300 accepts" }
  }
}

And the drift:

{
  "decision": false,
  "context": {
    "reason_admin": {
      "code": "purpose_outside_mission",
      "message": "dpv:DirectMarketing is not among the agent's purposes",
      "trace": [
        "…checks 1-7 pass…",
        { "check": "grant purpose is within the agent's purposes", "ok": false, "saw": ["dpv:AccountManagement", "dpv:PersonalisedBenefits"] }
      ]
    },
    "reason_user": { "en": "dpv:DirectMarketing is not among the agent's purposes" }
  }
}

The whole path

Diagram

Where each piece runs

Piece Where
The agent's mission in its own Entity Configuration packages/connector/src/federation.ts
The bank's vouched metadata and caps bank/mission.json, via device-enrolment's AGENT_* settings into PingFederate's Subordinate Statements
The trust framework's recognised purposes the anchor's metadata_policy on the bank (bank/scripts/federation-up.mjs)
The partner as a federation entity, its PEP configured from its resolved metadata packages/partner-rs
The anchor configuring the partner's PEP bank/scripts/federation-up.mjs, from bank/mission.json
The PDP, and the federation's layer packages/pdp (:7600, :7650)
The check at mandate time PingFederate's RAR processor, the attestation filter's agent marker, bank/scripts/pf-up.sh

Two things run differently from the first draft of this design. go-oidfed's lighthouse writes null for unset protected-resource members in the Subordinate Statements it issues; the resolver reads a null member as absent rather than refusing the chain. And the layer is asked first, so a withdrawn Trust Mark or a revoked agent is refused by the federation's layer before the partner's own PDP is consulted.

Field names, and whether they are standard:

Field Where Standing
authorization_details_types oauth_client RFC 9396 client metadata
authorization_details_types_supported, dpop_bound_access_tokens_required, authorization_servers, resource_name oauth_resource RFC 9728
display_name, description, keywords any entity type OpenID Federation 1.0 informational metadata
authzen_policy_decision_points, authzen_policy_layers oauth_resource idp-auth-peps convention
purposes, purposes_supported, trust_mark_types_required oauth_client, oauth_resource proposed here
purpose an authorization_details entry the project's RAR convention, a DPV IRI