A signed, revocable grant of authority for AI agents. Portable identity and
delegation - did:key + Ed25519 - that any harness can verify, with zero
runtime dependencies.
const writ = issueWrit(james, {
subject: agent.did,
capabilities: ["calendar:read", "travel:book<=500usd"],
ttlSeconds: 3600,
});
// agent presents `writ`; anyone can verify it acts for James, within limits, unexpired.A writ is a small signed credential that says:
This agent is acting on behalf of me, within these limits, and here is the signature that proves it - expiring then.
Companion essay: Get your AI-id (greenworks.dev).
A tiny library (one credential type, two functions) for delegated authority between agents:
generateKeypair()- an Ed25519 keypair plus itsdid:keyidentifier.issueWrit(issuer, grant)- sign a delegation of specific capabilities to a subject.verifyWrit(writ, opts)- check a presented writ: issuer trust, signature, expiry, scope.
No servers, no registry, no accounts. A writ is plain signed JSON any runtime can mint or check offline.
The instinct when you first think about agent identity is to build a filter: give every agent an id so you can catch the bad ones. That's backwards, and it fails twice.
- An id catches nobody. If an agent can mint its own id, a blocked agent just mints a fresh one and walks back in. (This is the Sybil attack, and it's why anonymous identifiers buy you nothing.)
- You're inspecting the wrong thing. A real identity check never asks the agent whether it's trustworthy - it checks a signature against a key it already trusts. The agent's persuasion is not an input.
So flip it. An agent id is a key, not a filter. Its job isn't to block bad agents, it's to unlock things for verified ones - to let an agent transact on your behalf in a way an anonymous agent can't. And what the key certifies isn't the agent's character. It's your delegation to it: on whose behalf, within what scope, vouched by whom, expiring when. Your agent is you, wearing a scope-limited power of attorney.
That means two identities are in play on every action - the industry calls this dual identity:
flowchart LR
H["Human principal<br/>did:key:z6Mk…"] -->|"delegates authority<br/>(signs a writ)"| W["Writ<br/>on-behalf-of · scope · expiry · proof"]
W -->|"carried by"| A["Agent<br/>its own did:key:z6Mk…"]
A -->|"acts"| S["Service / another agent"]
S -.->|"verifies BOTH:<br/>agent identity + the delegation"| W
The agent has its own identity (which process is this, really), and it borrows
a slice of yours (on whose behalf, how much). They're separate trust
relationships that fail and get revoked differently, so writ keeps them separate:
the subject is the agent, the issuer is you.
One more consequence: because the model and harness under an agent are disposable, identity can't live in the model. It lives in the credential the current agent authenticates into - exactly how SPIFFE hands workloads a short-lived signed identity that survives redeploys. Re-issue writs, don't extend them.
James delegates a narrow, time-boxed authority to his agent. A vendor's agent verifies it before doing anything. A malicious agent self-issues a writ and argues its case - and the argument never reaches the decision.
sequenceDiagram
actor James as James (principal)
participant A as James's agent
participant V as Vendor's verifier
participant E as Evil agent
James->>A: issueWrit(subject=A, caps=[travel:book≤500], ttl=1h)
Note over A: holds a signed writ
A->>V: "book travel" + present writ
V->>V: verifyWrit(writ, trustedIssuers=[James])
V-->>A: ALLOW ✓
A->>V: "wire money" + same writ
V-->>A: DENY - capability not delegated
E->>E: issueWrit(subject=E, caps=[bank:wire]) - self-signed
E->>V: "wire money" (+ a heartfelt paragraph)
V-->>E: DENY - untrusted issuer (checked before caps are even read)
The verifier is four gates, in order. The presenter's opinion of itself is not one of them.
flowchart TD
Start(["verifyWrit(writ, opts)"]) --> Q1{"issuer in<br/>trustedIssuers?"}
Q1 -- no --> D1["DENY<br/>untrusted issuer"]
Q1 -- yes --> Q2{"signature valid?<br/>(pubkey resolved<br/>from the issuer DID)"}
Q2 -- no --> D2["DENY<br/>forged or tampered"]
Q2 -- yes --> Q3{"now ≤ expiresAt?"}
Q3 -- no --> D3["DENY<br/>expired delegation"]
Q3 -- yes --> Q4{"capability in<br/>writ.capabilities?"}
Q4 -- no --> D4["DENY<br/>not delegated"]
Q4 -- yes --> Allow(["ALLOW"])
The key move is at gate 2: the issuer's public key is resolved from the issuer's
DID itself. A did:key is self-certifying - it carries its own public key - so
there's no registrar to call. Trust collapses to a single question: "is this
issuer DID in my set?" Everything else is math.
bun run examples/handshake.tsJames did:key:z6Mkvz…
James's agent did:key:z6MkmJ…
Evil agent did:key:z6Mkk5…
ALLOW James's agent books travel
-> ok
DENY James's agent tries to wire money (never delegated)
-> capability bank:wire not delegated (has ["calendar:read","travel:book<=500usd"])
DENY Evil agent insists it is legit and tries to wire money
-> untrusted issuer did:key:z6Mkk5… - anyone can mint a writ
DENY James's agent tries the same booking a week later
-> expired delegation
Every emerging agent-identity spec (Open Agent Passport, Google A2A's signed
Agent Cards, MCP-I, SPIFFE/WIMSE) is some arrangement of the same four things.
writ is the smallest honest implementation of them:
| # | Primitive | In writ |
|---|---|---|
| 1 | A decentralized identifier | did:key (Ed25519), self-certifying, no registrar |
| 2 | A signed capability + limit manifest | the Writ credential |
| 3 | A delegation chain to a human principal | issuer → subject |
| 4 | A verifier that ignores everything else | verifyWrit (the four gates above) |
import { generateKeypair, issueWrit, verifyWrit } from "@jmsgrn/writ";
const james = generateKeypair(); // the principal (you)
const agent = generateKeypair(); // your agent
const writ = issueWrit(james, {
subject: agent.did,
capabilities: ["calendar:read", "travel:book<=500usd"],
limits: { currency: "usd", perBooking: 500 },
ttlSeconds: 3600, // short-lived on purpose; re-issue, don't extend
});
verifyWrit(writ, {
trustedIssuers: [james.did], // DIDs you accept writs from
capability: "travel:book<=500usd",
});
// -> { ok: true, reason: "ok" }- Harness-agnostic on purpose. Same thesis as Lodestar/Serum, pointed at identity: your agent's authority shouldn't be a property of one vendor's platform. A writ is plain signed JSON any runtime can mint or check.
- A reference implementation, not a fifth standard. OAP, A2A Agent Cards, and MCP-I already exist. The goal is the cleanest cross-spec implementation of the shared primitives; the roadmap adds conformance adapters, not a rival format.
- Ed25519 over
did:key, zero dependencies. Runs on Bun or Node with onlynode:crypto. Base58btc anddid:keyencoding are ~40 lines in-repo, tested.
v0.1 (here): real Ed25519 + did:key, issue/verify, a two-agent handshake,
tests green.
- v0.1 - Ed25519 +
did:key, issue/verify, handshake, tests. - v0.2 - sub-delegation (an agent grants a narrower writ to a sub-agent) and revocation (short-lived + rotation, not long-lived tokens).
- v0.3 - two processes over HTTP exchanging and verifying writs before transacting (the real "your agent vs mine," off one machine).
- v0.4 - conformance adapters: emit/consume an Open-Agent-Passport credential and an A2A-style signed Agent Card.
- v0.5 - tamper-evident audit trail (who did what, on whose authority) - the regulated / CMMC angle.
bun test # unit tests (8)
bun run examples/handshake.ts # the demo aboveMIT (c) 2026 James Green.