kavach-sdk
v0.1.5
Published
Post-quantum execution boundary enforcement for AI agents, APIs, and distributed systems. TypeScript SDK on a compiled Rust core.
Maintainers
Readme
kavach-sdk
Post-quantum execution boundary enforcement for AI agents, APIs, and distributed systems. TypeScript SDK.
Kavach separates possession of credentials from permission to act. Every action passes through a gate that evaluates identity, policy, drift, and invariants before producing a verdict. All evaluation runs in compiled Rust via napi-rs; this package is the idiomatic TypeScript wrapper.
Action attempted ──▶ Gate (identity · policy · drift · invariants) ──▶ Permit / Refuse / InvalidateInstall
npm install kavach-sdkNative addons are published for Linux x64/arm64 and macOS x64/arm64. Node 20+.
60-second quickstart
import { Gate, type EvaluateOptions } from 'kavach-sdk';
// Policy as a plain JS object. No separate config format to learn.
const POLICY = {
policies: [
{
name: 'agent_small_refunds',
effect: 'permit',
conditions: [
{ identity_kind: 'agent' },
{ action: 'issue_refund' },
{ param_max: { field: 'amount', max: 1000 } },
],
},
],
};
const gate = Gate.fromObject(POLICY, {
invariants: [{ name: 'hard_cap', field: 'amount', maxValue: 50_000 }],
});
const verdict = gate.evaluate({
principalId: 'agent-bot',
principalKind: 'agent',
actionName: 'issue_refund',
params: { amount: 500 },
});
if (verdict.isPermit) {
console.log('permit', verdict.tokenId);
} else {
console.log(`blocked: [${verdict.code}] ${verdict.evaluator}: ${verdict.reason}`);
}A policy set with no matching permit Refuses by default. There is no implicit allow.
Loading a policy
The recommended surface for Node is a plain JS object (admin UI submissions, database rows, feature flags):
const gate = Gate.fromObject(policyObject); // native object (recommended)
const gate = Gate.fromJsonString(jsonString); // JSON over the wire
const gate = Gate.fromJsonFile('kavach.json'); // JSON file on diskFor operator-owned config that lives in git and is hand-edited, use TOML:
const gate = Gate.fromToml(tomlString); // operator-edited TOML
const gate = Gate.fromFile('kavach.toml'); // TOML file on diskTypo'd field names ({ idnetity_kind: 'agent' }) throw a clear error in every loader instead of being silently dropped, so a misspelled condition cannot quietly weaken a policy. The full TOML workflow (rendered in Rust, Python, and Node) lives at docs/guides/toml-policies.md.
Feature surface
Signed permit tokens (PqTokenSigner)
When a PqTokenSigner is attached to a gate, every Permit verdict carries an ML-DSA-65 (or ML-DSA-65 + Ed25519 hybrid) signed envelope. Downstream services verify independently.
import { Gate, PqTokenSigner } from 'kavach-sdk';
const signer = PqTokenSigner.generateHybrid();
const gate = Gate.fromObject(POLICY, { tokenSigner: signer });
const verdict = gate.evaluate({ /* ... */ });
if (verdict.isPermit) {
const ok = signer.verify(
{
tokenId: verdict.permitToken!.tokenId,
evaluationId: verdict.permitToken!.evaluationId,
issuedAt: verdict.permitToken!.issuedAt,
expiresAt: verdict.permitToken!.expiresAt,
actionName: verdict.permitToken!.actionName,
},
verdict.permitToken!.signature!,
);
console.assert(ok);
}Hybrid signers sign with both ML-DSA-65 and Ed25519; a hybrid verifier rejects PQ-only envelopes as a signature-downgrade guard.
Key pairs
import { KavachKeyPair } from 'kavach-sdk';
const kp = KavachKeyPair.generate(); // no expiry
const kp2 = KavachKeyPair.generateWithExpiry(3600); // 1-hour lifetime
console.assert(!kp.isExpired);
const bundle = kp.publicKeys(); // PublicKeyBundleView, safe to sharePersisting signer identity across restarts
Persist the identity once and reload the same one on every boot, so the node keeps one stable keyId and one continuous audit chain.
import { existsSync } from 'fs';
import { KavachKeyPair, PqTokenSigner } from 'kavach-sdk';
const keyPath = '/var/lib/kavach/node-signer.key';
const kp = existsSync(keyPath)
? KavachKeyPair.loadFromFile(keyPath)
: (() => { const k = KavachKeyPair.generate(); k.saveToFile(keyPath); return k; })();
const signer = PqTokenSigner.fromKeypairHybrid(kp);saveToFile writes secret key material 0600 (owner-only) on Unix and never widens an existing file's permissions. For a secrets manager, use kp.toSecretBytes() (seal it before storage) and KavachKeyPair.fromSecretBytes(blob). A permit or audit entry signed before a restart verifies unchanged afterward. The public bundle moves between processes with publicBundleToJson(bundle) / publicBundleFromJson(...) or publicBundleToBytes(bundle) / publicBundleFromBytes(...), so a central service can pin each node's bundle and independently re-verify every pushed audit chain.
Signed audit chain
Append-only, tamper-evident audit log. verify rejects tampered entries, wrong keys, and mode mismatches (e.g., a PQ-only verifier on a hybrid chain, which is a silent downgrade).
import { AuditEntry, SignedAuditChain } from 'kavach-sdk';
const chain = new SignedAuditChain(kp, true); // hybrid
chain.append(new AuditEntry({
principalId: 'agent-bot',
actionName: 'issue_refund',
verdict: 'permit',
verdictDetail: 'within policy',
}));
chain.verify(kp.publicKeys());
// Portable JSONL for off-node storage:
const blob = chain.exportJsonl();
SignedAuditChain.verifyJsonl(blob, kp.publicKeys());Bounded memory for long-running services. A plain SignedAuditChain keeps every entry in RAM, so a service that appends forever grows without bound. ManagedAuditChain streams old entries to a JSONL file and prunes them from memory under a retention policy you configure by entry count, by bytes, on a timer, or any combination. The full chain still verifies across the on-disk file plus the in-memory tail.
import { ManagedAuditChain, SignedAuditChain } from 'kavach-sdk';
import { readFileSync } from 'fs';
const chain = new ManagedAuditChain(kp, 'audit.jsonl', {
maxEntries: 10_000, // evict when the window exceeds 10k entries
maxBytes: 64 * 1024 * 1024, // ...or 64 MB, whichever first
flushIntervalMs: 1000, // ...or once per second, even below the sizes
onSinkFailure: 'reject', // back-pressure if the disk is unavailable
});
for (const entry of stream()) chain.append(entry); // append forever; memory plateaus
const disk = readFileSync('audit.jsonl');
SignedAuditChain.verifyJsonl(Buffer.concat([disk, chain.exportTailJsonl()]), kp.publicKeys());Secure channel
Hybrid-encrypted, PQ-signed byte channel between two peers. Sealed payloads are opaque; ship them over any transport.
import { SecureChannel, KavachKeyPair } from 'kavach-sdk';
const alice = KavachKeyPair.generate();
const bob = KavachKeyPair.generate();
const aliceCh = new SecureChannel(alice, bob.publicKeys());
const bobCh = new SecureChannel(bob, alice.publicKeys());
const sealed = aliceCh.sendSigned(Buffer.from('hello bob'), 'greeting');
const plaintext = bobCh.receiveSigned(sealed, 'greeting');
console.assert(plaintext.toString() === 'hello bob');Replay, cross-context, and wrong-recipient attacks all fail closed.
Public key directory
import { PublicKeyDirectory, DirectoryTokenVerifier, KavachKeyPair } from 'kavach-sdk';
import { writeFileSync } from 'fs';
const signingKey = KavachKeyPair.generate();
const manifest = signingKey.buildSignedManifest([bundleA, bundleB]);
writeFileSync('directory.json', manifest);
const directory = PublicKeyDirectory.fromSignedFile(
'directory.json',
signingKey.publicKeys().mlDsaVerifyingKey,
);
const verifier = new DirectoryTokenVerifier(directory, true /* hybrid */);
verifier.verify(token, signedEnvelope); // throws on tamper/miss/downgradeIn-memory (PublicKeyDirectory.inMemory([...])) and unsigned-file variants are also available.
Geo drift (tolerant mode)
Same-country IP hops become Warnings instead of Violations when you provide lat/lon and a threshold:
const gate = Gate.fromObject(POLICY, { geoDriftMaxKm: 500 });
const verdict = gate.evaluate({
principalId: 'u',
principalKind: 'user',
actionName: 'view_profile',
ip: '2.3.4.5',
sessionId: 'sess-1',
currentGeo: { countryCode: 'IN', city: 'Chennai', latitude: 13.08, longitude: 80.27 },
originGeo: { countryCode: 'IN', city: 'Bangalore', latitude: 12.97, longitude: 77.59 },
});Missing geo with a threshold set still fails closed. The SDK does not silently bypass.
Policy hot reload
gate.reload(...) accepts a TOML string; it throws on parse error and leaves the previous good set in place. See docs/guides/toml-policies.md for the full reload workflow (including the file-watcher pattern and the empty-TOML kill switch).
gate.reload(newPolicyToml); // parse error throws, previous set preservedIn-process invalidation
Fan out Invalidate verdicts to anything on this node that needs to react (metrics, kill-session hooks, downstream caches):
import {
Gate, InMemoryInvalidationBroadcaster, spawnInvalidationListener,
} from 'kavach-sdk';
const broadcaster = new InMemoryInvalidationBroadcaster();
const gate = Gate.fromObject(POLICY, { broadcaster });
const handle = spawnInvalidationListener(broadcaster, (scope) => {
console.log(`invalidated: ${scope.target} (${scope.reason})`);
});
// handle.abort() on shutdownThe Node SDK ships the in-process broadcaster only. Multi-replica Redis fan-out lives on the Rust side (see docs/guides/distributed.md).
Observe mode
Roll out incrementally: log verdicts without blocking.
const gate = Gate.fromObject(POLICY, { observeOnly: true });What's in the Rust engine
Every evaluate() call crosses FFI into compiled Rust via napi-rs. The TypeScript layer is pure wrappers. The engine implements:
- Policy: a small, fixed condition vocabulary (
identity_kind,action,param_max,rate_limit,time_windowwith optional timezone, etc.) expressed as a JS object, JSON, or operator-edited TOML. - Drift detectors: IP / geo, session age, device, behavior.
- Invariants: hard per-action limits that cannot be overridden by policy.
- Post-quantum crypto: ML-DSA-65, ML-KEM-768, Ed25519, X25519, ChaCha20-Poly1305.
- Fail-closed: any evaluator error, store failure, or broadcast issue errs on the side of Refuse.
License
Elastic License 2.0. Source-available; free to use, embed, and modify for any purpose, including commercially. You may not offer Kavach itself as a hosted or managed service that competes with SarthiAI. See the LICENSE file for the full text.
