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

corrected · research-editor; pilot-annotation-pass · 2026-10-11

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, not ExampleForge). Without one it is host.<name> or net.<name>. A tool that derives values from the path sees Example.com and example.com as 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 (alice above), 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 token and 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 only test and shared. 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 is outward even 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 moves issued.
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

corrected · research-editor; pilot-annotation-pass; citation pass 2026-10-11 · 2026-10-11
  1. The person mints, a role consumes. Every token under role/ was issued by an account the person controls, and can be revoked from there.
  2. No login under 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 under agent/, with its own login, second factor and recovery codes, and every cost that comes with them. Prefer a role.
  3. 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 read me/ 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: pass leaves 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.
  4. 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.
  5. 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.
  6. The path is the identifier. role/builder names 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 begins TestEnv/. 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

corrected · research-editor; citation pass 2026-10-11 · 2026-10-11

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

corrected · claim-validator; research-editor; pilot-annotation-pass; citation pass 2026-10-11 · 2026-10-11
  • 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: outward tokens sooner than private ones, tokens under proj/*/prod/ sooner than under proj/*/dev/, a login only on a leak. A token whose end date is never is 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.

5. What a role gets from shared systems

corrected · claim-validator; research-editor; pilot-annotation-pass · 2026-10-11

Every row was checked against the vendor or project documentation on 2026-10-10.

System The person holds A role gets
Hugging Face the account login a fine-grained token scoped to named repositories; read and write tokens are the coarse alternative
LiteLLM proxy the master key a virtual key with an alias, a budget and rate limits
Grafana the admin login a service account with a role, and a token on it
a hosted model API the console login a service-account key bound to one workspace (a project, at OpenAI)
OpenTelemetry collector its config a token or client certificate the receiver checks; the role in service.name, which the receiver does not verify
MQTT broker the password and ACL files, or the dynamic-security admin a username with a topic access list: write its own subtree, read the rest
Kafka a super-user principal a principal per role, with ACLs on its topics (prefixed for its own) and on its consumer group

6. Promotion: the same paths off the laptop

correct · main session, against the vendor docs · 2026-10-10

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.

  1. 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.
  2. Issue another. The runtime is a new holder, so it gets its own token, named for it (token/builder-agentcore beside token/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.
  3. Never. Nothing under me/ is promoted, and no login under 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

speculative · main session; citation pass 2026-10-11 · 2026-10-11

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>/dev never appears in a runtime's prod environment. 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

speculative · research-editor · 2026-10-10

The layout makes claims that can fail, so it can be tested.

  1. 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.
  2. Mint one credential per role and system into a throwaway store with throwaway keys. A real store is never opened.
  3. 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.
  4. Replace one role's credential, then assert the old one is refused and the new one works.
  5. Check that every probe shows up in the collector under the right role. The collector takes service.name on the sender's word, so this tests the client, not the store; a client certificate per role is what would tie the two.
  6. 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

corrected · research-editor; citation pass 2026-10-11 · 2026-10-11
  • 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. effect is 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 a secret-token: URI scheme so that leaked tokens are "easier to identify". The provider can be asked who a credential is. gh auth status tests each account against its host, and aws sts get-caller-identity returns 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 act claims 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-delegation adds 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 whether kind=/=effect=/=state still 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

corrected · citation-auditor; research-editor; citation pass 2026-10-11 · 2026-10-11

11. Related