@vorim/sdk
v3.23.0
Published
Official TypeScript SDK for Vorim AI — AI Agent Identity, Permissions & Audit
Maintainers
Readme
@vorim/sdk
The identity and trust layer for AI agents.
Register agents with cryptographic Ed25519 identities, enforce scoped permissions in under 5ms, emit tamper-evident audit trails, and verify trust scores — all in a few lines of code.
API key: create one in the Vorim dashboard under Settings, API keys. No account yet? Request access at vorim.ai. Documentation — Full API reference, framework integrations, and examples. Quick Start — Set up in under 5 minutes.
Why Vorim?
AI agents are shipping into production without identity, permissions, or audit trails. This is a problem:
- No identity — agents share API keys. You can't tell which agent did what.
- No permissions — agents get all-or-nothing access. No scoped, time-bounded controls.
- No audit trail — no tamper-evident record of agent actions. Compliance teams are blind.
- No trust signal — third parties can't verify an agent before interacting with it.
Vorim solves all four. One SDK. One protocol. Ships in minutes.
EU AI Act (in force since Aug 2024, high-risk obligations phasing in through 2026–2027) mandates traceability and audit trails for high-risk AI systems. Vorim gives your agents that traceability out of the box.
Install
npm install @vorim/sdk# or
yarn add @vorim/sdk
# or
pnpm add @vorim/sdkQuick Start
The key for this quick start needs the agents:read, agents:write, permissions:read, audit:read and audit:write scopes. Scopes are independent, so agents:write does not include agents:read.
The example uses top-level await, so run it as ES module code. Save it as a .mjs file, set "type": "module" in your package.json, or run the .ts file with npx tsx.
import createVorim from "@vorim/sdk";
const vorim = createVorim({
apiKey: "agid_sk_live_...",
});
// 1. Register an agent — Ed25519 keypair generated, private key shown once
const { agent, private_key } = await vorim.register({
name: "invoice-processor",
capabilities: ["read_documents", "extract_data"],
scopes: ["agent:read", "agent:execute"],
});
console.log(agent.agent_id); // agid_acme_a1b2c3d4
console.log(agent.trust_score); // 50 (initial)
// 2. Check permissions before acting (<5ms via Redis)
const { allowed } = await vorim.check(agent.agent_id, "agent:execute");
if (allowed) {
// 3. Perform the action, then emit an audit event
await vorim.emit({
agent_id: agent.agent_id,
event_type: "tool_call",
action: "process_invoice",
resource: "INV-2026-0042",
result: "success",
latency_ms: 142,
});
}
// 4. Verify any agent's trust (public endpoint, no auth required)
const trust = await vorim.verify(agent.agent_id);
console.log(trust.trust_score); // 0–100
// The public endpoint does not reveal scopes (active_scopes is always []).
// As the agent's owner, list them with your API key:
const permissions = await vorim.listPermissions(agent.agent_id);Framework Integration
LangChain
import createVorim from "@vorim/sdk";
import { ChatOpenAI } from "@langchain/openai";
import { AgentExecutor } from "langchain/agents";
const vorim = createVorim({ apiKey: "agid_sk_live_..." });
// Register your LangChain agent with Vorim
const { agent: identity } = await vorim.register({
name: "langchain-research-agent",
capabilities: ["web_browsing", "api_calls"],
scopes: ["agent:read", "agent:execute", "agent:communicate"],
});
// Check permissions before every tool call
async function guardedToolCall(toolName: string, input: any) {
const { allowed } = await vorim.check(identity.agent_id, "agent:execute");
if (!allowed) throw new Error("Permission denied by Vorim");
const result = await executeTool(toolName, input);
// Audit every action
await vorim.emit({
agent_id: identity.agent_id,
event_type: "tool_call",
action: `${toolName}: ${JSON.stringify(input)}`,
result: "success",
});
return result;
}CrewAI
import createVorim from "@vorim/sdk";
const vorim = createVorim({ apiKey: "agid_sk_live_..." });
// Register each crew member as a Vorim agent
const researcher = await vorim.register({
name: "crew-researcher",
capabilities: ["web_browsing"],
scopes: ["agent:read"],
});
const writer = await vorim.register({
name: "crew-writer",
capabilities: ["file_access"],
scopes: ["agent:read", "agent:write"],
});
// Verify permissions before delegation
const { allowed } = await vorim.check(writer.agent.agent_id, "agent:write");OpenAI Agents SDK
import createVorim from "@vorim/sdk";
import OpenAI from "openai";
const vorim = createVorim({ apiKey: "agid_sk_live_..." });
const openai = new OpenAI();
// Register your OpenAI agent
const { agent: identity } = await vorim.register({
name: "openai-assistant",
capabilities: ["api_calls", "code_execution"],
scopes: ["agent:read", "agent:execute"],
});
// Wrap function calls with Vorim permission checks
async function handleFunctionCall(call: any) {
const { allowed } = await vorim.check(identity.agent_id, "agent:execute");
if (!allowed) return { error: "Blocked by Vorim trust layer" };
const result = await executeFunction(call);
await vorim.emit({
agent_id: identity.agent_id,
event_type: "tool_call",
action: call.name,
resource: JSON.stringify(call.arguments),
result: "success",
});
return result;
}API Reference
Identity
// Register a new agent (private key returned once — store it securely)
const { agent, private_key } = await vorim.register({
name: "my-agent",
description: "Processes invoices",
capabilities: ["read_documents"],
scopes: ["agent:read", "agent:execute"],
});
// Get agent details
const agent = await vorim.getAgent("agid_acme_a1b2c3d4");
// List all agents in your organisation
const { agents, meta } = await vorim.listAgents({ page: 1, per_page: 20 });
// Permanently revoke an agent
await vorim.revoke("agid_acme_a1b2c3d4");Permissions
// Check if an agent has a specific permission (<5ms)
const { allowed, remaining_quota } = await vorim.check(
"agid_acme_a1b2c3d4",
"agent:execute"
);
// Grant a time-bounded, rate-limited permission
await vorim.grant("agid_acme_a1b2c3d4", "agent:transact", {
valid_until: "2026-06-01T00:00:00Z",
rate_limit: { max: 100, window: "1h" },
});Permission Scopes
| Scope | Description |
|-------|-------------|
| agent:read | Read data and resources |
| agent:write | Create or modify resources |
| agent:execute | Execute tools and functions |
| agent:transact | Perform financial transactions |
| agent:communicate | Send messages and notifications |
| agent:delegate | Delegate tasks to other agents |
| agent:elevate | Request elevated privileges |
Audit
// Emit a single audit event
await vorim.emit({
agent_id: "agid_acme_a1b2c3d4",
event_type: "tool_call",
action: "send_email",
resource: "[email protected]",
result: "success",
latency_ms: 230,
metadata: { template: "invoice_reminder" },
});
// Batch emit up to 1,000 events
await vorim.emitBatch([
{ agent_id: "agid_acme_a1b2c3d4", event_type: "api_request", action: "GET /users", result: "success" },
{ agent_id: "agid_acme_a1b2c3d4", event_type: "api_request", action: "POST /orders", result: "denied" },
]);Runtime Control (gate actions before they happen)
Ask Vorim whether an action should proceed before your agent performs it.
beforeAction() returns a typed decision and, by default, throws on deny —
because a denial is carried in the response body, not the HTTP status, so
without throwOnDeny you'd treat a denial as success.
import { VorimDeniedError } from "@vorim/sdk";
try {
const decision = await vorim.beforeAction({
agentId: "agid_acme_a1b2c3d4", // always the public agid_* id
actionType: "tool_call",
actionTarget: "sendEmail",
requiredScope: "agent:communicate",
payload: { to: "[email protected]", body: "..." },
});
// 'modify' verdicts hand back a sanitised payload (e.g. PII masked).
const payload = decision.modifiedPayload ?? { to: "[email protected]" };
if (decision.decision === "allow" || decision.decision === "modify") {
await sendEmail(payload);
} else if (decision.decision === "escalate") {
// A human must approve. Poll until resolved (or timeout).
const resolved = await vorim.waitForDecisionResolution(decision.decisionId);
if (resolved.decision === "allow") await sendEmail(payload);
}
// Link the post-action audit event back to the decision.
await vorim.emit({
agent_id: "agid_acme_a1b2c3d4",
event_type: "tool_call",
action: "sendEmail",
result: "success",
decision_id: decision.decisionId, // ← correlates audit ↔ decision
});
} catch (err) {
if (err instanceof VorimDeniedError) {
// err.decision carries the reason and decisionId.
console.warn("Action denied:", err.decision.reason);
} else {
throw err;
}
}Verdicts: allow · deny (throws VorimDeniedError by default) ·
modify (use decision.modifiedPayload) · escalate (poll
waitForDecisionResolution) · fallback (engine couldn't decide).
modifyis client-cooperative. Vorim returns the sanitisedmodifiedPayload; your agent must send it in place of the original. The platform does not sit inline and does not currently enforce that you do — carrydecisionIdinto the matchingemit()so the action stays auditable.
Fail-open: if the decision API is unreachable, beforeAction() returns a
synthetic fallback decision so a control-plane blip doesn't block your agent.
Pass runtimeFailOpen: false to the constructor to fail closed instead. A
reachable server returning deny always denies, regardless of this flag.
Requires an API key with the
runtime:decidescope and a Growth+ plan.modifyverdicts are produced by policy rules; the rule-authoring API ships in a later release — until then rules are provisioned by Vorim.
Trust Verification
// Public endpoint — no API key required
const trust = await vorim.verify("agid_acme_a1b2c3d4");
trust.verified; // true
trust.trust_score; // 82
trust.status; // 'active'
trust.owner.org_name; // 'Acme Corp'
trust.active_scopes; // [] (not revealed publicly; owners use vorim.listPermissions(agentId))
trust.key_fingerprint; // '' (not revealed publicly)
trust.revocation_status; // falseTrust score factors:
- Agent status (active, suspended, revoked)
- Account age (older = more trusted)
- Success rate over last 30 days
- Denial ratio (high denials = lower trust)
- Scope breadth (too many scopes = higher risk)
Per-event signing (auto-signing)
From v3.1.0, the SDK signs every audit event at source with the agent's Ed25519 private key. No code change required — register() caches the key in memory and emit() adds the signature transparently.
const { agent } = await vorim.register({ name: "agent", capabilities: [], scopes: ["agent:execute"] });
// Auto-signed. The signature is attached before the request leaves the process.
await vorim.emit({
agent_id: agent.agent_id,
event_type: "tool_call",
action: "transfer_funds",
result: "success",
});To verify signatures server-side, the API operator sets VORIM_VERIFY_AUDIT_SIGNATURES=true.
Canonical form. Since 3.4.0 the SDK defaults to v1 canonical form: RFC 8785 JSON Canonicalization Scheme (JCS) over the whole event minus signature and canonical_form. This brings metadata, replayable-evidence fields (model_version, tool_catalogue_hash, system_prompt_hash, prev_event_hash), and delegation context (on_behalf_of, delegator_agent_id, delegation_chain_id, delegation_depth) under the signature. The previous v0 form was a pipe-joined six-field string event_type|action|resource|input_hash|output_hash|result and is now deprecated — passing canonicalForm: 'v0' explicitly still works for verifier-compat scenarios but emits a deprecation warning. Use @vorim/[email protected]+ to verify v1 events offline. Both forms are byte-equivalent across the TypeScript and Python SDKs and the server, locked by scripts/check-replay-parity.sh.
Restoring keys across process restarts. The in-memory keyring is lost on restart. Load the private key from your secret store and call:
vorim.useAgentKey(agent.agent_id, privateKeyPem);
vorim.forgetAgentKey(agent.agent_id); // revoke from memoryOpting out. Per event with { sign: false }, or globally with autoSign: false in the config:
await vorim.emit(event, { sign: false });
const vorim = createVorim({ apiKey, autoSign: false });Manual signing. For payloads you sign yourself, vorim.sign(payload, privateKeyPem) returns an ed25519: prefixed signature. Any event with a pre-existing signature field is passed through unchanged.
HTTP Message Signatures (RFC 9421)
An agent can sign the HTTP requests it sends, so the service on the other end can check which agent made the call and that nobody changed it on the way. The signature follows RFC 9421 and goes in the Signature-Input and Signature headers. When the request has a body, the SDK adds an RFC 9530 Content-Digest header and puts it under the signature, which ties the body to it too.
Signing uses the key that register() or useAgentKey() cached for that agent. keyid is the agent_id, the algorithm is ed25519, and tag is vorim.
const body = JSON.stringify({ task: "summarise", doc_id: "d_91" });
const request = {
method: "POST",
url: "https://agent-b.example.com/tasks",
headers: { "content-type": "application/json" },
body,
};
const { headers } = await vorim.signHttpRequest(agent.agent_id, request);
await fetch(request.url, {
method: "POST",
headers: { ...request.headers, ...headers }, // Signature-Input, Signature, Content-Digest
body,
});On the receiving side, verifyHttpRequest sends the request to Vorim. Vorim looks up the signing agent's registered public key and refuses agents that are suspended or revoked. The result also carries the agent's org name, status and trust score. Pass the raw body if you want the digest checked; content_digest_checked tells you whether it was.
const result = await vorim.verifyHttpRequest(
{ method: req.method, url: fullUrl, headers: req.headers, body: rawBody },
{ maxAgeSeconds: 300 },
);
if (!result.valid) throw new Error(result.reason);
console.log(result.agent_id, result.trust_score);You can also verify locally with a public key you already hold, with no call to Vorim. The standalone module works in Node, browsers and Workers, and it also signs with ECDSA P-256 keys (ecdsa-p256-sha256), which is the curve iOS Secure Enclave and Android StrongBox keys use.
import { verifyHttpRequest, signHttpRequest } from "@vorim/sdk/http-signatures";
const result = await verifyHttpRequest(request, {
publicKey: agentPublicKeyPem, // SPKI PEM, Ed25519 or P-256
maxAgeSeconds: 300,
requiredComponents: ["@method", "@target-uri", "content-digest"],
});The module passes the Ed25519 test vector in RFC 9421 Appendix B.2.6. It supports every derived component in the RFC, and the req, name and bs component parameters. sf, key and tr raise an error because they aren't supported yet.
Embeddable Trust Badge
Every registered agent gets a public SVG trust badge you can embed anywhere:
<!-- Embed in your docs, landing page, or agent marketplace listing -->
<img src="https://api.vorim.ai/v1/trust/badge/agid_acme_a1b2c3d4.svg" alt="Vorim Trust Badge" />The badge displays the agent's current trust score and updates in real-time. Use it to signal trust to end users, partners, and other agents.
Configuration
import createVorim from "@vorim/sdk";
const vorim = createVorim({
apiKey: "agid_sk_live_...", // Required — your Vorim API key
baseUrl: "https://api.vorim.ai", // Optional (default)
timeout: 10000, // Optional — request timeout in ms (default: 10000)
autoSign: true, // Optional — sign audit events at source (default: true)
});| Option | Type | Default | Description |
|--------|------|---------|-------------|
| apiKey | string | — | Your Vorim API key (agid_sk_live_...) |
| baseUrl | string | https://api.vorim.ai | API base URL (override for self-hosted) |
| timeout | number | 10000 | Request timeout in milliseconds |
| autoSign | boolean | true | Sign every audit event at source with the agent's Ed25519 key |
| noPayload | boolean \| { maxMetadataChars?: number } | false | Refuse to transmit agent content. Throws before sending on non-scalar metadata, over-long metadata strings, or a hash field that is not a well-formed digest |
Payload Privacy
Your prompts and model outputs never have to reach Vorim. The audit event carries a digest of them, computed in your process.
Commit, don't just hash
A bare SHA-256 of a prompt is not a commitment, it is a lookup key. Prompts are low-entropy and usually templated, so anyone holding the digest and the template recovers the content by trying candidates until a hash matches. That includes us, and anyone who obtains an export.
import { commitPayload, hashPayload } from "@vorim/sdk";
const key = process.env.VORIM_COMMITMENT_KEY!; // never sent to Vorim
await vorim.emit({
agent_id: agent.agent_id,
event_type: "tool_call",
action: "invoice.refund",
result: "success",
input_hash: await commitPayload(prompt, key), // hmac-sha256:...
output_hash: await commitPayload(completion, key),
});Keep hashPayload for content that is already high-entropy, such as a file
digest or a random identifier.
To disclose later, hand the verifier the content and the key; they recompute and compare. Rotating the key does not invalidate past events, so retain the key you used for as long as you might need to prove the events it covers.
Refuse to send content at all
const vorim = createVorim({ apiKey: process.env.VORIM_API_KEY!, noPayload: true });
await vorim.emit({ ...event, metadata: { tool: "stripe.refunds.create" } }); // ok
await vorim.emit({ ...event, metadata: { prompt: fullPrompt } }); // throws
// VorimError PAYLOAD_BLOCKED — nothing was sentNot the same as the server-side Zero Data Retention setting, which drops metadata when we persist it, by which point the content has crossed the network. This mode means it never left your process.
action and resource are left alone: both are short structural identifiers
covered by the signature.
Referencing an event
Every emit returns a stable content digest beside the event id, computed over
exactly the bytes the signature covers. You can recompute it yourself, and it is
the same value prev_event_hash references on the next event.
const { events } = await vorim.emit({ /* ... */ });
// events: [{ event_id: "evt_...", digest: "sha256:..." }]
import { eventDigest } from "@vorim/sdk";
await eventDigest(sentEvent) === events?.[0].digest; // trueByte-identical in @vorim/verify and the Python SDK. A content digest, not an
identity: two byte-identical events share one, which is why the event id sits
beside it.
Error Handling
All errors throw VorimError with structured fields:
import createVorim, { VorimError } from "@vorim/sdk";
try {
await vorim.check("invalid_id", "agent:read");
} catch (err) {
if (err instanceof VorimError) {
err.status; // 404
err.code; // 'AGENT_NOT_FOUND'
err.message; // 'Agent invalid_id not found in the trust registry'
err.details; // Additional context (optional)
}
}| Error Code | Status | Meaning |
|-----------|--------|---------|
| INVALID_CREDENTIALS | 401 | Bad or expired API key |
| AGENT_NOT_FOUND | 404 | Agent ID doesn't exist |
| PERMISSION_DENIED | 403 | Agent lacks required scope |
| RATE_LIMITED | 429 | Too many requests |
| VALIDATION_ERROR | 400 | Invalid request payload |
Features
| Feature | Details |
|---------|---------|
| Cryptographic Identity | Ed25519 keypairs with SHA-256 fingerprints |
| 7 Permission Scopes | read, write, execute, transact, communicate, delegate, elevate |
| Immutable Audit Trail | Append-only events with content hashing |
| Trust Scoring | 5-factor algorithm producing a 0–100 score |
| Payload Signing | Client-side Ed25519 via Web Crypto or Node.js crypto |
| Dual Runtime | Node.js 18+ and modern browsers |
| Zero Dependencies | Types bundled — nothing extra to install |
| ESM + CJS | Dual module output for any bundler or runtime |
Protocol
This SDK implements the Vorim Agent Identity Protocol (VAIP) — an open standard for AI agent identity, permissions, and cryptographic audit trails.
VAIP defines 5 conformance levels, from basic identity to full cryptographic signing. The protocol is open-source under Apache 2.0 — anyone can implement it.
Read the full specification: SPEC.md
Requirements
- Node.js 18+ or modern browser with Web Crypto API
- TypeScript 5.0+ (optional but recommended)
Links
- Website: vorim.ai
- Protocol Spec: github.com/Vorim-AI-Labs/vorim-protocol
- npm: @vorim/sdk
- X: @vorim_ai_x
- Issues: GitHub Issues
License
MIT — see LICENSE for details.
Built by Vorim AI
