A trust plane, not a framework. Keep the orchestration you already run and give every actor — software agent, robot, drone, vehicle, sensor, or human — a cryptographic identity and a signed, revocable capability certificate. The control plane distributes those certificates once; after that any two peers verify each other directly, with nothing in the middle.
$ pnpm add @vaultysclaw/sdk $ node ./agent.js ✓ identity loaded did:vaultys:z6MkwF3jA7Qx… ✓ auth handshake complete (service: auth) … unknown DID → registration_pending waiting for an admin decision… ✓ cert_issued · 2 of 3 requested capabilities granted api_call, acme:invoice.approve withheld file_access ✓ staple verified · refresh every 150s > erp_approve_invoice allowed > read_ledger_dump denied (file_access not granted)
Bring your own orchestration — VaultysClaw governs it as an Actor
The scope
Orchestration is solved. Knowing who an actor is and what it may do, provably, is not — so that is all VaultysClaw answers.
Certificate distribution
This is the architectural choice everything else follows from. A capability is a signed certificate, not a lookup against a server — so once it has been distributed, any two actors can establish what the other is allowed to do without either of them talking to us.
The control plane signs a certificate and hands it over. That exchange is the only time it has to be reachable — the grant is a durable artefact the holder keeps, not a session it has to maintain.
A certificate is signed by the control plane and by its holder, so a second actor verifies it with the issuer's public key alone. No callback, no token introspection endpoint, no shared secret between the two.
Once distributed, decisions happen where the work happens. There is no broker holding every actor's authority, and no bottleneck that turns an outage into a fleet-wide denial.
A staple has an age, and trust.maxStatusAgeSeconds says how stale is tolerable. An air-gapped robot can be told to accept an hour-old staple; a payment agent, none at all.
Actors, not just agents
An Actor is anything that holds a keypair and takes actions. The protocol never asks whether a model is involved — which is why a drone, a factory cell, and a payments agent are governed by exactly the same ledger, the same certificates, and the same revocation.
The SDK
TypeScript and Go, both driven against a real control plane, both resolving permissions through the same 37 conformance vectors. No tool registry to adopt — the SDK owns one seam: which capability gates which of your operations.
import { ActorRuntime, loadOrCreateIdentity } from "@vaultysclaw/sdk";
const runtime = new ActorRuntime({
name: "invoice-approver",
kind: "openclaw",
controlPlaneWsUrl: "wss://cp.acme.internal:8081",
identityPath: "~/.acme/identity.key",
capabilityStatePath: "~/.acme/capabilities.json",
capabilityManifestPath: "./capabilities.json",
// A request, not a declaration. An admin may approve less —
// and your Actor has to work correctly holding less.
requestedCapabilities: ["api_call", "file_access", "acme:invoice.approve"],
});
await runtime.start(); // handshake → maybe pending → cert_issued
// Gate your own operation, whatever an "operation" is for you:
// a tool call, an HTTP route, a button, a queue consumer.
if (await runtime.isOperationAllowed("erp_approve_invoice", "invoice:8812")) {
await erp.approveInvoice("8812");
}
// A revocation, or a custom capability deleted from the registry,
// arrives here — rebuild any cached authorisation on this event.
runtime.on("capabilities", (held) => cache.rebuild(held));An unbound operation, an ungranted capability, and an unreachable control plane all deny. There is deliberately no “nothing granted means allow everything” path anywhere in the package.
refreshCertStatus() verifies the signed response against the control plane's key from the handshake — not merely because it arrived on an authenticated socket — and the verified result replaces the held set. That is how a revocation lands.
Cadence comes from actor_config: refresh at half of maxStatusAgeSeconds, and 0 means no cached status is acceptable at all. No trust config is treated as failClosed.
resolvePermission() is synchronous and decides on what is already known. Certificates are signed artefacts — packages/policy verifies one with no network and no database.
Certificate lifecycle
Capabilities are either built-ins from a closed list or admin-defined vendor:action names from the registry. Both travel through identical machinery — the same certificates, scoping, expiry, revocation, and resolution.
The Actor connects and proves its DID with a Challenger handshake. An unknown DID becomes a pending registration — it is connected, and nothing resolves.
An admin picks capabilities from what was requested. A second, independent certificate handshake signs the grant; cert_issued delivers it.
The Actor gates its own operations on what it holds. Scope, expiry, and resource limits are checked by pure code with an injected clock.
cert_status_request returns a signed status the holder verifies and staples. A revoked grant, or a deleted custom capability, stops resolving here.
Every decision lands in the append-only audit log, attributed to a DID and keyed to the certificate that authorised it.
Architecture
One Next.js + WebSocket process over Postgres, holding the certificate ledger and the admin console. Actors dial outbound, so there are no inbound firewall rules to negotiate — and no actor traffic transits anything of ours, because nothing of ours is in the path.
Control plane · try it
The admin console, wired to a fake ledger that behaves like the real one. Tick the capabilities you would actually grant, approve an Actor, then revoke its certificate and see what the holder loses.
Every number above is derived from the state your clicks change. Nothing here is a screenshot.
Powered by VaultysId
Every Actor holds a VaultysId DID that is uniquely, irrevocably its own. Not a session token you hand out. Not an API key you can paste into a second process and become two agents at once.
TS ↔ Go parity
An authorization decision that differs between a Go robot and a TypeScript agent is a security bug. So the logic is not documented as identical — it is tested as identical, from fixtures neither side may change alone.
permission-vectors.json37 casesRun by both packages/trust and sdk-go/authz. If the two ever disagree about a permission, a suite goes red.
capability-names.json31 casesOne grammar for capability names, mirrored in packages/policy and sdk-go/capability. Never validate a name by hand.
grant-fixture.jsonTS-signedSigned on the TypeScript side, verified on the Go side. Offline grant verification, proven across the boundary.
Open source · Self-hosted · Public alpha
A control plane you run, certificates any peer can verify, and two SDKs that refuse to guess. Ten minutes to a connected Actor holding a real grant.