@hermesplant/assurance-guard
v0.1.1
Published
Dependency-light local policy guard and typed client for Hermes Agent Commerce Assurance.
Maintainers
Readme
Hermes Plant Assurance Guard SDK
Dependency-light TypeScript primitives for putting a fail-closed policy gate in front of x402 payments and MCP tool calls. The package can evaluate organization policy locally, hash-bind the exact operation, request signed decisions from the Hermes Plant Agent Commerce Assurance API, verify returned record hashes, and record fulfillment receipts.
The npm registry contains version 0.1.0 (published July 29, 2026). This checkout prepares 0.1.1 with execution-recovery fixes; a source version is not proof of publication. Use the manual protected release workflow and verify the exact registry artifact as described in ../../docs/ASSURANCE_NPM_RELEASE.md.
Guarantees and boundaries
- Stable
hermes-stable-json-v1canonicalization and SHA-256 record hashing. - Exact x402 resource, method, scheme, network, asset, recipient, amount, facilitator, payment identifier, and payload-resource binding.
- Exact MCP server, URL, tool, and canonical argument-hash binding.
- Network-free, deterministic organization-policy preflight.
- Explicit
denyandneeds_reviewexceptions; neither is treated as success. - Typed calls to
/api/assurance/decisions,/api/assurance/receipts, and/api/assurance/historywith runtime envelope and record-hash checks. - No wallet, signer, payment executor, MCP transport, arbitrary network probe, analytics, or secret persistence.
Local preflight is a safety gate, not a replacement for a signed server decision when an organization needs retained policy history and receipts.
Runtime requirements
- Node.js 18+ or a modern browser/worker with
fetchandglobalThis.crypto.subtle. Missing Web Crypto fails deterministically withAssuranceCryptoUnavailableError. - TypeScript is optional for consumers; generated ESM and declarations are in
dist/afternpm run build.
Build and link locally
cd packages/assurance-guard
npm run verify:releaseThen install it from another project using a reviewed local path:
{
"dependencies": {
"@hermesplant/assurance-guard": "file:../HermesPlant/packages/assurance-guard"
}
}verify:release typechecks production code, examples, and tests; runs the
behavior suite; performs a clean declaration build; inspects the packed file
list; installs the tarball into an isolated consumer; typechecks that consumer
with strict NodeNext settings; and executes the installed ESM exports.
Publishing is a separate manual, protected npm OIDC workflow, not a side effect of a source push or application deployment.
Network-free quickstart
From the repository root:
npm run demo:quickstart --prefix packages/assurance-guardIt builds the package, runs local x402 and MCP policy decisions without credentials or network calls, and prints assurance-v2 verdicts and binding hashes. The clean-consumer release gate enforces a 15-minute maximum; the demo normally completes in seconds.
Installable reference integrations are shipped in examples/generic-x402.ts, examples/generic-mcp.ts, and examples/offline-jwks.ts.
Local x402 preflight
import {
AssuranceGuard,
AssuranceDeniedError,
AssuranceNeedsReviewError,
type X402AssuranceDecisionRequest,
} from "@hermesplant/assurance-guard";
const request: X402AssuranceDecisionRequest = {
protocol: "x402",
idempotencyKey: "run-42:exa-search:1",
subject: { runId: "run-42", agentId: "research-agent" },
policy: {
policyId: "research-spend",
version: 3,
maxUsdPerTransaction: 0.25,
allowedNetworks: ["eip155:8453"],
allowedHosts: ["api.example.com"],
requirePaymentIdentifier: true,
},
intent: {
resourceUrl: "https://api.example.com/search",
method: "POST",
scheme: "exact",
network: "eip155:8453",
asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
amountUnits: "7000",
payTo: "0x6d6E695b09861467c7d462f5AAF31cF3540B9192",
facilitatorUrl: "https://api.cdp.coinbase.com/platform/v2/x402",
paymentIdentifier: "run-42:exa-search:1",
paymentPayloadResource: "https://api.example.com/search",
paymentPayloadMethod: "POST",
},
};
const guard = new AssuranceGuard();
try {
const approved = await guard.authorize(request, { localOnly: true });
console.log(approved.bindingHash);
} catch (error) {
if (error instanceof AssuranceNeedsReviewError) {
// Route to an actual reviewer; do not execute.
} else if (error instanceof AssuranceDeniedError) {
// Reject and retain the findings.
} else {
throw error;
}
}Undefined safety booleans use fail-closed service defaults: HTTPS, exact x402 scheme, resource binding, and MCP argument binding are required unless the organization policy explicitly disables the corresponding check.
Connected authorization
import { AssuranceClient, AssuranceGuard } from "@hermesplant/assurance-guard";
const client = new AssuranceClient({
baseUrl: "https://hermesplant.com",
accessToken: () => process.env.HERMES_AGENT_TOKEN,
organizationId: "org_example",
});
const guard = new AssuranceGuard({ client });
const decision = await guard.authorize(request);
// Re-verify freshness, record hash, and every bound field immediately before
// handing the operation to your own x402 transport.
await guard.assertX402Executable(decision, request.intent);
// Your payment transport runs here. The SDK never signs or pays.
await guard.recordReceipt(decision, {
outcome: "fulfilled",
settlementId: "0x...",
responseStatus: 200,
evidenceHashes: ["b".repeat(64)],
});For browser customer sessions, omit accessToken and use a same-origin
baseUrl. For agent access, provide an existing Hermes Bearer token. The SDK
does not read .env files or store credentials.
MCP exact-argument binding
const mcpRequest = {
protocol: "mcp" as const,
idempotencyKey: "run-77:create-issue:1",
subject: { runId: "run-77", agentId: "ops-agent" },
policy: {
policyId: "mcp-production",
version: 1,
allowedMcpServers: ["github"],
allowedMcpTools: ["create_issue"],
requireMcpArgumentBinding: true,
reviewMcpRiskAtOrAbove: "high" as const,
},
intent: {
serverId: "github",
serverUrl: "https://mcp.example.com",
toolName: "create_issue",
toolArguments: { title: "Exact title", labels: ["ops", "agent"] },
declaredRisk: "low" as const,
},
};
const decision = await guard.authorize(mcpRequest);
await guard.assertMcpExecutable(decision, mcpRequest.intent);Raw MCP arguments are sent in the decision request but are not duplicated in
the signed decision binding. The binding retains only their canonical SHA-256
hash. If a caller supplies both toolArguments and toolArgumentsHash, a
mismatch is denied.
Direct API client
const decision = await client.createDecision(request);
const history = await client.listHistory(25);The client verifies:
- The response has the required assurance-v2 envelope; legacy v1 records remain readable for compatibility.
- The SHA-256 hash of the returned record matches
integrity.recordHash. - Decision protocol, idempotency key, binding, binding hash, policy hash, and request hash match the submitted request.
- Receipt decision, record hash, binding hash, protocol, settlement ID, outcome, response status, response-body hash, evidence hashes, and caller-supplied observation time match the submitted receipt.
HTTP failures throw AssuranceApiError with status, optional service code,
validation errors, contactUrl, and the parsed response body. Invalid or
mismatched success responses throw AssuranceProtocolError.
Integrity model
Connected decisions and receipts fail closed unless both the canonical SHA-256 record hash and an Ed25519 detached JWS verify against a configured or cached Hermes JWKS. AssuranceClient accepts retained keys or fetches /.well-known/assurance-jwks.json, caches them for five minutes, refreshes once for an unknown key, and rejects redirects, non-JSON responses, unusable keys, unsigned records, and HMAC-only records. Hash equality alone is never treated as server authenticity for execution.
For offline verification, retain a trusted JWKS while online and pass it to verifySignedRecord or AssuranceGuard. The SDK never needs or accepts a server private signing secret. The package includes a PUBLIC TEST-ONLY deterministic Ed25519 vector for cross-consumer interoperability.
Development
npm run typecheck
npm test
npm run build
npm run verify:packageThe package has no runtime dependencies. Tests use injected fetch functions
and make no live, paid, or wallet-backed calls.
Assurance v2 public verification and execution wrappers
The organization decision API is server-authoritative. Owners/admins activate an
immutable policy version at /api/assurance/policies; decision callers cannot
install a weaker policy. The SDK's local policy remains an early fail-closed
preflight, while the signed server verdict is evaluated against the active
organization version.
Production records carry an Ed25519 detached JWS and are verifiable offline:
import { verifySignedRecord, type AssuranceJwks } from "@hermesplant/assurance-guard";
const jwks = await fetch(
"https://hermesplant.com/.well-known/assurance-jwks.json",
).then((response) => response.json()) as AssuranceJwks;
const verification = await verifySignedRecord(decision, { jwks });
if (!verification.ok) throw new Error("Untrusted assurance decision");Use the caller-controlled wrappers to check binding immediately before execution:
const completed = await guard.executeX402(
request,
(exactIntent) => myX402Client.execute(exactIntent),
{ settlementId: (response) => response.headers.get("x-settlement-id") ?? undefined },
);
const toolResult = await guard.executeMcp(
mcpRequest,
(exactArguments, exactIntent) =>
myMcpClient.callTool(exactIntent.serverId, exactIntent.toolName, exactArguments),
);Both wrappers deep-clone and freeze the exact operation, verify its binding
immediately before the callback, refuse deny/needs_review without invoking the
callback, and bind the returned content into an assurance receipt. x402
exact is the production scheme; batch is intentionally unsupported.
Recovery contract (0.1.1)
The successful result shape is unchanged. MCP results with isError: true
produce a failed, content-bound receipt rather than a fulfilled receipt.
AssuranceExecutionError preserves the decision, original cause, processing
stage, and returned response when available. executionState: "returned"
means the callback returned; it does not prove settlement. Hashing, settlement
extraction, or receipt-storage failure must not be interpreted as action failure.
executionState: "unknown" means the callback rejected and the remote outcome
must be reconciled. Neither case writes a fabricated failed receipt.
When receiptInput is present, retry only
guard.recordReceipt(error.decision, error.receiptInput, { organizationId })
using the original organization context and a fresh cancellation signal. Keep
that exact payload; an earlier write may have committed despite a lost response.
Otherwise reconcile the executor outcome and recover its evidence first.
Both wrappers reject replayed decisions and same-guard decision reuse with
AssuranceReplayError, including after executor rejection. Calling authorize
and then execute* with the same key therefore fails closed; use either the
wrapper once or the lower-level explicit authorization/binding/receipt flow.
Do not rotate an idempotency key to bypass this protection. An authorization
whose response was lost may be blocked despite never having executed.
These checks do not provide universal exactly-once execution. The trusted caller-owned transport must enforce durable operation idempotency, preserve the exact bound intent, and reconcile across crashes and process restarts. Lower-level APIs do not reserve execution. No automatic action retry is safe merely because an HTTP request, callback, or receipt request failed.
MCP policy checks server/tool allowlists, binding, and caller-declared risk; they do not semantically inspect arbitrary arguments or infer malicious intent. Risk assertions must come from trusted host policy, not model self-assessment. Broadly allowlisting a shell, SQL, or general-purpose tool is not an argument safety policy. Narrow capabilities and independent argument validation belong in the trusted execution host before the wrapper.
