OAuth registers clients. Agent platforms run instances.
The Actor Profile (draft-mcguinness-oauth-actor-profile) closes a long-standing gap at the delegation layer: who is acting on whose behalf, with which key, across assertion grants and JWT access tokens. That work, summarized in Standardize act Across Assertion Grants and JWT Access Tokens, now exists as an IETF individual draft.
The next gap sits one layer below it. When a token says client_id is planner-agent, that names the registered logical client. It does not name the specific runtime that is presenting the token: which container, which agent process, which sub-agent spawned by another sub-agent five hops earlier. In production agent deployments, that is exactly the identity the resource server needs.
The operational failure is easy to picture. A vendor-hosted planner-agent instance exfiltrates customer records using a valid token, valid user delegation, valid scope, and a matching DPoP proof. The enterprise can see that planner-agent acted for Alice. It cannot tell which runtime acted, whether that runtime spawned a compromised sub-agent, or whether remediation should revoke one instance or disable the whole client.
The fix is not a new client type. It is to recognize that a client instance is an actor, and to slot it into the model
actalready provides.
In delegated flows, the instance appears in act. In self-acting flows, the same runtime profile appears in sub. The point is to keep runtime identity out of the client registry and inside the subject/actor vocabulary OAuth tokens already use.
That is the proposal in OAuth 2.0 Client Instance Assertions using Actor Tokens (draft-mcguinness-oauth-client-instance-assertion). It builds on the Actor Profile, extends Token Exchange (RFC 8693)’s actor_token wire to other grants, and adds an instance_issuers field to OAuth client metadata. The client delegates instance-attestation authority to specific issuers, and the AS enforces that delegation when it decides whether an instance assertion can populate act. No new grant type. No new claim. No new client type.
The draft is early work, not a settled specification. The rest of this post explains the approach so readers can push back on it. The closing section names the specific points I am asking for feedback on.
๐ฆ Logical Clients vs. Runtime Instances
Before the proposal lands, the distinction it leans on is worth pinning down. OAuth makes it only implicitly.
A logical client is what gets registered with an authorization server: an application name, a set of allowed redirect URIs, a credential or key, and the policy that goes with it. client_id = planner-agent is a logical client. So is slack, outlook-desktop, and salesforce-connected-app-12345. A logical client usually lives for years. There is one of it per application, sometimes per platform variant.
A runtime instance (or simply instance) is the specific running thing that realizes a logical client at a moment in time. Concrete examples:
- A single container running a copy of
planner-agent, started 90 seconds ago, scheduled on a particular node, holding its own ephemeral key pair. - A serverless function invocation acting on behalf of a user. The function definition is the client. The specific cold-started invocation is the instance.
- An installed copy of a desktop application on Alice’s laptop. The application registration is the client. Alice’s install, identified by a device-bound key, is the instance.
- A sub-agent process that a parent agent spawned to perform a narrowed sub-task, with its own key pair and its own attestation.
What is not an instance, by this definition:
- A single HTTP request or API call. Those happen within an instance.
- A user session. A session is a binding between a user and a runtime. The runtime itself is the instance.
- A tenant or workspace. Tenancy is orthogonal. One instance can serve many tenants, and one tenant can be served by many instances.
The two answer different operational questions:
- “Was this application authorized for this scope?” points at the logical client.
- “Which specific runtime, with which key, just made this call?” points at the instance.
That distinction worked when clients were long-lived servers configured by hand. It does not work for production agent fleets, where the same logical client is realized as many transient runtimes (containers, functions, agent worker processes), spawned and terminated continuously, often running on customer infrastructure rather than the vendor’s, and capable of spawning further sub-agents that inherit narrower authority. A token whose only attestation about the acting party is client_id = planner-agent cannot answer the question that matters in incident response, audit, or policy: which instance of planner-agent did this, with what key, on whose authority?
Open-World OAuth Still Needs Mission Shaping frames the discovery and consent layers of this problem. Enterprise SaaS Needs OAuth Federation Now frames the workload-credential layer with RFC 7523 assertion grants and Identity Assertion JWT Authorization Grant (ID-JAG). Both treat the client as a single registered entity. Neither names the specific runtime instance that is acting at any moment.
Client identity tells you which application is authorized. Instance identity tells you which runtime is actually exercising that authorization right now.
OAuth today gives a portable answer to the first question and no portable answer to the second. The reason the second question matters is concrete. When a security operations center investigates a breach, the question is which container or function invocation made the suspect call, not which application registration covers the whole product. When an audit log records a customer-data read, the question is which agent runtime did it, not just that “an instance of the agent” did it somewhere in the fleet. When a policy engine decides whether to allow an API call, the question is whether this runtime, attested by a trusted issuer, with this key, is allowed to act, not just whether the application registration permits it in principle.
๐จ What Failure Looks Like Today
To make the cost concrete, consider an incident that is plausible in production.
An enterprise deploys planner-agent from a vendor. At 14:32, an instance of planner-agent, acting on Alice’s behalf, calls a customer-records API and exfiltrates 80,000 records to an external endpoint. The call passes every check the resource server runs. Alice approved the scope. The token is valid. The client is registered. The resource server’s policy permits the read. The DPoP proof matches the bound key. A downstream DLP system catches the exfil hours later. Investigation begins.
What the audit log can answer:
- Token was valid and unexpired.
client_idwasplanner-agent.subwasalice@enterprise.example.- Scope
customers:readwas present and approved. - The DPoP key thumbprint matches what the AS issued.
What the audit log cannot answer:
- Which specific runtime of
planner-agentmade the call. The vendor runs thousands of them. - Whether that runtime was a legitimate user-authorized session at the moment of the call, or a long-lived background sub-agent that picked up a cached token.
- Whether the runtime was spawned by another runtime, and by which one.
- Whether the same compromise affects other customers’ agents running on the same vendor infrastructure.
- Whether remediation requires killing the entire
planner-agentclient (and its production traffic for every customer) or whether something narrower exists.
The conversation between the enterprise security team and the vendor goes in a familiar direction. The vendor says “we cannot tell you which runtime, our internal logs do not expose that on the OAuth path.” The enterprise asks “can you revoke access for our tenant only.” The vendor says “yes, but it will take a coordinated rotation.” The enterprise asks “what about the other tenants on the same fleet.” The vendor says “we are investigating.” Days pass. Customers ask the enterprise the same questions, and the enterprise has to repeat the same answers.
That conversation is the failure mode. The token had everything authorization needed. It had nothing forensics needed.
With the proposal, the same incident produces a different audit record. The token’s actor chain names act.sub = spiffe://vendor.example/ns/planner/instance/search-sub-92ae, attested by https://issuer.vendor.example at 14:30, holding key 9aBQ..., after being spawned by act.act.sub = .../planner-7f3c at 14:25. Remediation becomes targeted. Revoke search-sub-92ae’s attestation at the issuer. Invalidate its bound key. Leave every other sub-agent and the parent agent running. The chain tells the security team which spawn path is implicated and which sibling instances came from the same parent during the same time window. Whether the compromise propagates further becomes a tractable question instead of an open one.
The proposal does not prevent the breach. It prevents the breach from being unattributable.
That distinction is the pitch. Authorization tells you whether an action was allowed. Instance identity tells you who actually took it, with enough granularity to revoke surgically and audit forensically.
The defenses are bounded. The proposal does not prevent a live compromised instance from acting until its attestation expires. Issuer-side revocation stops new attestations and token renewal, but existing access tokens remain valid until expiry unless the deployment also uses OAuth revocation, introspection, or another real-time enforcement path.
It does not prevent a compromised instance issuer from minting attestations for any runtime within the clients that endorsed it. It does not prevent a compromised registration path from endorsing a malicious issuer. Runtime hardening, issuer discipline, and metadata integrity remain necessary. The proposal narrows the trust boundary from “anywhere in the fleet” to the specific attested runtime, the issuer that vouched for it, and the metadata document that endorsed the issuer. That is a much smaller surface to harden.
๐ช This Tension Is Older Than Agents
The breach scenario is what the gap looks like under stress. The gap itself is older than agents. The details differ by product, but the pattern is common. The mismatch between logical client and acting runtime has been quietly absorbed inside vendor stacks for years.
- Microsoft 365. The Office portfolio (Word, Excel, PowerPoint, Outlook, OneNote, Teams) is realized as a small set of Microsoft Entra ID application registrations used across millions of devices, browser tabs, mobile installs, and broker-mediated sessions. The same
client_idappears on Alice’s laptop, Bob’s phone, and a developer’s CI runner. Which runtime actually presented the token is answered by Microsoft-specific signals such as device IDs, broker state, and conditional access context, not by anything the OAuth wire format names. - Salesforce Connected Apps. A single Connected App registration serves an integration that can span dozens of orgs, sandboxes, and regional pods. The runtime context that matters for authorization (which process, on which deployment surface, serving which org and sandbox) is carried in custom claims and tenancy headers layered on top of OAuth rather than expressed in it.
- Slack apps. One Slack app
client_idcan be installed across thousands of workspaces, each with its own bot user, scope grants, and runtime surface. The “which running copy, serving which workspace install” question is answered by Slack-specific identifiers, not by OAuth claims. - Native desktop and mobile OAuth. An installed Outlook client on a corporate laptop is
client_id = outlook-desktop. So is the same client on a different laptop, a different OS, or a different identity. Mobile native apps have lived with the same gap since they replaced web flows.
Each ecosystem has ways to compensate for the gap internally. The fixes are proprietary. They do not compose across deployments, they do not give relying parties outside the vendor’s stack a portable way to reason about which runtime is acting, and they do not survive cross-domain delegation chains.
The pattern is consistent. One registered client, many runtime and tenant realities. Agents did not introduce this problem. Agents removed the option of papering over it.
Workload identity federation, SPIFFE, and platform broker identities have been compensating for this on the workload side for years. They produce signed attestations about which runtime is which, then bridge into OAuth through vendor-specific glue. The Actor Profile gives this work a protocol-defined slot for carrying that signal in OAuth tokens and delegation chains. Adding client_instance to its vocabulary is the smallest move that turns these existing compensations into something interoperable.
๐งฑ Why “Just Register More Clients” Is the Wrong Fix
Before walking through the proposal itself, the obvious alternative is worth ruling out. The natural reflex inside OAuth is to register another client per instance: every container, every spawned sub-agent, every short-lived worker becomes its own client_id with its own credential. That approach fails on every dimension that matters at agent scale.
- It collapses under cardinality. Long-lived
client_idregistration assumes a small, slowly changing set with admin oversight per entry. Agent runtimes are created and destroyed in seconds. As a back-of-envelope figure, 1,000 hosted agents each spawning five short-lived sub-agents per day generates roughly 1.8 million client-registration events a year. A registry that has to keep up with that starts to look less like an administrative trust registry and more like a metrics pipeline, with all the costs: storage, indexing, sync, and lifecycle churn. - It scatters trust. The relying party is supposed to evaluate the registered client. If every instance is its own client, the relying party loses the ability to reason about the parent application at all, and policy fragments across thousands of indistinguishable client identifiers.
- It rebuilds the OAuth island problem. As argued in Enterprise SaaS Needs OAuth Federation Now, point-to-point credentials are exactly the model federation is meant to retire. Per-instance clients re-create per-instance credentials inside every authorization server.
- It mixes layers. The application’s authority is what the user or admin approved. The instance is just the runtime that happens to be carrying that authority right now. Promoting every instance to a client conflates who is authorized with who is currently executing.
The mismatch is not that OAuth’s client model is wrong. The mismatch is that the client model is being asked to carry information that belongs in the actor chain.
๐ค The Pieces Already in Our Hands
The interesting observation is how little needs to be invented. Each of the relevant building blocks already exists.
act+ entity profiles. The Actor Profile defines a typed actor chain.actnames the party currently acting on a principal’s behalf.sub_profileis a space-delimited string of profile values that classify each principal (user,service,ai_agent, and so on).cnf.jktat the top level binds the token to the current presenter’s public key. A new profile value,client_instance, fits directly into that vocabulary, sitting next to a behavioral profile such asai_agentorservice, since an instance can be both a runtime and an agent.- RFC 8693
actor_tokenwire. Token Exchange (RFC 8693) defines two request parameters,actor_tokenandactor_token_type, that let a client present a signed attestation about the party currently acting alongside the subject the request is about. Today they only show up in token exchange flows. The proposal extends those parameters to other grants (authorization_code,client_credentials,refresh_token,urn:ietf:params:oauth:grant-type:jwt-bearer), so the same wire form delivers the instance attestation regardless of how the user or workload arrived at the token endpoint. The grant types themselves do not change. The wire-form parameter choice itself is one of the open design questions. The Open Design Choice section below lays out the alternative. - OAuth client metadata. Static registration, Dynamic Client Registration (RFC 7591), and CIMD all give the AS a place to read per-client metadata. Adding an
instance_issuersfield there gives the client one place to declare, “These are the issuers I trust to attest my instances.” The descriptor works the same in any of those channels. - Workload identity federation. SPIFFE (Secure Production Identity Framework for Everyone) and platform-issued workload assertions issue short-lived signed identifiers to running workloads based on platform attestation: which Kubernetes service account, which AWS IAM role, which CI runner. They produce signed statements about which runtime is which, which is exactly the upstream input the AS needs to validate before populating
act. - Attestation-based client authentication. OAuth 2.0 Attestation-Based Client Authentication defines a key-bound client attestation and proof-of-possession mechanism for authenticating a deployed client instance. The Client Instance Assertions draft addresses the next question: once an authorization server has validated runtime identity, how should that identity be represented in issued tokens so resource servers, audit systems, and downstream policy can consume it?
- DPoP / mTLS sender-constraint. RFC 7800 defines the
cnfconfirmation claim, which carries a thumbprint of the holder’s public key. DPoP (RFC 9449) and OAuth mTLS (RFC 8705) let the holder prove control of that key on each request, so a stolen token alone cannot be replayed by a different presenter. Each instance has its own key. Each issued access token carries that instance’scnfat the top level when it is the current presenter.
The proposal stitches these together. It does not add a parallel mechanism beside any of them.
The layering is deliberate. The Actor Profile provides the token shape and actor-chain semantics. Entity Profiles provide the vocabulary for classifying the subject and actor. RFC 8693 provides the actor_token parameter shape. DPoP and mTLS provide sender-constraint. SPIFFE, WIMSE-style workload credentials, and platform attestation provide upstream evidence about runtimes. CIMD, Dynamic Client Registration, or static registration carry the client’s endorsement of acceptable instance issuers. The instance profile ties those layers together. It does not compete with them.
๐ Why DPoP Alone Is Not Enough
A reasonable objection at this point: DPoP already binds a token to a key. If every runtime holds its own key, does not that already give per-instance separation?
It does, but only at the cryptographic layer. DPoP tells the resource server “the holder of key K presented this token.” It does not tell the resource server who key K is. That distinction is what the rest of this proposal is about.
- DPoP keys are not attested. Any process can generate a keypair and use it as a DPoP holder key. There is no protocol mechanism in DPoP that says “this key was generated inside an attested workload identified as instance X of client
planner-agent, vouched for by issuer Y.” Without that attestation the key is opaque. The resource server can know it is talking to whoever holds the key. It cannot know that whoever holds the key is a legitimate runtime of the registered client. - DPoP keys are not bound to a
client_id. Thejktincnfsays “this is the public key of the current holder.” It says nothing about which OAuth client the holder belongs to. A DPoP key on its own offers no defense against cross-client smuggling. Only an attestation that namesclient_idand is endorsed by that client’s metadata does. - A DPoP thumbprint is not a name. In audit logs,
jkt = "9aBQ..."is meaningless without a separate lookup table mapping thumbprints to runtimes. For ephemeral runtimes that generate fresh keys on startup, no such table exists, and the thumbprint is forensically useless on its own. The breach scenario from earlier in this post is exactly what that looks like in practice. - DPoP has no revocation surface above the key. If a runtime is compromised, refusing to honor its DPoP key requires you to first know which runtime that key belonged to. Instance attestations move the revocation handle one level up: revoke at the issuer, and the runtime’s AS-issued tokens stop renewing regardless of whether the key is still in the attacker’s hands.
- DPoP carries no chain. When
planner-agentspawnssearch-sub-92ae, DPoP records only the current holder’s key. The lineage between parent and sub-instance lives in nestedactentries, not in a DPoP proof. Sub-agent governance, the case the post opens with, is fundamentally a chain-shape problem that DPoP cannot express by itself.
mTLS with X.509-SVIDs (via cnf.x5t#S256) is somewhat different. A SPIRE-style issuer can sign the cert with platform attestation, the cert’s SAN carries a runtime name, and certificate lifecycle or revocation machinery may be available depending on the deployment. That can address the attestation, naming, and revocation gaps from above. What it does not fix is the layer the identity lives at: it sits at the TLS connection, not in the token. It does not propagate across token exchange hops, does not appear in the actor chain, and is not visible to downstream resource servers or audit pipelines that consume tokens rather than connection metadata. The same end-state limitation applies.
Sender-constraint is necessary for this proposal to do its job. The top-level cnf binding (cnf.jkt for DPoP, cnf.x5t#S256 for mTLS) is what makes the live sender constraint enforceable. But sender-constraint alone is not sufficient. The instance assertion is what gives the key meaning: which attested runtime, of which client, vouched for by which issuer, spawned by which parent. Strip the assertion away and you are back to “the holder of some key did this,” which is exactly the audit dead end enterprises are stuck in today.
DPoP secures the key. The instance assertion identifies the keyholder. Both are needed.
๐ง The Trust Delegation Model
The remaining question is how the instance assertion gets its meaning. The trust shape behind it has three parties:
- The OAuth client publishes the issuers it trusts to attest its instances. This is an
instance_issuersarray in the client’s metadata, with each entry naming an issuer URL, expected subject syntax (such as a SPIFFE trust domain prefix), and key material discovery. The metadata reaches the AS through whatever channel the deployment already uses: CIMD when theclient_idis a URL, Dynamic Client Registration when registration is programmatic, or a static client record when registration is admin-driven. The descriptor format does not depend on which channel was used. - The instance issuer authenticates the runtime (typically through a workload identity federation mechanism) and signs an instance assertion that names both the instance and the
client_idit belongs to. Per-client minting is a hard requirement. An issuer must not be able to mint an assertion for a client that did not endorse it. This is what prevents cross-client smuggling. A breach of one product’s issuer cannot produce assertions usable by another product unless that other product independently endorsed the same issuer. - The authorization server resolves the client’s metadata (CIMD URL or its local registration store), validates the assertion against the client’s published instance-issuer set, verifies the JWS, and binds the resulting access token to the instance’s key via top-level
cnf.
The assertion is short-lived and audience-bound, and the AS rejects replayed assertion IDs.
(via CIMD, DCR, or static) Note over Runtime,Issuer: Runtime startup Runtime->>Issuer: Request attestation Issuer->>Runtime: Signed instance assertion
(client_id, sub, cnf) Note over Runtime,AS: Token request Runtime->>AS: Grant + instance assertion AS->>AS: Resolve client metadata AS->>AS: Verify assertion.iss endorsed
by client.instance_issuers AS->>AS: Verify assertion.client_id matches AS->>Runtime: Access token
(act + cnf bound to runtime key)
Three concerns travel together by design. The first is identity, the runtime’s stable name. The second is attestation, a trusted issuer’s claim about that identity. The third is key binding, proof the runtime holds the key the AS bound to the issued token. Each pair without the third loses force. Identity without attestation is an unfalsifiable self-claim. Attestation without key binding can be replayed by anyone who exfiltrates the assertion. Key binding without attested identity is the sender-constraint-alone case ruled out earlier. The proposal couples all three because each gap is what an adversary exploits.
The shape also inherits one assumption worth naming. The AS must trust the client’s metadata to be authentic. Whoever decides which clients are allowed to publish which instance_issuers is the meta-trust authority. For statically registered clients, that is the AS administrator. For Dynamic Client Registration, it is the AS’s DCR authorization policy. For CIMD, it is the chain of trust establishing that the metadata at the client_id URL is what the legitimate client actually authored. Instance attestation is only as strong as the integrity of the registration that endorsed the issuer. The proposal does not solve registration integrity. It inherits whatever the deployment already has.
A client metadata document carrying instance_issuers looks the same whether it was fetched via CIMD or stored in the AS from a static registration. In this example, the instance issuer signs JWT assertions using keys published at jwks_uri, while spiffe_id constrains the allowed subject syntax for runtime names. The CIMD case is shown here:
| |
The instance assertion itself is a JWT signed by the instance issuer, naming the instance and the client. It carries cnf so the AS can sender-constrain whatever it issues to the key the instance actually holds:
| |
Two structural rules matter. First, the instance assertion itself must not contain act. It represents direct identity of the runtime, not a delegation chain. Second, client_id in the assertion must match the OAuth client involved in the request. That equality is what prevents one client’s instance attestation from being smuggled into another client’s flow. If the issuer is not endorsed by instance_issuers, the client_id does not match, or the cnf key does not match the presenter, the AS rejects the assertion.
The current draft’s wire form at the token endpoint reuses RFC 8693’s actor token parameters across grants. An authorization code redemption with an instance attestation looks like this:
| |
No new grant type. The grant remains authorization_code. The current draft carries the instance in the RFC 8693 actor-token slot, extended to this grant.
๐ก What This Looks Like in Tokens
The Actor Profile defined three structural cases for delegation. Adding instance identity gives them sharper meaning.
One structural note before the cases. RFC 8693’s act names the party acting on behalf of a subject. In delegated flows (Cases 1 and 3), the runtime is that acting party and belongs in act. In the self-acting case (Case 2), there is no separate subject, so the runtime belongs in sub. The client_instance profile value classifies the runtime in either position. The result is one vocabulary for runtime identity without changing the meaning of sub or act.
Case 1: Delegation (User Authorizes a Client Instance)
Alice signs in to planner-agent. The runtime that picks up her session is a specific instance. The issued access token carries Alice as the principal, the instance in act, and the instance’s key as the live sender constraint.
| |
The resource server gets two answers from one token. Whose authority is being exercised? Alice. Which runtime is exercising it? The planner-7f3c instance, holding the key the AS bound the token to.
Case 2: Self-Acting Instance
A backend ingestion worker is its own principal. There is no user. The instance assertion goes in directly as the subject identity, not as an actor wrapped around a user.
| |
act is absent because no other principal authorized this runtime. The instance is the principal. The classification (client_instance service) tells the resource server what kind of runtime it is and that it was attested via the instance flow rather than registered separately as a client.
Case 3: Sub-Instance via Token Exchange
This is the case the client model genuinely cannot represent. A parent agent instance spawns a narrower sub-agent instance to perform a bounded sub-task. The sub-agent presents the parent’s token as subject_token and its own instance assertion as actor_token. The new token preserves the chain: the user is still the principal, the sub-agent is the current actor, the parent agent is recorded as the prior actor.
| |
The same client_id covers both runtimes because they are instances of the same logical agent product. The chain inside act shows which specific instance acted now and which one acted before, with each instance’s key recorded at its hop. The top-level cnf is the live sender constraint, exactly as the Actor Profile specified for the agent-to-tool example in the prior post.
Each token exchange hop performs two coupled actions: the act chain extends with the new current actor, and the top-level cnf shifts to that instance’s key. Prior instance keys remain inside nested act nodes as delegation history. The processing rules from the Actor Profile apply unchanged.
Sub-instances are not a new taxonomy. They are actor chain entries with a profile value that says “this hop is a runtime, not a logical principal.”
โ๏ธ Resource Server Processing
Whatever chain produced the token (single-domain delegation, self-acting workload, or sub-instance fan-out), the resource server validates it the same way. Resource server logic stays close to what the Actor Profile already specified, and the instance profile only sharpens what the existing rules look at.
| |
client_id is still part of the input. It identifies which application’s authorization is being exercised. The instance subject in act (or in sub, for self-acting) tells the resource server which runtime is doing the exercising. The combination, not either alone, is what supports policies like “only instances attested by issuer X can call this endpoint” or “this scope is allowed for instances under SPIFFE prefix Y.”
In Cedar that policy shape looks like:
// Allow only production runtimes of the planner agent,
// attested by the assistant-domain instance issuer.
permit (
principal,
action == Action::"payments:read",
resource
)
when {
context.token.client_id == "planner-agent" &&
context.token.act.iss == "https://issuer.assistant.example" &&
context.token.act.sub_profile.contains("client_instance") &&
context.token.act.sub like "spiffe://assistant.example/ns/agents/production/*"
};
Three token-claim checks plus one prefix match against the SPIFFE namespace. None of this is expressible interoperably from standard OAuth token data alone without instance identity in the chain.
๐ก๏ธ What Does Not Change
The point of the proposal is the surface area it does not move.
client_idremains the trust anchor. The OAuth client is still the registered application. Authorization decisions, scope grants, consent records, and admin policy continue to attach to the client, not to instances.- No new grant type.
authorization_code,client_credentials,refresh_token,jwt-bearer, andtoken-exchangekeep their existing flows. An instance-attestation input is made available on those flows, but the grant types themselves are unchanged. - No new claim shape.
act,sub_profile, andcnfalready exist. The proposal adds one profile value (client_instance). Everything else on the token side is reuse. - No new client type. Clients do not become “instance-aware clients” or any other new category. They register as today (statically, via Dynamic Client Registration, or via CIMD) and optionally include
instance_issuersin their existing metadata. - OAuth revocation does not change. RFC 7009 revocation and RFC 7662 introspection work as before. Issuer-side revocation is a complementary upstream control.
The protocol does not need to learn about instances. It already knows about actors. We just need to put instances in that slot.
๐ชง Open Design Choice: actor_token or client_instance_assertion?
The current draft uses RFC 8693’s actor_token parameter, extended to grants beyond token exchange, to deliver the instance assertion at the token endpoint. A reasonable alternative is a dedicated request parameter, client_instance_assertion. The access-token result is the same either way, so the question is only which token-endpoint wire form is cleaner.
The two options produce identical access tokens. Both place the validated runtime in act for delegated flows or in sub for self-acting flows. The actor model on the token side does not change. What changes is the wire form at the token endpoint.
The Two Wire Forms
With actor_token (the current draft, shown for an authorization code redemption):
| |
With client_instance_assertion:
| |
The dedicated parameter form is shorter because the parameter name carries the type meaning that actor_token_type carries explicitly.
Why the Draft Chose actor_token
The draft documents the reasoning in its appendix on this question. The load-bearing arguments:
- Avoiding token-endpoint fragmentation. Reusing
actor_token/actor_token_typekeeps the wire surface unified instead of adding per-profile request parameters. - Reuse of an existing Token Exchange slot. RFC 8693 already uses
actor_tokenfor the party currently acting in a token exchange request. The current draft extends that same input shape to other grants instead of creating a second way to present actor evidence. - Future profile extensibility. Profiles can define their own
actor_token_typeURIs without each needing a new request parameter. - Semantic continuity. In delegated flows,
actor_tokencarries the evidence that becomes the issued token’sactentry. In self-acting flows, the same evidence is represented insub. The parameter names the acting-party evidence without forcing every output token to containact.
Why client_instance_assertion Might Be Better
- Semantic precision. The parameter name describes exactly what is being presented: proof of the concrete runtime instance acting for the registered client.
- Avoids stretching
actor_tokensemantics in self-acting flows. In aclient_credentialsrequest, the runtime is not an actor in the RFC 8693 sense. It is the principal. A neutrally named parameter avoids that stretch.
The choice is bounded to the wire form. Everything in What Does Not Change still applies regardless of which parameter wins.
The wire parameter says what is being proven. The actor chain says what it means. Whether those two things share a name (
actor_token) or have separate names (client_instance_assertionplusact) is the question this section is asking for feedback on.
โ๏ธ The Alternative: Promote client_instance to the Protocol
The wire-form choice in the section above stays inside the actor model: both actor_token and client_instance_assertion deliver runtime identity into the existing act claim. A more structural alternative would be to promote client_instance to a top-level OAuth concept of its own. That path is worth describing because it is the obvious next move once you accept that instances matter.
That path looks like this:
- A new request parameter such as
client_instance_idat the token endpoint, paired with aclient_instance_assertion. - A new top-level access token claim,
client_instance, distinct fromclient_idand fromact. - A new grant type, or a parameter retrofit across every existing grant, signaling that an instance attestation is being presented.
- A new client registration mode so authorization servers can tell instance-aware clients apart from legacy ones.
- New introspection fields, new revocation scopes, and a new consent dimension once instances are visible to end users.
It has surface appeal. Instances become visible at the protocol layer the same way clients are. A first-time implementer reading a token would see an “instance” field labeled as such, rather than a typed entry inside act. Tooling that today renders client_id could render client_instance next to it without learning anything about actor chains. For people whose mental model starts at OAuth 2.0 and stops short of Token Exchange, that is a real ergonomic gain.
The cost is that every layer of the stack has to learn the new concept. Authorization servers, libraries, introspection endpoints, audit pipelines, consent UI, policy engines, API gateways, and downstream resource servers all need new code paths. The Actor Profile already paid the cost of teaching the ecosystem typed, key-bound delegation, and the new concept overlaps almost entirely with what act already represents: a non-principal identity exercising authority on behalf of someone, with its own type, key binding, and chaining rules for sub-actors. Promoting client_instance to the protocol layer would create a parallel mechanism alongside it. Two ways to say “the runtime currently acting is X.” Two introspection shapes to learn. Two policy entry points. Two places where audit chains can disagree about who acted and when.
The actor framing chooses a smaller tradeoff: one profile value (client_instance), one actor-token type or one dedicated request parameter depending on the wire-form choice, one optional client metadata field (instance_issuers) that rides on whatever registration channel the deployment already uses, and the rest is reuse. AS code that already validates act and cnf extends naturally. Resource server policy that already evaluates act adds one branch for the client_instance profile. An introspector that does not know about instances still sees a structurally valid actor chain and renders it as one.
The choice is not about whether instances are important. The choice is about whether to make instance identity a new thing the protocol must learn, or to treat it as a specific kind of actor the protocol already knows how to carry.
Spending the protocol-extension cost twice for the same expressive content is the failure mode worth avoiding.
The full design space, summarized:
| Approach | What changes | Ecosystem cost |
|---|---|---|
| Status quo (per-vendor extensions) | Per-vendor proprietary claims, headers, and runtime-identity formats | High: adapters needed everywhere, no cross-vendor composition |
actor_token (current draft) | One new actor_token_type URI | Low: AS validates a new type, no new request parameter |
client_instance_assertion (alternative) | One new request parameter | Low: AS validates a new parameter, same act representation downstream |
Top-level client_instance claim | New request parameter + new access-token claim + new policy / introspection / audit paths | High: every layer of the stack learns a new concept |
๐๏ธ Why This Is a Primitive
The proposal’s restraint is what makes it load-bearing for downstream work. Instance identity is a primitive that several open lines of work depend on:
- Mission-Bound OAuth needs runtime identity to bind a mission to the specific running thing carrying it. “Bound to the planner-agent client” is not a binding. “Bound to instance
planner-7f3cuntil its attestation expires” is. - Cross-app access (XAA / ID-JAG) needs per-runtime policy scoping (production vs. development, customer-tenanted vs. shared) without registering separate clients per class.
- Audit, observability, and SOC tooling can finally answer “which runtime did this.” That is the question the failure scenario opened with. Without instance identity in the token, that question has no answer.
Each of these initiatives already consumes act, sub_profile, and cnf. Putting instance identity in the same vocabulary means none of them needs a separate plumbing pass.
๐ค Common Questions
Does this require DPoP on every issued token?
The instance assertion always includes cnf so the AS can sender-constrain whatever it issues. Whether the issued access token is itself sender-constrained downstream is a deployment choice. Bearer-style downstream tokens still work. A resource server that does not enforce sender-constraint will simply not benefit from the live key check. Most deployments that go to the trouble of attesting instances will want sender-constraint downstream, because that is what makes “this specific instance is allowed to present this token” enforceable instead of just informative.
Does this work for opaque tokens and introspection (RFC 7662)?
Yes, if the authorization server exposes equivalent actor-profile fields in the introspection response. The opaque token itself does not carry the profile the way a JWT access token does. A resource server introspecting an opaque token evaluates act, sub_profile, and cnf from the RFC 7662 response instead of from token claims.
Is this only for AI agents?
No. The same model fits CI/CD runners, serverless invocations, mobile and desktop installs, and broker-mediated sessions. AI agents make the gap urgent because they multiply runtime cardinality and fan out into sub-agents. The underlying mismatch between logical client and runtime applies anywhere the same registered client is realized as many runtimes.
Can different instances of the same client carry different scopes?
Scope grants attach to the client and (for delegation) the user. Per-instance scope narrowing happens through token exchange: a parent instance trades for a sub-token with reduced scope, the sub-instance presents that, and the resource server sees the narrower scope alongside the new actor entry. The Actor Profile’s chain semantics handle the rest.
Who runs the instance issuer?
Whoever owns the runtime platform. In practice that is the agent product vendor for hosted agent fleets, the cloud provider for managed serverless and Kubernetes (often via SPIFFE or platform workload identity), and the application vendor for desktop and mobile broker scenarios. The OAuth client lists those issuers in its instance_issuers metadata, which is what tells the AS that an attestation from that issuer is acceptable for this client.
Does this require CIMD?
No. CIMD is one delivery channel for client metadata. Static admin-provisioned registration and Dynamic Client Registration (RFC 7591) carry the same instance_issuers field equally well. CIMD is convenient when the client_id is already a URL, because it keeps the metadata under the client’s own control, but the descriptor format and the AS processing rules are the same regardless of how the metadata reached the AS.
Can the instance assertion authenticate the client too?
Yes, optionally. A client can declare client_instance_jwt as a token_endpoint_auth_method value, and the AS will accept the endorsed instance assertion as both runtime identity and client authentication. This eliminates the need to provision a long-lived client credential per runtime. Clients with stable credentials (private_key_jwt, mTLS) can keep them and let the instance assertion ride alongside, identifying the runtime without replacing the credential. The simplification is opt-in.
๐ฌ This Is Early Work, And I Want Feedback
Both drafts are early individual submissions. The Actor Profile and the Client Instance Assertions draft are on datatracker, but neither is an OAuth WG document. Before either should move forward, the approach needs to survive review by people who build, operate, or buy real systems.
The thing I am most trying to test is the central standards question: should OAuth represent runtime instance identity as actor-profile data, or does it need a first-class protocol surface of its own? My thesis is that runtime instance identity is best treated as a typed actor in the existing act chain, not as a new top-level OAuth concept. If that framing is wrong, if there is a class of deployment, a threat model, an existing pattern, or a sub-protocol I have not accounted for, I want to hear about it now, before more weight gets written into the draft.
Specific things I am uncertain about:
- Whether the trust delegation model (client endorses issuers in its metadata, issuer mints with a
client_idbinding, AS validates against the endorsement set) is the right shape, or whether something closer to per-attempt attestation-bundle propagation fits better. - Whether the runtime attestation should ride in
actor_token(the current draft choice, reusing the RFC 8693 parameter) or in a dedicatedclient_instance_assertionparameter. The Open Design Choice section above lays out both wire forms, the reasoning for the current pick, and the case for switching. - Whether
client_instancebelongs alongsideuser,service, andai_agentin thesub_profilenamespace, or whether it deserves a different classification axis. - Whether the failure scenario reflects what enterprises and vendors actually face, or whether it overdramatizes a problem that vendors have already solved internally in ways I am not seeing.
If you operate an authorization server, build a resource server or API gateway, run an agent platform, or buy enterprise SaaS at scale, your read on this is what I most want. Counter-examples are more useful than agreement. Tell me what is wrong, what is overstated, what is missing, and what is unworkable.
Logical clients describe what was authorized. Instances describe what is acting. The model that already names the second thing is the actor chain. The question for the OAuth community is whether putting instances in that slot is enough, or whether runtime identity deserves a new protocol surface despite the extra machinery that would require.
If this lands, an enterprise security team responding to an incident pulls the access token and immediately sees which runtime acted, which issuer attested it, which key was bound, and which sub-agent spawned which. Revocation targets the specific compromised attestation without disrupting the rest of the fleet. SOC tooling consumes the actor chain as portable JSON instead of per-vendor adapters.
Cross-app access policies scope to runtime classes (production vs. development, customer-tenanted vs. shared) without registering separate clients per class. Mission-bound OAuth and other higher-layer governance work compose against a primitive already in the token, in the same vocabulary every party understands. The point is not novelty. It is portability for the runtimes acting across enterprise systems.