@openagentid/arsenal-sdk
v0.1.1
Published
Arsenal agent-side SDK: Agent Capability Tokens (ACT), scoped credential proxy, broker client, policy engine.
Downloads
54
Maintainers
Readme
@openagentid/arsenal-sdk
Arsenal agent-side SDK for TypeScript/JavaScript. Ports the Rust
arsenal-sdk crate to a native TypeScript implementation that runs on
Node 20+, Bun, Deno, modern browsers, and Cloudflare Workers.
ARSENAL is an agent-native API key management and secure handoff
framework. Agents never see raw credentials — they receive
short-lived Agent Capability Tokens (ACTs) from a broker, and API
calls are proxied server-side so credentials are injected without
reaching the agent's process. ACTs are scoped (service:resource:action),
short-lived (30s–24h), Ed25519-signed, and bound to a
proof-of-possession key.
Install
npm install @openagentid/arsenal-sdk
# or
bun add @openagentid/arsenal-sdk
# or
pnpm add @openagentid/arsenal-sdkThe SDK has a peer dependency on @openagentid/crypto-wasm for
Ed25519, HKDF-SHA256, AES-256-GCM, and BLAKE3 primitives. Cross-SDK
signature parity (with the Rust, Go, Python, Swift, and Kotlin SDKs)
requires this peer to be installed in production. When it is absent,
the SDK falls back to WebCrypto for Ed25519/HKDF/AES-GCM and SHA-256 as
a non-wire-compatible stand-in for BLAKE3.
Quickstart
import {
ArsenalClient,
TenantId,
KeyFingerprint,
AgentIdentity,
} from "@openagentid/arsenal-sdk";
// 1. Identity — normally loaded from secure storage.
const tenantId = TenantId.generate();
const fingerprint = KeyFingerprint.fromBytes(new Uint8Array(32));
const identity = AgentIdentity.create(tenantId, "my-agent", fingerprint);
// 2. Configure the client with a broker.
const client = ArsenalClient.create({
identity,
broker: { baseUrl: "https://broker.example.com" },
});
// 3. Session → capability → proxy.
await client.startSession();
const capability = await client.requestCapabilityForScopes(
["stripe:charges:read"],
300,
);
const response = await client.proxyHttp({
method: "GET",
url: "https://api.stripe.com/v1/charges",
headers: { Authorization: "Bearer {{OAUTH2_STRIPE_TOKEN}}" },
capability_token: capability.encode(),
});
console.log(response.status, new TextDecoder().decode(response.body));The broker resolves {{OAUTH2_STRIPE_TOKEN}} server-side before
forwarding the request to api.stripe.com. The agent never sees the
real token — only the placeholder.
Modules
@openagentid/arsenal-sdk— top-level re-export (core + policy + broker + sdk)@openagentid/arsenal-sdk/core— types: tokens, scopes, constraints, consent, policy, secrets, delegation, audit, CBOR, codec@openagentid/arsenal-sdk/policy— declarative policy evaluator (PolicyEngine,evaluatePolicy)@openagentid/arsenal-sdk/broker— HTTP client for the ARSENAL broker@openagentid/arsenal-sdk/sdk— high-levelArsenalClient,CapabilityRequest,SessionManager@openagentid/arsenal-sdk/crypto— thin wrapper around@openagentid/crypto-wasm
Scope grammar
Scopes follow service:resource:action. * is a wildcard at any
position, so stripe:*:* grants full access to Stripe and *:*:read
grants read-only access across all services.
import { Scope, ScopeSet } from "@openagentid/arsenal-sdk";
const set = ScopeSet.fromStrings([
"stripe:charges:read",
"github:repos:*",
]);
set.allows(Scope.parse("stripe:charges:read")); // true
set.allows(Scope.parse("github:repos:write")); // true (wildcard action)
set.allows(Scope.parse("aws:s3:read")); // falsePolicy evaluation
import {
ConditionOperator,
PolicyEffect,
PolicyEngine,
addPolicyRule,
createPolicyDocument,
createPolicyRequest,
createPolicyRule,
} from "@openagentid/arsenal-sdk/policy";
let policy = createPolicyDocument("tenant-1", "stripe-readonly");
policy = addPolicyRule(policy, {
...createPolicyRule("allow-stripe-read", PolicyEffect.Allow),
conditions: [
{
type: "scope",
operator: ConditionOperator.StartsWith,
value: "stripe:charges:read",
},
],
});
const engine = new PolicyEngine();
engine.addPolicy(policy);
const decision = engine.evaluate(
createPolicyRequest("agent-1", "tenant-1", "stripe:charges:read"),
);
// decision.effect === "allow"Error handling
All SDK errors are ArsenalError instances (which extend Error) and
carry a numeric code that matches the Rust crate's
arsenal_core::error::ErrorCode. Use code ranges to categorize
failures:
import { ArsenalError, ErrorCode, isPermanentError } from "@openagentid/arsenal-sdk";
try {
await client.requestCapabilityForScopes(["stripe:charges:read"], 300);
} catch (err) {
if (err instanceof ArsenalError) {
if (isPermanentError(err.code)) {
// Token revoked, consent denied, etc. — do not retry.
} else if (err.code === ErrorCode.RateLimitExceeded) {
// Back off and retry.
}
}
throw err;
}Targets
- Node 20+, Bun, Deno
- Modern browsers (ESM)
- Cloudflare Workers (uses native
fetch; no Node built-ins in the core) - Dual ESM + CJS build via
tsup - TypeScript 5.7+ with
strictmode
License
Copyright © 2026 L1fe Labs, Inc.
Licensed under either of Apache License 2.0 or MIT license, at your option.
