A Practical Layout for Agent Identities: Me, Agents, Roles and Projects
Table of Contents
The standards work on agent identity is about what goes over the wire: an identifier, a credential bound to it, a token that says who an agent is acting for. None of it says where one person keeps the credentials that their agents hold today, most of which are plain API keys. This is a proposal for that layout, written against a file-per-entry password store such as pass, and a note of where it meets the standards and where it falls short of them.
It is a layout and a set of rules, not a protocol. Nothing here has been built.
1. The layout
the layout is a proposal; its only checkable claim is about pass, in the intro above this heading
pass keeps one encrypted file per entry, not one directory per entry; "directory-per-entry" corrected to "file-per-entry"
2026-10-10 evening: six environments, the two OAuth kinds and issuer normalisation come from a 62-entry fixture built in a local workshop session; attributed, not rerun here
2026-10-11, reproduced: =pass-1.7.4= in a disposable GNUPGHOME/PASSWORD_STORE_DIR (never the real store on this machine). Inserted fictional entries at =me/example-forge.com/alice/login= and =role/builder/example-forge.com/alice/token/laptop=; =find= and =pass ls= confirmed one =.gpg= file per leaf entry, with intermediate path segments as real directories -- "file-per-entry" holds exactly as corrected.
Five kinds of owner hold credentials, and the first segment says which. Every path has the same shape after the owner:
path = owner "/" realm "/" handle "/" kind [ "/" name ] owner = "me" | "agent/" name | "role/" name | "proj/" project "/" env | "site/" place
| Owner | Path prefix | Holds |
|---|---|---|
| the person | me/ |
the person's own accounts: logins, second factors, recovery codes, tokens |
| a named agent | agent/<name>/ |
an agent with accounts of its own, when a service issues tokens only to accounts |
| a role | role/<name>/ |
an agent that is a job, not a persona: tokens issued for that job and nothing else |
| a project | proj/<project>/<env>/ |
what identifies an application or deployment to a provider, per environment |
| a place | site/<place>/ |
what admits anyone standing there: a network, a door, a host's admin |
An example tree, with invented names:
me/ example-forge.com/alice/login example-forge.com/alice/otp example-forge.com/alice/token/laptop role/ builder/example-forge.com/alice/token/builder-laptop builder/llm-gateway.internal/_/token/builder-laptop builder/broker.internal/builder/token/builder-laptop reviewer/example-forge.com/alice/token/reviewer-laptop observer/metrics.internal/_/token/observer-laptop proj/ workshop/dev/graph-db.internal/app/login workshop/prod/graph-db.internal/app/login workshop/prod/example-idp.com/workshop-prod/token/app workshop/shared/dns.example/_/token/ci site/ home/net.guest/_/login
The segments have fixed meanings.
- Realm is where the credential is honoured: the provider's registrable
domain, in lower case, with no trailing dot (
example-forge.com, notExampleForge). Without one it ishost.<name>ornet.<name>. A tool that derives values from the path seesExample.comandexample.comas two entries, though DNS sees one host. - Handle is what the realm calls the account, verbatim;
_when the realm has no accounts. A role's token is usually issued on someone's account (aliceabove), and the handle says whose. - Kind is closed:
login(typed to begin a session),otp(an authenticator secret),recovery(codes that stand in for a lost factor),token(issued by the provider, revocable there one at a time),key(material nobody can reissue),pin. No segment may be one of these words anywhere else in the path, which is what lets a path parse without knowing its owner. - Name follows
tokenand is the holder: the machine or service that presents it (laptop,ci,builder-laptop). Two holders get two tokens. What the token may do is not in the path. - Environment belongs to a project and is closed:
local,dev,test,staging,prod,shared. A project uses the ones it has; a library may have onlytestandshared. A role reaches an environment through the scope of the token it holds, not through its path.
An entry is the secret on line 1, then name: value fields from a closed
list. The ones that carry the layout's weight:
scope- what the provider says the token permits, in its own words. This is where the privilege level lives.
effect- the worst the bearer can do in front of other people:
private(nobody sees it, or it can be undone),staged(it prepares something a person approves),outward(it acts under a name, at once). A token that can publish isoutwardeven if it never has. issued,expires- the current value's issue date and end date, or
never. A derived secret also carries its generation; bumping the generation movesissued. used-by- what presents it, in a sentence.
A project's registration with a sign-in provider is a token under the
project (the provider issues the client secret); its client id and redirect
address are fields, and differ per environment. An account the person signs
into through another provider holds nothing of its own: it has a line in the
record (below) pointing at the account it hangs from, and no entry.
2. The rules
readers per owner holds for contents: pass init -p <subfolder> gives a subfolder its own .gpg-id recipients
pass leaves file and directory names unencrypted, so every reader of the repo can see the whole tree; the boundary is for contents only
survey claim correct: arXiv 2601.12538 §5.1.1 lists Leader/Coordinator, Worker/Executor and Critic/Evaluator, §5.1.2 developers and code reviewers; arXiv 2508.02744 §3.1.2 has "Planner and executor" and "Coder and reviewer"
2026-10-10 night: grammar, holder-named tokens, effect and the record aligned with a sealed specification written from a store built for real accounts the same day (private); the earlier agent-<role>/<env>/.../token/<scope> grammar is withdrawn
2026-10-11T12:48:42Z Macaroons, NDSS 2014 PDF (Birgisson, Politz, Erlingsson, Taly, Vrable, Lentczner) -- correct: abstract says caveats "attenuate and contextually confine"
2026-10-11T12:48:18Z Biscuit (doc.biscuitsec.org, redirected from biscuitsec.org) -- attributed: project docs, "can only be restricted, it will never gain more rights", offline attenuation
2026-10-11T12:48:18Z UCAN spec 1.0.0 (github.com/ucan-wg/spec; ucan.xyz links to it) -- correct: "Each direct delegation MUST either directly restate or attenuate (diminish) its capabilities"
2026-10-11T12:48:18Z AWS IAM session policies -- attributed: "the intersection of the identity-based policies ... and the session policies"; a resource policy naming the session ARN is not limited by it
2026-10-11T12:48:18Z Google Credential Access Boundaries (docs.cloud.google.com) -- attributed: "fewer permissions than the service account"; Cloud Storage only
2026-10-11T12:47:18Z Vault child tokens (api-docs/auth/token, concepts/tokens) -- attributed for policies: "must be a subset ... unless the calling token is root or contains sudo"; revoked with the parent. MISMATCH against the claim checked: neither page says a child's TTL is shorter than or bounded by the parent's; the text does not claim it
2026-10-11T12:48:18Z GitHub fine-grained PATs (managing-your-personal-access-tokens) -- attributed: created in settings per token; no documented way to derive a narrower token. "Not self-attenuated" is this page's reading of that absence
2026-10-11T12:46:31Z OWASP NHI9 -- correct (quoted); OWASP Secrets Management Cheat Sheet (raw markdown, 12:47:01Z) -- correct only in weak form: it says "Often, services share the same secrets, which makes identifying the source of compromise or leak challenging"; it has no rule against reuse, and the text says only what it says
2026-10-11T12:47:18Z Vault policies and policy-templating tutorial -- attributed: =*= is a prefix match at the end of a path; ={{identity.entity.name}}= example quoted. AWS Secrets Manager auth-and-access examples -- attributed: =TestEnv/*= prefix grant; the =prod/app/db= example checked for is not on that page
2026-10-11T12:46:31Z SPIFFE-ID.md and spiffe.io concepts -- MISMATCH against the claim checked: neither says policy can match on path prefixes; the spec says paths "MAY be hierarchical" and leaves meaning to the implementer. The text says that
2026-10-11, reproduced (same disposable sandbox as the layout heading above): =pass ls= on the test store printed the full plaintext tree (owners, realms, handles, kinds) with no decryption step -- confirms the boundary is contents-only. =pass init -p role/builder <fpr>= wrote a distinct =role/builder/.gpg-id= while the top-level =.gpg-id= stayed on the original key -- confirms per-subfolder reader separation directly.
- The person mints, a role consumes. Every token under
role/was issued by an account the person controls, and can be revoked from there. - No
loginunder a role. A role holds what it was issued, and nothing that can mint more. If a service issues tokens only to accounts, that is a named agent underagent/, with its own login, second factor and recovery codes, and every cost that comes with them. Prefer a role. - Readers per owner. Each owner has its own set of keys that can decrypt
it:
role/builder/to the builder's key and the person's,me/to the person's alone. A process acting as an owner is given a key for that owner and no other, so that it cannot readme/is a fact about keys, not about where it was told to look. Recipients per directory, separate stores, or a broker holding the only key all satisfy this. It hides contents, not names:passleaves paths in clear, so any reader can list the whole tree. One key for everything, decrypted by an unlocked agent on the person's own machine, satisfies none of it; a setup like that should say so. - A role is a ceiling. The role's directory is the most an agent in that role may hold. A task takes less: where the issuer can mint a shorter-lived or narrower token from the stored one, the agent uses that. Some formats let the holder do the narrowing. A macaroon embeds "caveats that attenuate and contextually confine" it (Birgisson et al., NDSS 2014); a Biscuit token "can only be restricted", offline; a UCAN delegation "MUST either directly restate or attenuate" its capabilities. Elsewhere the issuer does it on request. An AWS STS session gets the intersection of the role's policies and the session policy. Google's Credential Access Boundaries give a token "fewer permissions than the service account", for Cloud Storage only. A Vault child token's policies "must be a subset" of its parent's, and it is revoked with the parent. A GitHub fine-grained token is none of these. Each one is issued from the account's settings, and GitHub documents no way to derive a narrower token from an existing one.
- No two owners share an entry. If two need the same access, the provider issues two credentials. Moving an entry from one owner to another is not a rename; it is a revoke and an issue. OWASP lists the failure as NHI9, reusing one identity "across different applications, services, or components", and its Secrets Management Cheat Sheet notes that when services share a secret, the source of a leak is hard to find.
- The path is the identifier.
role/buildernames the same thing in the store, in the broker's access list, in a telemetry attribute and in an audit log. Secret stores already key policy on the path. A Vault policy path ending in*matches everything under that prefix, and a templated policy writes the caller into the path (user-kv/data/{{identity.entity.name}}/*). An AWS Secrets Manager policy can grant every secret whose name beginsTestEnv/. SPIFFE does not go that far: its paths "MAY be hierarchical", and their meaning is "reserved as an exercise to the implementer".
Four roles are enough to exercise this: a planner that reads everything and
writes nothing, a builder that writes in dev, a reviewer that reads and
comments, an observer that reads telemetry only. The surveys of multi-agent
systems keep finding the same splits: a coordinator that decomposes work,
planner against executor, coder against reviewer.
3. Where it meets the standards
WIMSE/SPIFFE correct: draft-ietf-wimse-identifier-03 defines "a URI that uniquely identifies a workload within the context of a specific trust domain"; SPIFFE-ID.md gives spiffe://trust-domain-name/path
klrc-00 §5-6 checked verbatim (one identifier per agent, primary credentials provisioned at runtime, secondary by exchange, "Static API keys are an antipattern"); replaced by draft-ietf-wimse-aims-00 (2026-09-15), same text
RFC 8693 §4.1 correct: act is the current actor, sub the subject; me=sub, role=act is the author's mapping; §4.4 may_act is the RFC's name for who may act
araut-00 (2026-04-16) is real; current is -02 (2026-05-22), which reuses the RFC 8693 act claim and makes agentic_ctx chain-level context updated at each hop; row corrected
ni-wimse-03 is real and current (2026-10-01); abstract: "binding an AI agent identity to an accountable user or organization"
DID 1.1 is a Candidate Recommendation Snapshot since 2026-03-05; the Agent Identity Registry Protocol Community Group exists, created 2026-04-24
OIDF whitepaper is arXiv 2510.25819; executive summary names user impersonation and missing on-behalf-of delegation
NCCoE draft concept paper (Feb 2026) never says "shared credentials"; it is about telling agent identities from human ones and tying actions to the agent's identity; row corrected
SP 800-57 was cited at Rev 3; current is Rev 5; the row pointed at a rotation column that does not exist; corrected
2026-10-11T12:47:58Z SP 800-57 Pt1 r5 PDF (nvlpubs) re-read: §5.2 "a single key shall be used for only one purpose" and §5.3 cryptoperiod both present -- correct. The purpose is a cryptographic function, not an owner; the row says so rather than mapping it onto rule 5
2026-10-11T12:46:31Z OWASP NHI Top 10 2025 (owasp.github.io, redirected from owasp.org) -- correct: NHI5 Overprivileged NHI, NHI7 Long-Lived Secrets, NHI8 Environment Isolation, NHI9 NHI Reuse, all quoted from the list page. Was cited only through the nhimg secondary; primary added
2026-10-11T12:46:10Z RFC 8707 §1 and RFC 9396 abstract fetched as text from rfc-editor.org -- correct as quoted
2026-10-11T12:46:31Z MCP authorization (latest = 2026-07-28, and 2025-06-18) -- correct for RFC 8707 (MUST resource parameter; MUST validate audience). MISMATCH against the claim checked: neither revision names RFC 9396, RFC 8693 or RFC 7662; the text says so
Most of the documents below are individual drafts or community-group output, not standards. The mapping is mine.
| Work | What it defines | This layout |
|---|---|---|
| IETF WIMSE; SPIFFE | a workload identifier: a URI unique within a trust domain, spiffe://<trust-domain>/<path> |
the path prefix is that path: spiffe://example.org/role/builder. Same shape, no issuer behind it |
draft-klrc-aiagent-auth, now draft-ietf-wimse-aims |
one identifier per agent; primary credentials bound to it and provisioned at runtime; secondary ones obtained by exchange | everything in the store is at best secondary. The draft calls static API keys an antipattern, and the store is where they live |
draft-ni-wimse-ai-agent-identity |
binding an agent to an accountable user or organisation | rule 1, and rule 3: the person is always among an owner's readers |
| RFC 8693 token exchange | sub for the subject, act for the actor, may_act for who may become one |
me is sub, role/<name> is act. The store is a local may_act; the token records who did |
draft-araut-oauth-transaction-tokens-for-agents |
the RFC 8693 act claim for the agent, sub for the principal, and an agentic_ctx claim updated at each agent hop |
the owner and the project environment are what would go in that context |
| W3C DID 1.1 (a Candidate Recommendation since March 2026), Verifiable Credentials; the Agent Identity Registry Protocol Community Group (April 2026) | resolving an agent identifier across organisations; a credential format; revocation | no overlap. Nothing here resolves outside one person's store |
| OpenID Foundation, Identity Management for Agentic AI | names user impersonation and missing on-behalf-of delegation as the gaps | rule 2 is the local form of "do not impersonate": a role cannot hold the person's login |
| NIST NCCoE concept paper (draft, February 2026) | telling agent identities from human ones, and tying an agent's actions to its own identity | one owner per role, and rule 5: no two owners share an entry |
| NIST SP 800-57 Part 1 Rev. 5 | cryptoperiods for keys; "a single key shall be used for only one purpose" (§5.2) | the rotation section below; it counts in years, for keys, and says nothing about bearer tokens. Its "purpose" is a cryptographic function, such as encryption or signing, not a holder |
| OWASP Non-Human Identities Top 10 (2025) | NHI5 overprivileged identities, NHI7 long-lived secrets, NHI8 environment isolation, NHI9 reuse of one identity "across different applications, services, or components" | rules 4 and 5, environments that do not cross, and the rotation section |
The honest reading of the table: the layout borrows the standards' names and none of their guarantees. An entry here is a bearer secret in a file. It is not bound to the workload that uses it, it does not expire unless the issuer makes it, and nothing attests which process read it.
Two OAuth documents bear on what a token is for rather than who holds it.
RFC 8707 lets a client name the resource, so the authorization server "can
restrict the intended audience of that token to the given resource"; RFC 9396
adds an authorization_details parameter for "fine-grained authorization
data". The MCP authorization specification requires the first: clients
"MUST implement Resource Indicators for OAuth 2.0 as defined in RFC 8707",
and servers must check that a token was issued for them. It names RFC 8707,
9728, 8414, 7591 and the OAuth 2.1 draft (RFC 6750 and 9207 in the current
revision, 9068 in 2025-06-18). It does not name RFC 9396, 8693 or 7662. The
store has nothing like an audience. An entry's realm is where it is
honoured, but the token itself is not bound to that realm.
4. Rotation
the 30-90 / 90-180 day figures were in no cited source (nhimg gives no interval, OWASP NHI7 only a 90-day statistic, SP 800-57r5 counts key cryptoperiods in years); replaced
vendor figures that exist: AWS Security Hub IAM.3 flags access keys older than 90 days; GitHub's default org maximum for fine-grained PATs is 366 days
"login only on a leak" is supported by NIST SP 800-63B-4: no periodic password changes, change on evidence of compromise
age-by-last-commit breaks on a recipient change: pass init re-encrypts and recommits every file in the directory; caveat added
the overlap-window clause was unsourced either way; removed
2026-10-10 evening: the counter rollback, the stale issued date and the 7/5 prefix-scoped mock incident were observed by the workshop session on its scratch database; attributed, not rerun here
2026-10-11T12:46:31Z OWASP NHI7 Long-Lived Secrets -- correct; 2026-10-11T12:47:01Z OWASP Secrets Management Cheat Sheet (raw markdown from OWASP/CheatSheetSeries) -- correct: "create secrets to expire after a defined time where possible", "regularly rotate secrets", least privilege; no interval in either
2026-10-11, reproduced in the same disposable sandbox: an initial attempt with primary-key-only test keys (no separate encryption subkey) showed no reencryption on =pass init -p= and no second commit -- reading =password-store='s own =reencrypt_path= (/usr/local/libexec/password-store/pass) showed it compares recipients by their encryption /subkey/ id, so a subkey-less test key can never match and the comparison is degenerate. Regenerated with a key that has a proper encryption subkey (matching the real keys on this machine, which all carry one) and reran: =pass init -p role/builder <new-fpr>= printed "reencrypting to <subkey-id>", produced a second commit ("Reencrypt password store using new GPG id..."), and =gpg --decrypt --list-only= on the existing entry confirmed it now decrypts under the new key only. The claim holds for realistically-shaped keys; the first, contradicting result was an artifact of the test key, not of pass.
- Age without decrypting. The store is a git repository. The last commit
that touched an entry's file is when it was last replaced, so a report of
credential age never opens a secret. Changing a directory's recipients
re-encrypts and recommits every file in it, so the report has to skip
pass's re-encryption commits. - Policy by path and effect. One file at the root maps path patterns and
effects to intervals:
outwardtokens sooner thanprivateones, tokens underproj/*/prod/sooner than underproj/*/dev/, aloginonly on a leak. A token whose end date isneveris the first thing to fix. No standard sets an interval for API keys or tokens. AWS flags access keys older than 90 days; GitHub caps fine-grained tokens at 366 days by default. NIST SP 800-57 gives cryptoperiods in years, for keys. For passwords NIST SP 800-63B says the opposite: no periodic change, change on evidence of compromise. OWASP names long-lived secrets as a risk (NHI7), and its Secrets Management Cheat Sheet says to "create secrets to expire after a defined time where possible". Neither gives a number. - Rotated means the old one is dead. A new value in the store is half of it. The rotation is done when every consumer has switched and the issuer refuses the old value.
- Derived or stored. A secret the person chooses (a broker password, a database password, a local shared key) can be derived from a root secret and the entry's path, with a counter as its generation, the way a stateless password manager derives one per site. A secret the issuer chooses cannot, and stays in the store; an OAuth client secret is the common case, since the provider issues it. Deriving buys reproducibility and nothing on disk. It does not buy forward secrecy: the root secret gives every generation, and an old generation stays derivable for ever, so only the issuer refusing it ends it.
- Rotation only goes forward. In the workshop fixture, asking the derivation tool for generation 1 saved that counter and silently rolled the entry back. A store that derives needs a rotate operation that refuses to lower the counter, separate from reading an old value.
- Scope by prefix. A mock incident on the fixture, "one project's secrets were exposed", rotated everything under that project's path, seven entries, and left the neighbouring project's five alone. The path is the blast radius.
6. Promotion: the same paths off the laptop
AgentCore: workload identity ARN arn:aws:bedrock-agentcore:region:account:workload-identity/directory/default/workload-identity/agent-name (identity "Features" page); token vault stores "OAuth 2.0 tokens, OAuth client credentials, and API keys", KMS-encrypted; vault access limited to "the specific agent and user combination that originally obtained them" (terminology page); 2LO and 3LO credential providers
Google Cloud: agent identity is "based on SPIFFE", principal://TRUST_DOMAIN/NAMESPACE/AGENT_NAME with reasoningEngines/AGENT_ENGINE_ID as the name; certificate-bound tokens with mTLS binding to the runtime; granted by IAM allow policies; ADC uses it automatically. The page says nothing about third-party keys
GitHub: sub "repo:octo-org/octo-repo:environment:Production"; "Without id-token: write, the OIDC JWT ID token cannot be requested"; repositories created after 2026-07-15 get owner and repository IDs in the subject; environment secrets released only "after any configured rules (for example, required reviewers) pass"
the three-way split (federate, issue another, never) is the author's proposal; not built. Revised 2026-10-10 night: "copy into the runtime's vault" became "issue another", following the rule that two holders get two tokens
The layout is written for one laptop. Agents also run elsewhere: in Amazon Bedrock AgentCore, in Google Cloud's Agent Runtime, on a GitHub Actions runner, in a container or a jail. The aim is to move a role into one of those without two failures: the laptop's store stops being the record of what exists, or a copy of a secret lands somewhere that nobody rotates.
A credential reaches a runtime in one of three ways, in order of preference.
- Federate. The runtime gives the workload an identity of its own and issues it short-lived credentials. Nothing is copied. The store holds the trust instead: which runtime identity may act as which role. That is configuration, not a secret.
- Issue another. The runtime is a new holder, so it gets its own token,
named for it (
token/builder-agentcorebesidetoken/builder-laptop), issued on the same account and revocable on its own (rule 5). Only where the issuer gives out one secret per account, which is the case for some API keys and most database passwords, is the value copied; the record then says the entry has two holders, and rotation has to reach both. - Never. Nothing under
me/is promoted, and nologinunder a role (rule 2). A runtime that asks for one is asking for the person.
| Runtime | Identity it gives the workload | What the path becomes | Federated | Held there |
|---|---|---|---|---|
| the laptop | none; the owner's readers | the identifier itself | -- | nothing; the store is read in place |
| GitHub Actions runner | an OIDC token per job, if the workflow has id-token: write; the subject names repository and environment, repo:example-org/workshop:environment:dev |
the GitHub environment carries a project's <env>; the cloud role the token may assume is named after the owner, role-builder |
AWS and Google Cloud accept the token, so no cloud credential is stored in GitHub | environment secrets, released to a job only after the environment's rules, such as required reviewers, pass |
| Amazon Bedrock AgentCore | a workload identity, arn:aws:bedrock-agentcore:<region>:<account>:workload-identity/directory/default/workload-identity/<name> |
<name> is the owner flattened: role-builder |
AWS resources through IAM; outside services through OAuth credential providers, client credentials or authorization code | API keys and OAuth client credentials in the token vault, which releases them only to the agent and user that obtained them |
| Google Cloud Agent Runtime | a per-agent identity based on SPIFFE, principal://agents.global.org-<org>.system.id.goog/resources/aiplatform/projects/<n>/locations/<loc>/reasoningEngines/<id>, with tokens bound to a certificate and usable only from the runtime |
Google assigns <id>, so the path cannot be the identifier; the record maps owner to principal |
Google Cloud resources through IAM grants to the principal | not covered by the agent identity docs |
Two things follow from the table. First, promotion is where the standards' guarantees turn up: each runtime binds its identity to the workload, which the laptop never does (see the standards). The bearer-secret gap closes for federated credentials and stays open for everything held. Second, rule 6 bends. GitHub and AgentCore let the owner survive as a name; Google does not, and GitHub's subject for repositories created after 15 July 2026 carries owner and repository IDs, so a rename does not move the trust. Where the runtime picks the name, the record maps one to the other.
7. The record
the record's four states and its derivation from the entries follow the sealed specification of a store built the same day (private); the federation lines and the place of a copy are this page's proposal, not in that specification; nothing built
2026-10-11T12:46:31Z OWASP NHI8 Environment Isolation -- correct: "Reusing the same NHIs across multiple environments ... can introduce significant security vulnerabilities"
2026-10-11T12:47:18Z Stripe API keys page -- attributed: sandbox vs live mode, "Each mode has its own set of API keys, and objects in one mode aren't accessible to the other"
The store needs one file in the clear that answers three questions without decrypting anything: what is held, what ends when, and what it could do. Entry names are visible on disk already, so dates and effects beside them leak little more.
Each entry has a line with a state: held (in the store), wanted (not yet
issued), elsewhere (exists, but not here), dropped (revoked, with the
reason). For a held entry the end date and the effect are copied from the
entry's fields, and the copy is derived or checked by a tool, never typed: a
record kept by hand drifts from what it describes, and a wrong line is
believed.
Promotion adds two things. An elsewhere line says where. And a role that is
federated has a line of its own, keyed by owner, because nothing is filed:
role/builder/llm-gateway.internal/_/token/builder-laptop held 2027-01-10 private role/builder/llm-gateway.internal/_/token/builder-agentcore elsewhere 2027-01-10 private agentcore:role-builder api-key provider llm-gateway role/builder federated github:example-org/workshop env=dev -> cloud role role-builder role/reviewer federated gcp:reasoningEngines/1234 (assigned) proj/workshop/prod/graph-db.internal/app/login held never private also: github:example-org/workshop env=prod secret GRAPH_DB_APP
That turns "don't lose anything locally" into checks a harness can run:
- Every holder is recorded. List each runtime's vault; a name with no line is drift or a leak.
- Losing a runtime loses nothing. Federated roles held nothing there, and what was issued there is revoked there without touching the laptop's.
- Rotation reaches every holder. An entry with
also:is rotated when the store, every copy and the issuer agree. - An incident is a prefix. The prefix that selects a project's entries also selects their lines, so scope by path covers the runtimes.
- Environments do not cross. An entry under
proj/<p>/devnever appears in a runtime'sprodenvironment. OWASP lists reuse across environments as NHI8. Stripe builds the separation into the key: each mode "has its own set of API keys, and objects in one mode aren't accessible to the other". - The role stays a ceiling. A runtime gets at most the role's entries (rule 4), never a union of roles.
8. A harness
not built or run; the page says so
step 5 as first written tested what the client claims, not the store: service.name is taken on the sender's word and bearertokenauth does not record which token matched; caveat added
The layout makes claims that can fail, so it can be tested.
- Stand up the local shared systems only: a model gateway, a metrics server, a broker, a collector. Hosted services stay out of an automated run.
- Mint one credential per role and system into a throwaway store with throwaway keys. A real store is never opened.
- Run a table of probes: role, system, action, and whether it should be allowed. The denials are the point. The reviewer publishing to the builder's topic must fail. A role decrypting another role's directory must fail.
- Replace one role's credential, then assert the old one is refused and the new one works.
- Check that every probe shows up in the collector under the right role.
The collector takes
service.nameon the sender's word, so this tests the client, not the store; a client certificate per role is what would tie the two. - For each promoted role, list the runtime's vault and compare it with the record. A local stand-in for a runtime, a container with its own secrets directory, is enough to test the comparison.
The result is a grid of role by system: credential age, when it is due, and the last probe.
9. Gaps
"nested act claims lose the constraints at each hop" overstated: act never carried constraints; liu-00 says act "captures only the identity of each actor in the chain, not the authorization constraints applied at each hop"
RFC 8693 §4.1: nested actors are informational only for access control
roles-against-tasks point rests on Okta, one vendor's view, labelled as such
2026-10-10 evening: the persona answer to named agents and the store-isolation gap come from the workshop session; attributed
2026-10-10 night: named agents moved from gap to owner, with the costs observed building them in a private skill; machines now under site/; hardware factors and unchecked effect added
2026-10-11: =babashka-workshop-spectre= (jwalsh/babashka-workshop-spectre) implements an entirely different family -- the stateless Spectre/Master Password algorithm, where nothing resembling a record-in-the-clear is ever written. Read its seed data and derivation engine directly (fictitious test inputs only): an "entry" is a bare =site -> {counter, template, variant}= map with no secret, no owner, no realm, and no effect; the password is recomputed from a master name and passphrase on every call. Added as a gap below, not a correction -- this layout has no answer for a credential model that derives instead of stores.
2026-10-11T12:47:18Z GitHub "About authentication to GitHub" -- attributed: ghp_, github_pat_, gho_, ghu_, ghs_, ghr_. The 2021 token-format blog post lists five prefixes and not github_pat_, so the docs page is cited for it
2026-10-11T12:47:18Z Stripe API keys page -- attributed: pk_/rk_/sk_ with _test_ (sandbox) and _live_; a bare "rk_" is the restricted-key type, which also takes the mode
2026-10-11T12:46:11Z RFC 8959 (Informational) -- correct: "When tokens are easier to unambiguously identify, they can trigger warnings"
2026-10-11T12:47:18Z gh auth status manual and AWS CLI get-caller-identity reference -- attributed: "tests the authentication state of each known account"; "No permissions are required to perform this operation"
2026-10-11T12:46:11Z RFC 7662 -- MISMATCH against the "who-am-I" framing checked: introspection is for "a protected resource to query" the authorization server and the endpoint "MUST also require some form of authorization"; it is not a holder's self-check. The text says so
- Bearer secrets. Nothing binds an entry to the process that uses it. The standards' answer is a credential issued at runtime to an attested workload; this is a layout for the world before that arrives.
- One person. There is no trust domain beyond a single store, no resolution, no way for another party to verify a role.
- Named agents cost what people cost. An earlier version listed agents with
accounts of their own as a gap. Building them gives the answer,
agent/<name>/, and its price: a mailbox to verify to, a second factor with no hand to hold it (it ends up as a hardware key a person owns, or a secret in the same store as the password), recovery codes, and usually a sign-in through another provider, which makes that provider's account the real credential. Whoever reads the mailbox can reset the rest. Make one only when a service issues tokens to nothing but an account. A role can still carry a persona, a name and a description, as metadata that never appears in a path. - Hardware second factors. There is nothing to file, so the record does not say which keys are registered where. That is what a second custodian would most need.
- What it means to others.
effectis typed at issue time. Nothing checks it against the provider, and a wrong line is believed. Some values say part of what they are. GitHub tokens begin with their type (ghp_for a classic personal token,github_pat_for a fine-grained one,ghs_for an app installation), Stripe keys carry their kind and mode (sk_live_,rk_test_), and RFC 8959 registers asecret-token:URI scheme so that leaked tokens are "easier to identify". The provider can be asked who a credential is.gh auth statustests each account against its host, andaws sts get-caller-identityreturns the caller's ARN and needs "no permissions". RFC 7662 introspection returns whether a token is active and its scope, but it is "a method for a protected resource to query" the authorization server, not a call for the holder. None of these says what a credential can do in front of other people. - The store's own isolation. A test copy of the store has to be told apart from the real one, per checkout. A shell started in one checkout keeps that checkout's settings when it moves into a worktree, which is the sub-agent case, so the check has to be made where the write happens, not where the shell started.
- Roles against tasks. The guidance on least privilege for agents argues that a static role is too broad, because an agent picks its tools at runtime (Okta's view; see Sources). Rule 4 concedes the point without solving it.
- Delegation depth. An agent that starts another agent is not modelled.
Nested
actclaims record who acted at each hop, not what each hop was allowed, and only the outermost actor counts for access control.draft-liu-oauth-chain-delegationadds a per-hop record for that. - Derive, don't store, is a different family entirely. The whole layout
assumes a stored secret with a record derived from it. The Spectre/Master
Password algorithm (spectre.app;
exercised locally via
babashka-workshop-spectre) instead recomputes a site's password from a master name and passphrase every time, storing only non-secret derivation parameters (a counter, an output template, a variant). Nothing here says whetherkind=/=effect=/=statestill make sense for an entry that is never at rest – or whether a derived secret belongs in this store at all, versus being out of scope for it the way hardware factors already are.
10. Sources
no fabrication: arXiv 2603.24775, 2604.23280, 2601.12538, 2508.02744 and 2510.25819 match their link text
CSA note title is "IETF's Race to Standardize AI Agent Identity" (2026-09-02); corrected
superseded links updated: klrc-00 (now draft-ietf-wimse-aims), araut-00 (now -02), SP 800-57 Pt1 r3 (now r5), NCCoE project page (403; concept-paper PDF cited), Grafana .md URL
RFC 8693, draft-ietf-wimse-identifier and SPIFFE-ID were relied on but missing; added
2026-10-11 citation pass: every link added below was fetched between 12:46:10Z and 12:48:42Z; per-source times and verdicts are in the drawer of the heading that cites it (overlap, rules, rotation, record, gaps). RFC 8693 re-fetched 12:46:10Z. Verdict mapping: "correct" = the source says it; "attributed" = a vendor or project describing its own product; mismatches are flagged MISMATCH in the citing drawer
2026-10-11T12:46:31Z SPIFFE concepts page added beside SPIFFE-ID: neither supports prefix-matched policy (see rules)
- draft-klrc-aiagent-auth-00 – identifier, primary and secondary credentials, static keys; replaced by draft-ietf-wimse-aims, same text
- RFC 8693: OAuth 2.0 Token Exchange –
act,may_act - draft-ietf-wimse-identifier-03, SPIFFE-ID – the identifier shape
- draft-ni-wimse-ai-agent-identity-03 – binding an agent to an accountable party
- draft-araut-oauth-transaction-tokens-for-agents-02 –
actandagentic_ctx - draft-liu-oauth-chain-delegation-00 – per-hop constraints in a delegation chain
- CSA: IETF's Race to Standardize AI Agent Identity – where the IETF work sits
- W3C Agent Identity Registry Protocol Community Group
- AIP: Agent Identity Protocol for Verifiable Delegation Across MCP and A2A – DID 1.1 status and the cost of resolution
- Identity Management for Agentic AI – the OpenID Foundation whitepaper
- NCCoE: Accelerating the Adoption of Software and AI Agent Identity and Authorization – concept paper, draft, February 2026
- NIST SP 800-57 Part 1 Rev. 5 – cryptoperiods
- NIST SP 800-63B-4 – no periodic password changes
- AWS Security Hub IAM.3, GitHub fine-grained token policy – vendor rotation defaults
- AI Identity: Standards, Gaps, and Research Directions for AI Agents
- Okta: least privilege for AI agents – roles against task scope; one vendor's view
- NHI Management Group on the OWASP NHI Top 10 – rotation scope; secondary source
- A Survey of Agentic Reasoning for Large Language Models, Large Language Model-based Data Science Agent: A Survey – recurring roles
- AgentCore Identity features, AgentCore Identity terminology – workload identity ARN, token vault, credential providers
- Google Cloud: Use Agent Identity with Agent Runtime – SPIFFE-based principal, certificate-bound tokens
- GitHub Actions OIDC reference, GitHub environments – subject format,
id-token: write, environment secrets - Hugging Face access tokens, LiteLLM budgets and rate limits, Grafana service accounts
- OWASP Non-Human Identities Top 10 (2025) – NHI5, NHI7, NHI8, NHI9
- OWASP Secrets Management Cheat Sheet – expiry, rotation, least privilege
- SPIFFE concepts – the ID as trust domain plus workload path
- Vault policies, Vault ACL policy templating – prefix globs,
{{identity.entity.name}} - Vault token auth API, Vault tokens – child policies a subset of the parent's; revoked with the parent
- AWS Secrets Manager: identity-based policy examples – grants on a name prefix
- RFC 8707: Resource Indicators for OAuth 2.0; RFC 9396: OAuth 2.0 Rich Authorization Requests; RFC 7662: OAuth 2.0 Token Introspection
- MCP authorization specification – RFC 8707 required; 9396, 8693 and 7662 not named
- GitHub: about authentication – token prefixes; GitHub blog: token formats (2021, without
github_pat_) - Stripe API keys – key prefixes; sandbox and live modes do not share objects
- RFC 8959: The "secret-token" URI Scheme
- gh auth status; aws sts get-caller-identity
- Birgisson et al., Macaroons: Cookies with Contextual Caveats for Decentralized Authorization in the Cloud – NDSS 2014
- Biscuit – offline attenuation; UCAN specification (ucan.xyz) – delegation restates or attenuates
- AWS IAM: session policies; Google Cloud: Credential Access Boundaries – Cloud Storage only
- GitHub: managing personal access tokens – fine-grained tokens are issued one at a time
11. Related
- Identity Store Specification: A Stack of Drafts – this layout as a sealed specification, v1 to v7
- Identity Store Enterprise Ontology – the same v1 formalized in OWL/RDF and SHACL, with a worked LiteLLM virtual-key example
- Naming for Isolation – how names become paths, branches and ports, and which escapes collide
- Agent Identity and Attestation – who acted and who claims what
- JSON Web Tokens – the token substrate
- Agent Permission Guardrails – what an agent may do once it holds a credential
- Dashboarding Agent Sessions from Hook Events – the other half of the grid