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

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

  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.
  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.
  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.

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

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 the rotation section below; it counts in years, for keys, and says nothing about bearer tokens

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.

4. Rotation

  • 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.
  • 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

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

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

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.
  • The role stays a ceiling. A runtime gets at most the role's entries (rule 4), never a union of roles.

8. A harness

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

  • 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.
  • 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

11. Related