@asqav/sdk
v0.10.14
Published
AI agent governance - audit trails, policy enforcement, ML-DSA cryptographic signatures
Maintainers
Readme
asqav
TypeScript SDK for asqav.com, the evidence layer for AI agents.
Every agent action gets a signed, hash-chained compliance receipt: ML-DSA-65 (FIPS 204, post-quantum), timestamped against independent witnesses, and verifiable by anyone - auditor, counterparty, regulator - without an Asqav account and without trusting us.
Zero native dependencies. Cryptography runs server-side.
Install
npm install @asqav/sdkQuick start
npm install -g @asqav/sdk
asqav login # validates your key, saves it to ~/.asqav/credentialsimport { govern } from "@asqav/sdk";
// govern() = init() + Agent.create() in one call
const agent = await govern({ apiKey: process.env.ASQAV_API_KEY, agentName: "my-agent" });
const sig = await agent.sign({ actionType: "api:openai:chat", context: { model: "gpt-4o" } });
console.log(sig.actionRef); // "sha256:..." over the JCS-canonical action
console.log(sig.previousReceiptHash); // 64 hex; "0".repeat(64) on this agent's first receipt
console.log(sig.verificationUrl); // anyone can open thisOne install, one govern, one sign. init({ apiKey }) + Agent.create({ name }) remain
available when you want control over algorithm, capabilities and other agent options.
Each Agent keeps the API key and base URL selected when its creation or retrieval starts.
Its signing mode and a copy of the supplied salt stay with it too. A later init or govern
configures future Agents; it does not retarget an existing Agent, even during an awaited operation.
To rotate a key, call init with the new configuration and obtain a new Agent with Agent.get(id).
Hooks and detector registrations continue to apply to existing Agents when they sign.
Module helpers such as request and verifySignature use the current default configuration.
Integrations that create an Agent lazily through the static API also use the default at that time;
storing configuration in another object does not give that object an isolated SDK connection.
Use AsqavClient when each caller needs its own connection before creating an Agent:
import { AsqavClient } from "@asqav/sdk";
const first = new AsqavClient({ apiKey: process.env.FIRST_ASQAV_API_KEY });
const second = new AsqavClient({ apiKey: process.env.SECOND_ASQAV_API_KEY });
// Neither constructor sends an API request.
const firstAgent = await first.createAgent({ name: "first-agent" });
const secondAgent = await second.getAgent("agt_existing");
await firstAgent.sign({ actionType: "api:call", context: { task: "first" } });Clients keep their construction-time configuration and never change the module default.
An omitted baseUrl uses ASQAV_API_URL at construction, or the SDK cloud URL when unset;
it does not inherit another caller's init. Mode resolution follows the same rules as init.
The client owns a copy of the supplied salt. Create a new client and Agent to rotate credentials.
Hooks and detectors still apply across clients. The returned objects are ordinary Agents.
Verify it without an account
This is the point of the whole thing - the receipt stands on its own:
Run this right now, with no key and no signup:
import { verify } from "@asqav/sdk";
const result = await verify("sig_example_regulator_cold_verify_2026");
console.log(result.verified); // false -- and that is the point, see below
console.log(result.chainHash); // recomputed on your machine from the canonical bytesThat id is a shape example: its signature bytes are placeholders and its kid resolves
to no key, so the verifier returns verified: false instead of waving it through. Swap in a
signature_id of your own for a receipt that passes. A verifier that says no when the
evidence is absent is the only kind worth having.
From the shell (npm install -g @asqav/sdk provides the asqav command):
asqav verify <your_signature_id>Offline or air-gapped, snapshot the keys once and verify with no network at all. This re-derives the signature itself from the RFC 8785 canonical bytes and reports a verdict per axis, so it never asks Asqav to vouch for anything:
import { fetchJwks, verifyReceiptOffline } from "@asqav/sdk";
import { readFileSync, writeFileSync } from "node:fs";
const jwks = await fetchJwks(); // online, once
writeFileSync("jwks.json", JSON.stringify(jwks));
// Offline from here. Pass the raw text, not JSON.parse output (see below).
const result = verifyReceiptOffline(readFileSync("receipt.json", "utf8"), readFileSync("jwks.json", "utf8"));
if (result.verdict === "unverified") throw new Error(JSON.stringify(result.axes));verdict is "verified" | "verified_keyed" | "unverified". verified_keyed is a pass over a
keyed digest: internally consistent, but not third-party re-derivable. Pass the predecessor
receipt as the third argument to check the hash-chain link too.
Raw text gets the same verdict as the Python verifiers and the conformance runner, because
the parser keeps each number as it was written. The signed bytes can hold 500.0 or -0.0,
and the receipt profile accepts only integers in the safe range, so those receipts come back
unverified / unverifiable everywhere. An object from JSON.parse cannot carry that
spelling. On that path a -0 in a signed part is refused as unverifiable, because it could
have been -0 or -0.0. A 500.0 reads back as 500: if the signer wrote the float 500.0,
the signature check fails with unverified / invalid, but if the signer wrote the integer
500 and the file respells it as 500.0 or 5e2 (or 0 as 0.0), the object verifies while
raw text and the Python verifiers refuse it. Pass raw text when you need the strict profile.
An integer past 2^53 anywhere in the receipt is refused as unverifiable on raw text and by
every Python verifier, a parsed dict included. JSON.parse rounds it, so one outside the
signed bytes goes unseen and that object can still verify.
Fractions such as 1e-07 and 0.5 verify the same way on both paths.
verifyCounterpartyBinding and computeEnvelopeHash also take the origin as raw text, and
then agree with Python's verify_counterparty_binding. A JSON.parse origin loses the
spelling here too. If it holds -0, verifyCounterpartyBinding reports unresolved and
computeEnvelopeHash and computeCounterpartyBinding throw counterparty_origin_unavailable.
If the origin signed the integer 500 and the file respells it as 500.0, the binding reports
matches while Python reports mismatch. If the origin signed a float such as 500.0 or -1.0,
a JSON.parse origin reads it back as an integer and the binding reports mismatch while Python
reports matches. An integer past 2^53 anywhere in the origin makes the binding unresolved
on raw text and in Python, while a JSON.parse origin that holds one outside the signed bytes
reports matches. Pass raw text for Python's answer.
ML-DSA-65 verifies in-process via @noble/post-quantum; Ed25519 and ES256 via node:crypto.
Guide: offline and air-gapped verification.
Neutral verifier
A standalone verifier for agent receipts across formats ships as a subpath export:
import { verify, ADAPTERS } from "@asqav/sdk/verifier";
const result = verify(receipt, ADAPTERS, keyProvider);It checks the issuer signature over the canonical bytes, the chain link and structural
presence for the asqav-native, AERF, ACTA, agent-receipts and Authproof formats. No account
required. It is the port of the Python asqav.verifier.oracle, held to verdict parity by a
shared conformance corpus.
Data handling modes
The SDK picks the safer default for where you point it:
- Cloud (
*.asqav.com): hash-only. A SHA-256 fingerprint is computed locally and only the hash plusactionType,agentId,sessionId,modelName,toolNameis sent. Prompts and tool arguments never leave your process. - Self-hosted: full payload, so the server can run policy checks and richer audit.
await init({ apiKey: "...", baseUrl: "https://api.asqav.com", mode: "hash-only" });There is no local offline queue in TypeScript; use the Python SDK's local_sign for that.
High-value actions
For regulated or high-risk actions, pass envelope fields that an auditor will look for:
const sig = await agent.sign({
actionType: "payment.wire_transfer",
context: { amountEur: 850000, beneficiaryIban: "DE89370400440532013000" },
receiptType: "protectmcp:decision",
riskClass: "high", // low | medium | high | unknown
issuerId: "legal:Acme GmbH", // LEI, EIN, CIK or W3C DID
iterationId: "task-2026-Q2-4821", // logical task, distinct from session
});nonce is an opaque per-call token (12 random bytes when omitted). The cloud refuses a
value a live signature for the same agent in the same organisation already holds, answering
HTTP 409, so the field bounds replay while that earlier receipt is inside its validity
window. A receipt whose validUntil has passed frees the token; one signed with no expiry
keeps it.
CLI
asqav whoami # active key source, validated
asqav init # print a ready-to-paste snippet
asqav verify <signature_id> # verify a receipt (no key needed)
asqav sign --agent-id ID --action-type T --action-json action.json
asqav agents list | create | revoke
asqav replay-verify <agent_id> <session_id> [--strict]
asqav audit-pack export --start ISO --end ISO --output-file bundle.json
asqav payloads erase <signature_id> # right-to-erasureFull command reference: asqav.com/docs/cli.
Framework integrations
Native adapters under @asqav/sdk/extras/*:
- Vercel AI SDK:
createAsqavExporter, passed toexperimental_telemetry. Every span becomes a signed action. - LangChain.js:
AsqavCallbackHandler, passed tocallbacks: [handler]. - Mastra:
AsqavMastraHook, attached to a Mastra agent. - OpenAI Agents JS:
AsqavOpenAIAgentsAdapter, wrapping a tool.
Each needs its framework as a peer dependency. CrewAI, LiteLLM, LlamaIndex and Haystack follow the same Hooks API; the Python SDK has the richer ecosystem. Parity status: integrations.
What a receipt does not prove
Stated plainly, because an auditor will ask:
- Not that the action ran, or ran once. A receipt records a decision and the bytes that passed through, never the effect. A retry produces a second receipt.
- Not that the policy was correct.
policy_digestproves which policy artefact existed, not that it was the right one. - Not that the environment was intact. No field here carries a remote attestation result.
- Tamper-evident, not tamper-proof. Modification is detectable, not prevented.
The full list is in the IETF profile under "What a Compliance Receipt Does Not Prove".
Reference
| Topic | Docs |
|---|---|
| Threat-framework mappings (MITRE, OWASP, NIST AI RMF, ISO 42001, EU AI Act) | threat-framework-mapping |
| NSA CSI U/OO/6030316-26 receipt fields | nsa-mcp-csi-alignment |
| Build provenance (executable_hash, sbom_digest, SLSA) | executable-hash-and-sbom-provenance |
| Witness policy and multi-witness anchoring | multi-witness-anchoring |
| Binding tool output (result_digest) | result-digest |
| Configuration-change receipts | configuration-change-receipts |
| Code-authorship receipts | code-authorship-receipts |
| Structured receipts (optional schema) | structured-receipts |
| Bring-your-own DLP / policy detectors | scanning |
| Audit Pack export | compliance |
| Independent verification protocol | independent-verification |
Requirements
Node 20.19 through 20.x, or Node 22.12 and later. Uses the built-in fetch. Zero native dependencies.
Standards
Profiled in the IETF Internet-Draft
draft-marques-asqav-compliance-receipts,
an Independent Submission profiling
draft-farley-acta-signed-receipts.
Aligns with NIST FIPS 204 (ML-DSA), RFC 8785 (JCS) and NSA CSI U/OO/6030316-26.
Links
Docs · SDK guide · Repository · Discovery descriptor
License
Elastic License 2.0. Get an API key at asqav.com.
