The certificate ledger
Every permission anyone holds in VaultysClaw is a row in one table:
CapabilityCertificate. Signed, time-boxed, individually revocable, and
verifiable by anyone, offline, without calling the control plane.
This replaces two things the previous design had: mutable unsigned policy rows, and a role enum. Neither survives.
Why a ledger and not a policy table
A policy row is a statement of intent. A certificate is a record of what was actually issued, to whom, by whom, and when — an artefact that outlives the database it lives in and can be checked by a party that has no access to that database.
That property is what makes the rest of the Zero Trust story possible. A third party can be handed a certificate and a signed status response and decide for itself whether to extend trust. With a policy table, it can only ask the control plane and believe the answer.
The shape
model CapabilityCertificate {
id String @id
agentDid String // the subject — any Actor, human or not
workspaceId String?
capabilities Json @default("[]")
resourceLimits Json?
certFormat String @default("packcert") // "packcert" | "challenger"
certificate String // the artefact itself, base64
requestCertificate String? // packcert only — the embedded request
status String @default("active") // active | revoked | superseded | expired
issuedAt DateTime @default(now())
expiresAt DateTime? // null = until revoked (rare, deliberate)
revokedAt DateTime?
revokedBy String?
revokedReason String?
supersededByCertId String?
}
Rows are never deleted, and never mutated after issue except to flip status
and fill the revocation or supersession fields. That is what makes this an audit
trail rather than a cache.
An Actor holds a set, not a certificate
Nothing here is unique per Actor. Certificates are keyed by subject DID, and an Actor routinely holds several active at once:
- a long-lived standing grant —
agent_communication, expiring in days; - layered on top, one or more short-lived, narrowly scoped grants — read this one file, valid for the next ten seconds.
Permission is therefore resolved per action over a set, never per Actor over a singleton:
resolvePermission(
action: { capability: AgentCapability; resource?: string },
activeCerts: CapabilityCertificate[],
now: number
): { allowed: boolean; grantingCertId?: string }
The returned grantingCertId is the point. An action is not authorised by "some
capability the Actor happened to hold" — it is authorised by a specific,
citable certificate, and the audit log records which one.
This function lives in packages/trust, has no database and no network access,
and is deliberately isolated so a security audit can point at one self-contained
artefact. It exists a second time in Go, inside the interception point, and both
implementations run the same committed conformance vectors.
See Capabilities & scope.
Two certificate formats
Two issuance situations exist, because sometimes there is a live counterpart to
negotiate with and sometimes there is not. Anything decoding a certificate
branches on certFormat first.
challenger — interactive issuance
Used whenever the Actor asking for capabilities is online. The same SRP-style
Challenger protocol that authenticates a connection is run a second time with
service: "certificate" instead of "auth", carrying the approved capabilities
as metadata.
The result is the library's own native dual-signature certificate — both parties'
keys and signatures as first-class fields (pk1/pk2/sign1/sign2/
metadata) — verifiable later by anyone, offline.
This is the stronger path: it requires both the Actor and the control plane to
be mutually, cryptographically present at the moment of issuance. requestCertificate
is null for these rows because the co-signature is native to the artefact.
packcert — system or admin issued
Used when there is no live counterpart: the bootstrap admin grant, an admin issuing a grant directly, an approval for an Actor that is currently offline.
Two nested tokens — a capability_request embedded inside a capability_grant,
both signed by the control plane. The honest audit signal for this path is
precisely that both halves verify against the same key: nobody outside asked for
this, the system or an admin decided it directly. The console shows which key
verified each half, so that signal is inspectable rather than implied.
It follows from the situation. An online Actor negotiating its own grant gets
challenger; an offline or system-originated grant gets packcert. Both are
independently verifiable; they differ in what they prove about liveness.
Expiry
Time-boxed is the default. expiresAt: null is a deliberate, narrow
exception, meaning "valid until explicitly revoked" — no renewal cadence, no
automatic lapse.
A null expiry does not weaken the revocation model: an indefinite certificate is neutralised exactly like any other, by flipping its status. It only removes the time dimension.
The canonical use is the bootstrap admin grant. Issuing another indefinite certificate should be a conscious, reviewable choice, so the console:
- renders "Never" in warning amber, never as neutral text;
- requires an explicit confirmation checkbox on the issuance form;
- flags a system-issued no-expiry grant in the ledger so the first real admin sees it immediately and can reissue it with an expiry if they want a stricter posture.
Loud, not silent. That is the rule for this whole class of exception.
Revocation is a ledger write
Revocation is not a network action.
An admin revokes → the row's status flips to revoked with a required reason. The
control plane still attempts a cooperative push to the Actor, which the Actor may
honour. But the authoritative effect is that every status check from that moment
returns revoked, to every party that asks — including the control plane's own
dispatcher.
This is the reframing that makes revocation meaningful against code you do not
control. A modified binary can ignore a push. It cannot make a signed status
response say active.
See Trust verification.
Renewal
Two modes, for standing grants only:
- Reactive — the Actor notices its certificate is near expiry and requests again, running the same issuance flow.
- Proactive — a scheduled scan finds certificates expiring soon and pushes a fresh grant over the already-open connection, so there is no window where the Actor runs uncertified.
Scoped and short-TTL certificates are excluded from proactive renewal. A certificate issued for one file read in the next ten seconds should expire and disappear, not be silently refreshed. Renewal is a property of standing grants.
Inspecting a certificate
The console's certificate detail view shows, for any row:
- the full raw and decoded payload — for
packcert, both the grant and the embedded request; - which key actually verifies each signature;
- an independent live re-verification against the stored bytes, not a stored validity flag;
- the status-check history — every party that asked about this certificate, and when.
Older rows recorded before public keys were captured show "could not verify" gracefully rather than claiming a verification that did not happen.
Bootstrap: the first certificate
Gating admin access behind a certificate creates an obvious chicken-and-egg problem — issuance normally requires an existing admin to approve.
The exception fires at most once per deployment: on the first human login
where no Actor anywhere holds an active admin_console_access certificate, the
control plane issues one to that DID with no approval step, attributed to
system:bootstrap, with no expiry.
The existence check and the insert are atomic — a fixed certificate id doubles as the lock — so two humans registering in the same race window cannot both walk through the gap. The moment any such certificate exists, every later registration falls through to normal approval.