Skip to content

jmsgrn/writ

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

writ

ci license: MIT runtime: Bun

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


What writ is

A tiny library (one credential type, two functions) for delegated authority between agents:

  • generateKeypair() - an Ed25519 keypair plus its did:key identifier.
  • 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.

Why writ exists

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.

  1. 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.)
  2. 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
Loading

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.

How it works

The handshake

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)
Loading

Inside verifyWrit

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"])
Loading

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.

Quickstart

bun run examples/handshake.ts
James        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

The four primitives

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 issuersubject
4 A verifier that ignores everything else verifyWrit (the four gates above)

API

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" }

Where it fits

  • 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 only node:crypto. Base58btc and did:key encoding are ~40 lines in-repo, tested.

Status and roadmap

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.

Develop

bun test                        # unit tests (8)
bun run examples/handshake.ts   # the demo above

License

MIT (c) 2026 James Green.

About

A signed, revocable grant of authority for AI agents. Portable identity + delegation (did:key + Ed25519) any harness can verify. An agent id is a key, not a filter. Reference impl of the primitives behind Agent Passport / A2A / MCP-I. Zero deps.

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors