d in a JSONL record that also contains a reference to the previous record's hash, forming an unbroken chain.
Step 1: Identity & Key Management
Generate a dedicated Ed25519 keypair scoped to the agent runtime. The private key signs payloads; the public key enables verification. Keep the private key isolated from version control and cloud storage.
import { execSync } from 'child_process';
import { mkdirSync, writeFileSync } from 'fs';
import { join } from 'path';
const IDENTITY_DIR = join(process.env.HOME!, '.agent-audit', 'keys');
const KEY_NAME = 'prod-agent-runner';
export function provisionSigningIdentity(): void {
mkdirSync(IDENTITY_DIR, { recursive: true });
const cmd = `signet identity generate --name ${KEY_NAME} --owner ops@company.internal`;
execSync(cmd, { stdio: 'inherit' });
console.log(`[audit] Identity provisioned at ${IDENTITY_DIR}`);
}
Step 2: Plugin Integration & Runtime Hook
Install the audit plugin into the agent gateway. The plugin intercepts tool calls, canonicalizes the payload, signs it, and appends the receipt to the local JSONL store before forwarding execution.
import { readFileSync, writeFileSync } from 'fs';
import { join } from 'path';
const GATEWAY_CONFIG = join(process.env.HOME!, '.agent-gateway', 'config.json');
export function attachAuditPlugin(): void {
const config = JSON.parse(readFileSync(GATEWAY_CONFIG, 'utf-8'));
config.plugins = config.plugins || { entries: {} };
config.plugins.entries['cryptographic-audit'] = {
config: {
keyName: KEY_NAME,
target: 'agent://gateway/local',
policy: join(process.env.HOME!, '.agent-audit', 'policies', 'safety.yaml'),
encryptParams: true
}
};
writeFileSync(GATEWAY_CONFIG, JSON.stringify(config, null, 2));
console.log('[audit] Plugin attached to gateway runtime');
}
Step 3: Policy Enforcement Layer
Define declarative rules that evaluate tool calls before signing. Policies can deny destructive operations, enforce rate limits, or require human approval for sensitive scopes. Denied calls are logged at warning level but intentionally excluded from the cryptographic chain to preserve chain continuity.
# safety.yaml
version: 1
name: agent-safety-policy
default_action: allow
rules:
- id: block-destructive-shell
match:
tool: shell_exec
params:
command:
contains: "rm -rf"
action: deny
reason: "Destructive filesystem operation requires manual override"
- id: throttle-network-requests
match:
tool:
one_of: [http_request, fetch_url]
action: rate_limit
rate_limit:
window_secs: 60
max_calls: 12
Step 4: Verification & Chain Validation
Post-execution, validate the audit trail using only the public key. The verifier walks the JSONL file, checks each Ed25519 signature against the canonicalized payload, and confirms that each record's prev_hash matches the SHA-256 digest of the preceding entry.
import { createHash } from 'crypto';
import { readFileSync } from 'fs';
interface AuditRecord {
id: string;
action: Record<string, unknown>;
prev_hash: string;
sig: string;
}
export function verifyAuditChain(logPath: string): boolean {
const lines = readFileSync(logPath, 'utf-8').trim().split('\n');
let lastHash = 'genesis';
for (const line of lines) {
const record: AuditRecord = JSON.parse(line);
if (record.prev_hash !== lastHash) {
throw new Error(`Chain broken at ${record.id}: expected ${lastHash}`);
}
const payloadHash = createHash('sha256')
.update(JSON.stringify(record.action))
.digest('hex');
// Ed25519 verification would occur here using the public key
const isValid = verifyEd25519Signature(record.sig, payloadHash);
if (!isValid) {
throw new Error(`Signature mismatch at ${record.id}`);
}
lastHash = createHash('sha256').update(line).digest('hex');
}
return true;
}
Architecture Rationale
- RFC 8785 JCS: JSON key ordering and whitespace variations break naive hashing. JCS guarantees deterministic serialization regardless of parser implementation.
- SHA-256: Provides collision resistance and fast computation. Used for payload hashing and chain linking.
- Ed25519: Offers 128-bit security with 64-byte signatures and sub-millisecond verification. Ideal for high-frequency tool calls.
- Hash Chaining: Each record references the SHA-256 digest of the previous line. Reordering or deletion invalidates all subsequent entries.
- XChaCha20-Poly1305 Encryption: Applied to
action.params when encryptParams: true. Preserves chain verifiability while protecting sensitive inputs at rest.
Pitfall Guide
| Pitfall | Explanation | Fix |
|---|
| Canonicalization Mismatch | Different JSON parsers serialize keys in varying orders. If the signing runtime and verification runtime use different serializers, signatures will fail. | Enforce RFC 8785 JCS at both signing and verification boundaries. Never use JSON.stringify() directly for payload hashing. |
| Key Rotation Without Chain Migration | Swapping signing keys mid-run breaks verification for historical records. Auditors cannot validate pre-rotation entries with the new public key. | Maintain a key versioning scheme. Store key_version in each receipt. Provide a key rotation manifest that maps old public keys to new ones. |
| Policy Bypass via Tool Substitution | Agents may achieve the same outcome using alternative tools (e.g., python_exec instead of shell_exec). Static deny rules miss indirect paths. | Implement semantic policy matching that evaluates intent or output impact, not just tool names. Combine with output validation hooks. |
| Assuming Denied Actions Are Signed | The audit chain only contains allowed executions. Denied calls are logged separately and intentionally excluded to prevent chain pollution. | Maintain a parallel denied_actions.jsonl for compliance reporting. Cross-reference denied logs with agent conversation history for full context. |
| Private Key Exposure in CI/CD | Embedding signing keys in pipeline secrets or container images risks mass compromise. A leaked private key allows forgery of historical receipts. | Use hardware security modules (HSMs) or OS-level keyrings. Restrict private key access to the agent runtime process only. Rotate keys on container rebuild. |
| Ignoring Rate Limits on High-Frequency Tools | Unbounded tool loops can generate thousands of receipts per second, exhausting disk I/O and verification bandwidth. | Apply rate_limit policies to network and filesystem tools. Buffer receipts in memory and flush to disk in batches of 100-500. |
| Failing to Backup Public Keys Separately | If the host machine fails and only the private key is backed up, verification becomes impossible without the corresponding public key. | Export public keys to a secure, version-controlled vault. Distribute them to audit teams and compliance systems ahead of incidents. |
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Internal debugging & rapid iteration | Standard JSONL logging + console output | Low overhead, fast to implement | Minimal compute/storage |
| Compliance-ready agent deployments | Hash-chained Ed25519 audit trail + policy enforcement | Satisfies EU AI Act Art. 12, enables offline verification | Moderate CPU for signing, negligible storage |
| Multi-tenant SaaS with regulatory scrutiny | Merkle tree aggregation + periodic blockchain anchoring | Provides third-party timestamping and cross-tenant isolation | High infrastructure cost, complex key management |
| Air-gapped or offline environments | Local hash chain + public key distribution via secure USB | No network dependency, verifiable without external services | Manual key distribution overhead |
Configuration Template
{
"plugins": {
"entries": {
"cryptographic-audit": {
"config": {
"keyName": "prod-agent-runner",
"target": "agent://gateway/local",
"policy": "~/.agent-audit/policies/safety.yaml",
"encryptParams": true,
"flushIntervalMs": 500,
"maxBatchSize": 250
}
}
}
}
}
# safety.yaml
version: 1
name: production-safety
default_action: allow
rules:
- id: deny-destructive-filesystem
match:
tool: shell_exec
params:
command:
regex: "^(rm|dd|mkfs|format)\\s"
action: deny
reason: "Destructive filesystem operations require manual approval"
- id: throttle-api-calls
match:
tool:
one_of: [http_request, fetch_url, api_call]
action: rate_limit
rate_limit:
window_secs: 60
max_calls: 15
- id: require-approval-for-db-write
match:
tool: database_query
params:
type:
one_of: [INSERT, UPDATE, DELETE, DROP]
action: require_approval
reason: "Database mutations require human sign-off"
Quick Start Guide
- Generate Identity: Run
signet identity generate --name prod-agent-runner --owner ops@company.internal to create the signing keypair.
- Attach Plugin: Add the audit plugin entry to your gateway configuration file, pointing to the generated key and policy path.
- Define Policies: Create a YAML policy file with deny rules for destructive tools and rate limits for high-frequency operations.
- Launch Gateway: Start the agent runtime. Every tool call will now produce a signed, hash-chained receipt before execution.
- Verify Chain: After a test run, execute
signet audit --verify to confirm cryptographic integrity and policy enforcement.