@clawdreyhepburn/ovid
v0.5.0
Published
Cryptographic identity documents for AI agents — Ed25519 signed JWTs with delegation chains
Maintainers
Readme
New here? Read this first (no background assumed)
What is this, in one sentence? OVID is a small software library that gives each automated AI helper its own tamper-proof ID badge, so you always know who a helper is, who created it, what it's allowed to do, and when its access expires.
Why does that matter? When an AI assistant is given a big job, it often spawns smaller helper programs ("sub-agents") to handle pieces of it. By default, each helper inherits all the power of the thing that created it — like handing a house-painter the keys to your house, car, and bank account when they only needed one room. OVID replaces that with a specific, limited, unforgeable badge for each helper.
A few terms you'll see, in plain English:
- Agent / sub-agent — an automated AI worker. A "sub-agent" is a helper spawned by another agent.
- Badge / identity document / "OVID" — a small signed file that proves who a helper is and what it may do. (OVID = OpenClaw Verifiable Identity Document.)
- Mandate — the list of allowed actions printed on the badge.
- Signing / cryptographic signature — unforgeable digital math (the same kind that secures websites) that makes a badge impossible to fake or alter.
- Chain — because a helper can spawn its own helper, badges link together in a traceable chain leading back to you, the human.
- JWT — a common, standard file format for signed digital tokens. An OVID badge is a JWT with some extra fields. You don't need to know the format to use the library.
What OVID does and doesn't do: OVID issues and verifies badges (identity + expiry + traceability). It does not by itself stop a helper from misbehaving — that enforcement is a separate, companion job. See How OVID Fits the Stack. Think: OVID prints and validates the ID card; a separate security desk checks it at every door.
The rest of this README goes deeper and is aimed at developers integrating the library. If you just want the OpenClaw plugin that does all of this automatically, see @clawdreyhepburn/openclaw-ovid.
The Problem
When an AI agent spawns a sub-agent, the sub-agent inherits everything — API keys, credentials, tool access, filesystem. The code reviewer has a credit card. The browser worker can send tweets. The research agent can read every file on the machine.
This is ambient authority, and it's the same mistake we made with Unix root shells, shared browser cookies, and unsandboxed containers. The fix has always been the same: explicit, attenuated credentials.
OVID gives every sub-agent its own identity document — a signed JWT that says who it is, what mandate it carries, who created it, and when it expires. The spawning agent signs it. The chain is verifiable back to the human.
Read more: Your Sub-Agents Are Running With Scissors
How It Works
Human (root of trust)
│
│ delegates authority to
▼
Primary Agent (long-lived, has keypair)
│
│ issues OVID to
▼
Sub-Agent (ephemeral, carries OVID JWT with Cedar mandate)
│
│ can issue derived OVID to
▼
Sub-Sub-Agent (shorter lifetime, auditable chain)Four principles:
- The spawner is the attestor. You trust a sub-agent because you trust the thing that created it — and that trust is cryptographically verifiable.
- Lifetime can only shorten. A child's OVID can't outlive its parent's. When the parent expires, everything downstream expires.
- Identity is self-contained. An OVID carries everything needed for verification. No database. No central server. No network calls.
- The chain is the proof. Each OVID embeds its full parent chain of cryptographic attestations. Walk it back to the root and verify every signature against a trusted root public key. No intermediate JWTs required.
What OVID verifies — and what it doesn't
OVID is an identity and lifetime layer. A successful verifyOvid proves:
- The leaf agent's keypair was attested by the chain of parents back to a trusted root.
- Every
iat/expalong the chain is internally consistent (lifetimes can only shorten, and the JWT'siatcannot predate the parent's attestation). - The JWT was authored by the leaf's own keypair (not forged by a sibling).
- The token has not yet expired.
OVID does not verify:
- Mandate attenuation. OVID will happily sign a chain where a child's
policySetis broader than its parent's. A rogue parent can mint a wide-open child token if it wants to. Enforcing that children only receive a subset of their parent's authority is the job of@clawdreyhepburn/ovid-me(or any policy engine that consumes the verified mandate). Use OVID for identity, OVID-ME for policy evaluation and subset proof. - Resource authorization. OVID carries a Cedar policy set but doesn't evaluate it. Pass the verified mandate to Cedar (Cedarling, OVID-ME, etc.) to make an allow/deny decision for any specific action.
Quick Start
Install
npm install @clawdreyhepburn/ovidIssue an OVID
import { generateKeypair, createOvid } from '@clawdreyhepburn/ovid';
// Primary agent creates a keypair (do this once, persist it)
const primaryKeys = await generateKeypair();
// Spawn a sub-agent with a signed identity and Cedar mandate
const reviewer = await createOvid({
issuerKeys: primaryKeys,
issuer: 'clawdrey',
mandate: {
rarFormat: 'cedar',
policySet: 'permit(principal, action == Ovid::Action::"read_file", resource);',
},
ttlSeconds: 1800, // 30 minutes
});
console.log(reviewer.jwt); // standard JWT string
console.log(reviewer.claims.authorization_details[0].policySet); // Cedar policy
// In v0.4.0+, parent_chain is a ChainLink[] with cryptographic attestations.
// A root token has exactly one self-signed link (binding its own agent_pub).
console.log(reviewer.claims.authorization_details[0].parent_chain);Verify an OVID
import { verifyOvid } from '@clawdreyhepburn/ovid';
// Preferred: options form with trustedRoots (v0.4.0+).
const result = await verifyOvid(reviewer.jwt, {
trustedRoots: [primaryKeys.publicKey],
maxChainDepth: 5, // optional, defaults to 5
});
if (result.valid) {
console.log(result.principal); // "clawdrey/agent-7f3a"
console.log(result.mandate); // { type, rarFormat, policySet, ... }
console.log(result.chain); // ["clawdrey/agent-7f3a"] — flattened sub list
console.log(result.expiresIn); // seconds until expiry
}
// Legacy single-key overload (deprecated, emits console warning):
// const result = await verifyOvid(reviewer.jwt, primaryKeys.publicKey);Delegation chains
Sub-agents can issue OVIDs to their own sub-agents:
const helper = await createOvid({
issuerKeys: reviewer.keys,
issuerOvid: reviewer,
mandate: {
rarFormat: 'cedar',
policySet: 'permit(principal, action == Ovid::Action::"read_file", resource);',
},
ttlSeconds: 600, // shorter than parent ✅
});
// v0.4.0: parent_chain is ChainLink[] with root first, leaf last.
const chain = helper.claims.authorization_details[0].parent_chain;
console.log(chain.length); // 2
console.log(chain[0].sub); // root's sub (e.g. "clawdrey")
console.log(chain[1].sub); // helper's sub
// Each link carries { sub, agent_pub, iat, exp, sig } — the parent's signed
// attestation binding the child's identity.Chain verification (v0.4.0+)
Each delegation step emits a ChainLink: a compact signed attestation from
the parent binding the child's identity. The verifier walks the chain from
leaf to root, verifying each link's signature against the preceding link's
agent_pub, and anchors the root link against a caller-supplied set of
trusted root public keys.
A ChainLink is:
interface ChainLink {
sub: string; // the agent this link represents
agent_pub: string; // base64url Ed25519 pubkey bound to sub
iat: number; // issue time (unix seconds)
exp: number; // expiry (unix seconds)
sig: string; // base64url Ed25519 sig by PARENT over canonical bytes
}The signature covers this exact byte string (UTF-8):
ovid-chain-link/v1\n<sub>\n<agent_pub>\n<iat>\n<exp>where <iat> and <exp> are decimal integers with no leading zeros
(String(value) in JavaScript). Roots self-sign (the root's link is verified
against its own agent_pub, which must equal one of trustedRoots).
Implementation notes:
- JWT payloads in v0.4.0 are signed by the leaf agent's own keypair (the
one bound in the last
ChainLink). This is a change from v0.3.x where children's JWTs were signed by the parent's keys. renewOvidcan only renew root tokens. Chained tokens cannot be renewed in place because only the parent holds the key needed to sign a new chain link — request a fresh token from the parent instead.- Legacy (pre-0.4.0) tokens with
string[]parent_chainare still accepted byverifyOvidvia a fallback path, but their chains are not cryptographically walkable. A one-time deprecation warning is emitted per process.
API
generateKeypair(): Promise<KeyPair>
Generates an Ed25519 keypair using the Web Crypto API.
exportPublicKeyBase64(key: CryptoKey): Promise<string>
Exports a public key as a base64url string.
createOvid(options: CreateOvidOptions): Promise<OvidToken>
Issues a new OVID JWT.
| Option | Type | Required | Default | Description |
|--------|------|----------|---------|-------------|
| issuerKeys | KeyPair | yes | — | Issuing agent's keypair |
| issuerOvid | OvidToken | no | — | Parent's OVID (omit for root) |
| mandate | CedarMandate | yes | — | Cedar policy set |
| issuer | string | no | — | Issuer ID |
| agentId | string | no | auto | Unique agent ID |
| ttlSeconds | number | no | 1800 | Time to live |
| kid | string | no | — | Key ID for JWT header |
verifyOvid(jwt, options): Promise<OvidResult>
Verifies an OVID JWT's signature and full delegation chain. The modern form takes an options object:
verifyOvid(jwt, { trustedRoots: [rootPublicKey], maxChainDepth: 5 });Returns { valid, principal, mandate, chain, expiresIn }. A legacy single-key overload verifyOvid(jwt, publicKey) still works but is deprecated and emits a one-time warning.
Mandate Builder
Writing raw Cedar inside a spawn task is error-prone. buildMandate() compiles a structured intent into a Cedar policySet that the OVID-ME evaluator and cedar-wasm both accept — so the same mandate is enforceable and provable. This is the accurate on-the-fly authoring path: fill a form, not freeverse Cedar.
import { buildMandate, buildMandateTag } from '@clawdreyhepburn/ovid';
const { policySet, summary, warnings } = buildMandate({
ttlSeconds: 1800,
allow: [
{ action: 'read', resource: { type: 'File', pathLike: ['**/workspace/**'] } },
{ action: 'exec', resource: { type: 'Shell', in: ['git', 'gh', 'npm'] } },
{ action: 'fetch', resource: { type: 'API', in: ['api.github.com'] } },
],
forbid: [
{ action: 'exec', resource: { type: 'Shell', in: ['rm', 'sudo'] } },
],
});
// policySet is ready to sign into an OVID mandate; forbid always wins.Vocabulary (shared across the stack, src/schema/vocabulary.ts):
- Actions:
read write edit exec fetch search browse send delegate remember recall call_tool summarize - Resource kinds:
File Shell Tool WebEndpoint Channel Memory Session(APIis accepted and normalized toWebEndpoint) - Default (no intent):
read,search,summarize
Grant shapes:
| Intent | Emitted Cedar |
|---|---|
| { action: 'read' } | permit(principal, action == Ovid::Action::"read", resource); |
| { action: ['read','write'] } | ... action in [Ovid::Action::"read", Ovid::Action::"write"] ... |
| { action:'exec', resource:{ type:'Shell', in:['git'] } } | ... resource == Ovid::Shell::"git" |
| { action:'read', resource:{ type:'File', pathLike:['/src/*'] } } | ... resource) when { resource.path like "/src/*" } |
| { effect:'forbid', ... } | forbid(...) (always wins) |
For spawning sub-agents, buildMandateTag() returns a ready-to-prepend block that the openclaw-ovid hook parses:
const { tag } = buildMandateTag({ ttlSeconds: 1800, allow: [{ action: 'read' }] });
// tag =
// [OVID_TTL:1800]
// [OVID_MANDATE]
// permit(principal, action == Ovid::Action::"read", resource);
// [/OVID_MANDATE]
await sessions_spawn({ task: `${tag}\n\n${realTask}` });Ids and path globs are validated against a conservative charset; unsafe values throw rather than emit injectable Cedar. Unknown actions are dropped with a warning. Empty grants compile to an explicit deny-all.
Mandate Evaluation
Looking for Cedar policy evaluation, enforcement, audit logging, and a forensics dashboard?
See @clawdreyhepburn/ovid-me (OVID Mandate Evaluation) — reads mandates from verified OVID tokens, evaluates tool calls against Cedar policies, provides three enforcement modes (enforce/dry-run/shadow), and includes a full audit + dashboard system.
OVID JWT Format
An OVID is a JWT compliant with RFC 7519, signed with EdDSA (Ed25519), with the dedicated media type ovid+jwt. The mandate travels in the authorization_details claim (RFC 9396) using the cedar profile from draft-cecchetti-oauth-rar-cedar-02.
Header
{ "alg": "EdDSA", "typ": "ovid+jwt" }| Claim | Required | Notes |
|-------|----------|-------|
| alg | yes | Always EdDSA (Ed25519). |
| typ | yes | Always ovid+jwt. Distinguishes OVIDs from generic JWTs at parse time. |
Payload — root token
A root token (depth 1) is one a top-level agent issues to itself. Its parent_chain contains exactly one self-signed ChainLink, anchoring the chain to a trustedRoots key supplied at verify time.
{
"jti": "clawdrey/agent-7f3a",
"iss": "clawdrey",
"sub": "clawdrey/agent-7f3a",
"iat": 1777561629,
"exp": 1777563429,
"authorization_details": [
{
"type": "agent_mandate",
"rarFormat": "cedar",
"policySet": "permit(principal, action == Ovid::Action::\"read_file\", resource);",
"parent_chain": [
{
"sub": "clawdrey/agent-7f3a",
"agent_pub": "AVQXD2Fw6fdYMoFCMsYxTZ-km-Z9ZmmoBnlLIWOdPjo",
"iat": 1777561629,
"exp": 1777563429,
"sig": "KxbNAyLTXYPW6uBXrwPOwrw1h976K8SJZqeNZWF7WmreFEPKTgm0p-4I1m--16x-l16jWcoCPtszJ-pND3HUCw"
}
],
"agent_pub": "AVQXD2Fw6fdYMoFCMsYxTZ-km-Z9ZmmoBnlLIWOdPjo",
"ovid_version": "0.4.1"
}
]
}Payload — delegated token (depth 2)
When a parent agent spawns a child, the child gets a fresh keypair and a new OVID with a parent_chain that grows by one link. The new link is signed by the parent's agent_pub and binds the child's sub and agent_pub. Lifetime is attenuated: iat and exp are clamped inside the parent's window.
{
"jti": "clawdrey/agent-7f3a/reviewer-9d2b",
"iss": "clawdrey",
"sub": "clawdrey/agent-7f3a/reviewer-9d2b",
"iat": 1777561629,
"exp": 1777562229,
"authorization_details": [
{
"type": "agent_mandate",
"rarFormat": "cedar",
"policySet": "permit(principal, action == Ovid::Action::\"read_file\", resource == Ovid::Resource::\"/tmp/report.md\");",
"parent_chain": [
{
"sub": "clawdrey/agent-7f3a",
"agent_pub": "AVQXD2Fw6fdYMoFCMsYxTZ-km-Z9ZmmoBnlLIWOdPjo",
"iat": 1777561629,
"exp": 1777563429,
"sig": "KxbNAyLTXYPW6uBXrwPOwrw1h976K8SJZqeNZWF7WmreFEPKTgm0p-4I1m--16x-l16jWcoCPtszJ-pND3HUCw"
},
{
"sub": "clawdrey/agent-7f3a/reviewer-9d2b",
"agent_pub": "4-1bUD-aCMszelJA_ZN15hwEWEf_yuU0mz1vq9qFDI4",
"iat": 1777561629,
"exp": 1777562229,
"sig": "pGmrWMsdRy1A_jYjo7SmO1s1TGMd2rvQlvvkP2O1cKGoysbwVpJKcItiDhACTZsT588V7P4I6g_eggqKOYCLCg"
}
],
"agent_pub": "4-1bUD-aCMszelJA_ZN15hwEWEf_yuU0mz1vq9qFDI4",
"ovid_version": "0.4.1"
}
]
}Multi-hop chains (depth 3 and beyond)
A helper can spawn its own helper, which can spawn another, and so on. Each hop adds one signed link. The verifier walks the whole chain and enforces two rules at every step, not just the first:
- Lifetime can only shorten — each link's expiry is clamped inside its parent's.
- Each link is signed by its immediate parent's key — a grandchild's link must be signed by its parent, not by the root. A link signed by the wrong key fails verification.
This means the traceable chain-of-custody holds no matter how deep the delegation goes (up to maxChainDepth, default 5). The companion library @clawdreyhepburn/ovid-me additionally proves that each hop's permissions only ever narrow — a grandchild can never hold more authority than its parent, and that is checked with a formal proof engine at issuance time.
Top-level claims
| Claim | Type | Required | Notes |
|-------|------|----------|-------|
| jti | string | yes | JWT ID. By convention the agent's path-style identifier (<parent>/<child>). |
| iss | string | yes | Issuer ID — the human or organization the root agent serves. |
| sub | string | yes | Subject — the agent this token identifies. Equal to jti for OVIDs. |
| iat | number | yes | Issued-at, unix seconds. Must be >= parent's iat. |
| exp | number | yes | Expiry, unix seconds. Must be <= parent's exp (lifetime attenuation). |
| authorization_details | AuthorizationDetail[] | yes | RFC 9396 carrier for the agent's mandate(s). OVID currently issues exactly one entry. |
| parent_ovid | string | legacy only | Pre-0.4.x tokens recorded the parent's sub here. Modern verifiers ignore it; the source of truth is authorization_details[0].parent_chain. |
authorization_details entry (the mandate)
Each entry is an AuthorizationDetail — RFC 9396 with the cedar profile from draft-cecchetti-oauth-rar-cedar-02.
| Field | Type | Required | Notes |
|-------|------|----------|-------|
| type | string | yes (RFC 9396) | Always agent_mandate for OVID. |
| rarFormat | "cedar" | yes | Selects the Cedar profile. |
| policySet | string | yes | Cedar policy text. The agent's mandate. |
| parent_chain | ChainLink[] | yes (v0.4.0+) | Cryptographic delegation chain, root first, leaf last. Pre-0.4 tokens used string[] of subs and are accepted via a fallback path but are not cryptographically verifiable. |
| agent_pub | string | yes | Base64url Ed25519 public key bound to this token's sub. Equal to the leaf link's agent_pub. |
| ovid_version | string | yes (modern) | Wire protocol version (currently "0.4.0"), not the npm package version. Verifiers branch on this for shape compatibility. Bumping @clawdreyhepburn/ovid on npm does not change this field. See OVID_PROTOCOL_VERSION / CHAIN_PROTOCOL_VERSIONS in the package exports. |
ChainLink
Each link is a parent-signed attestation that some sub controls some agent_pub for some validity window.
| Field | Type | Notes |
|-------|------|-------|
| sub | string | Subject this link represents. |
| agent_pub | string | Base64url Ed25519 public key bound to sub. |
| iat | number | Issued-at. Must be >= parent link's iat. |
| exp | number | Expiry. Must be <= parent link's exp. |
| sig | string | Base64url Ed25519 signature over the canonical bytes below, produced by the parent link's agent_pub. The root link is self-signed. |
Canonical signed bytes (UTF-8, byte-exact for interop):
ovid-chain-link/v1\n<sub>\n<agent_pub>\n<iat>\n<exp>iat and exp are decimal integers with no leading zeros.
Verification
verifyOvid(jwt, { trustedRoots, maxChainDepth }) (the preferred form) checks, in order:
- JWT signature (EdDSA) using
issuerPublicKey. typ === "ovid+jwt".iat,expagainst current time.parent_chainis non-empty and bounded bymaxChainDepth(default 5).- The leaf link's
subandagent_pubmatch the token'ssubandauthorization_details[0].agent_pub. - Each link's
iat/expis within its parent's window (lifetime attenuation). - Each non-root link's
sigverifies under its parent'sagent_pub. - The root link is self-signed and its
agent_pubis a member oftrustedRoots.
A token that fails any check returns { valid: false, ... }. A passing token returns { valid: true, principal, mandate, chain, expiresIn }.
Development
git clone https://github.com/clawdreyhepburn/ovid.git
cd ovid
npm install
npm test # 48 tests via vitest
npm run build # TypeScript → dist/Project structure
ovid/
├── src/
│ ├── index.ts # Public API exports
│ ├── keys.ts # Ed25519 keypair generation
│ ├── create.ts # OVID issuance with lifetime attenuation
│ ├── verify.ts # Signature verification and claims validation
│ └── types.ts # TypeScript interfaces (including CedarMandate)
├── test/
│ ├── keys.test.ts
│ ├── create.test.ts
│ ├── verify.test.ts
│ ├── chain.test.ts
│ ├── renew.test.ts
│ ├── delegation.test.ts
│ └── depth3-chain-construction.test.ts # multi-hop chain soundness
├── docs/
│ └── SECURITY.md
├── ARCHITECTURE.md
├── LICENSE
├── NOTICE
└── package.jsonHow OVID Fits the Stack
OVID provides identity and mandates — it tells you who a sub-agent is and what authority was delegated to it. But OVID itself doesn't enforce anything. Enforcement is handled by two complementary layers:
Carapace — the deployment-level ceiling. The human defines what tools are allowed at all via Cedar policies, enforced on every
before_tool_callhook. Binary allow/deny. This is the human's hard limit — no agent can exceed it regardless of what mandate it carries.OVID-ME — mandate evaluation. Reads the Cedar policy from a verified OVID token and evaluates whether the specific tool call is permitted by the parent's delegation. Three modes: enforce, dry-run, shadow.
Both must allow a tool call to proceed. Carapace gates what the human permits; OVID-ME gates what the parent delegated. A sub-agent with a broad mandate still can't exceed the deployment ceiling, and a sub-agent under a permissive deployment ceiling still can't exceed its parent's mandate.
Tool call arrives
│
├─ Carapace: "Does the deployment policy allow this?" ── deny ──> blocked
│ │
│ allow
│ │
├─ OVID-ME: "Does the agent's mandate allow this?" ── deny ──> blocked
│ │
│ allow
│ │
└─ Tool executesRelated Projects
The libraries:
- @clawdreyhepburn/ovid-me — Cedar policy evaluation for OVID mandates (enforcement, audit, dashboard). This library is the companion enforcer to OVID's identity.
The ready-to-use OpenClaw plugins (install these if you just want it to work, no coding):
- @clawdreyhepburn/openclaw-ovid — automatically issues an OVID badge to every sub-agent your assistant spawns (built on this library).
- @clawdreyhepburn/openclaw-ovid-me — checks those badges on every action and allows/logs/blocks accordingly.
- @clawdreyhepburn/carapace — the human-set deployment ceiling: the absolute limit no badge can exceed.
License
Copyright 2026 Clawdrey Hepburn LLC. Licensed under Apache-2.0.
