Aqua ProtocolAqua Protocol
Aqua Protocol
Aqua ProtocolAqua Protocol, an open protocol for verifiable data provenance and accountability. Developed in the open as a proposed standard, with public specification, documentation, and reference implementations.

Documentation

Welcome to Aqua ProtocolQuick StartCore ConceptsRelease Status & Versioning
Developer Onboarding
aqua-rs-sdk-coreTemplate RegistryAudit Trails for AI Agents
Protocol Reference
Protocol ReferenceData ModelHashing and CanonicalizationTemplatesSignaturesAnchors and LinksSelective DisclosureVerification
Use Cases
Aqua Protocol Use CasesDocument VerificationIdentity AttestationSupply Chain Tracking

Documentation

Welcome to Aqua ProtocolQuick StartCore ConceptsRelease Status & Versioning
Developer Onboarding
aqua-rs-sdk-coreTemplate RegistryAudit Trails for AI Agents
Protocol Reference
Protocol ReferenceData ModelHashing and CanonicalizationTemplatesSignaturesAnchors and LinksSelective DisclosureVerification
Use Cases
Aqua Protocol Use CasesDocument VerificationIdentity AttestationSupply Chain Tracking
Docs
Dev Tools
Audit Trails for AI Agents

Audit Trails for AI Agents

Record every step an AI agent takes as its own signed, verifiable, selectively disclosable Aqua tree using the audit template family

12 min read

An AI agent that reads prompts, reasons, calls tools, and acts on external systems produces claims that someone will later need to check: what did the user actually ask, what did the API actually return, who approved the risky action, what did the agent actually answer? Application logs cannot carry that burden — they are mutable, unsigned, and owned by the very operator whose conduct is in question.

The audit template family — the headline use case of aqua-rs-sdk-core — turns each step of an agent session into its own signed Aqua tree, causally linked to the others by role-tagged links. The result is a tamper-evident record that any third party verifies without trusting the agent's operator, and that can be selectively disclosed: an auditor can confirm that a prompt was signed and belongs to a turn without ever reading the prompt text.

The audit template family

Eleven data-only templates, rooted at the abstract audit_artifact:

TemplateRole
audit_artifactAbstract root of the family; every concrete template derives from it. Never instantiated directly.
audit_user_turn_marker (T1)Opens a user turn; its object revision hash becomes the turn_id.
audit_user_prompt (T2)The user's prompt, signed by the user's own session key.
audit_agent_thinking (T3)The agent's reasoning trace.
audit_agent_tool_call (T4)A tool invocation (tool name, arguments, risk level).
audit_api_response (T5)A third-party API response, attested by an independent signer.
audit_tool_result (T6)The tool result as the agent saw it.
audit_hitl_approval (T7)A human-in-the-loop decision, signed by the human's key.
audit_agent_response (T8)The agent's response; is_final: true closes the turn.
audit_round_anchorMerkle commitment closing a turn. Despite the name, a typed object revision, not an anchor revision.
audit_session_closeRecords the end of a session.

The normative template identities (bare SHA3-256 digests, per the protocol specification §8) are listed in the Templates catalog.

Three properties to keep in mind:

  • Data-only. Every template is a JSON Schema constraint set with no WASM verification section. Recording an artifact validates a payload against the schema and signs it — nothing executes, and the compute stage of verification is never involved.
  • Identified by hash, not name. revision_type on an audit object is the template's multihash; names are display data.
  • A deliberate fork. This family is re-rooted at audit_artifact rather than the identity-rooted family of richer Aqua profiles, so all eleven identities differ from their full-SDK counterparts even though the constraint content matches.

The session model

Identities

Separate signing keys per role keep the record honest: the agent cannot forge the user's approval, and the attestor's signature pins what an external API actually returned. The canonical example uses four Ed25519 did:key identities (the SDK also supports EIP-191, P-256, and WebAuthn suites):

IdentitySignsPurpose
Server (trust anchor)T1, round anchors, session closeOpens turns, seals rounds and the session
User sessionT2, T7What the user asked and what the human approved
AgentT3, T4, T6, T8Reasoning, tool calls, tool results, answers
API attestorT5Independent observation of third-party API responses

One tree per artifact

Each artifact is its own small Aqua tree with the shape:

Code
text
1Genesis Anchor -> Object (typed payload) -> Anchor (compositional links) -> Signature

The link anchor is present only when the artifact has links; T1, which opens the turn, carries none.

Causality between artifacts is expressed by compositional links — {hash, role} pairs carried in an anchor revision. The SDK stores them but never resolves them during verification, so linking across trees needs no extra verification inputs. The audit example establishes this role vocabulary:

RoleEdge meaning
aqua:in_user_turnMembership: this artifact belongs to the turn opened by T1
aqua:prev_artifact_in_turnOrdering: the previous artifact within the turn
prov:usedThis artifact consumed another (T6 used the attested T5 observation)
prov:wasGeneratedByThis artifact was produced by another activity (the T5 response by the T4 call)

One turn, end to end

Code
text
1Every artifact (T2..T8, round anchor) links T1 with aqua:in_user_turn.
2The ^ arrows are aqua:prev_artifact_in_turn: each artifact points to
3its predecessor in the turn. Each line is its own signed Aqua tree.
4 
5 T1 turn marker (server key) object revision hash = turn_id
6 T2 user prompt (user key)
7 ^
8 T3 agent thinking (agent key)
9 ^
10 T4 tool call (agent key)
11 ^
12 T5 api response (attestor key) prov:wasGeneratedBy -> T4
13 ^
14 T6 tool result (agent key) prov:used -> T5
15 ^
16 T7 hitl approval (user key)
17 ^
18 T8 agent response (agent key) prov:used -> T6, is_final: true
19 
20 round anchor (server key) audit_round_anchor object:
21 merkle_root over the object
22 hashes of T2..T8
23 session close (server key) audit_session_close object:
24 last turn_id + round anchor hash

The T1 object's revision hash is the turn_id that every later artifact carries in its payload, pinning the whole turn to one content-addressed identifier. When the turn ends, the server emits an audit_round_anchor object whose merkle_root commits to the turn's artifact hashes; the verifier schema-validates its shape but does not recompute the root — auditors recompute it from the listed leaf_hashes when checking completeness. audit_session_close finally records the end of the session, referencing the last turn and round anchor.

Walkthrough

The complete runnable version of everything below ships with the SDK:

Code
bash
1cargo run --example agent_audit_trail --features native

It records one full turn of an order-processing assistant as ten trees (T1–T8, round anchor, session close), verifies all of them, and produces a pseudonymous redaction of the prompt. The snippets here are excerpts adapted from it; elisions are marked with // ….

Code
toml
1[dependencies]
2aqua-rs-sdk-core = { git = "https://github.com/inblockio/aqua-rs-sdk-core" }
3tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
4serde_json = "1"
5hex = "0.4"

Derive the signing identities

Each role gets its own Ed25519 seed (from a key store or HSM in production) and a did:key derived from it:

Code
rust
1use aqua_rs_sdk_core::core::signature::sign_did::DIDSigner;
2use aqua_rs_sdk_core::schema::SigningCredentials;
3use aqua_rs_sdk_core::Aquafier;
4// …
5 
6let aquafier = Aquafier::new(); // shared by all later snippets
7 
8let seed: [u8; 32] = /* per-role key material */;
9let did = DIDSigner.derive_did(&seed)?; // "did:key:z6Mk…"
10let credentials = SigningCredentials::Did { did_key: seed.to_vec() };

Emit an artifact (T4 tool call)

Create a typed object from the template and payload, attach the compositional links via an anchor revision, and sign the tip once:

Code
rust
1use aqua_rs_sdk_core::primitives::{HashType, Method, RevisionLink};
2use aqua_rs_sdk_core::schema::link::{Anchor, CompositionalLink};
3use aqua_rs_sdk_core::schema::template::BuiltInTemplate;
4use aqua_rs_sdk_core::schema::templates::AuditAgentToolCall;
5use aqua_rs_sdk_core::schema::{AnyRevision, AquaTreeWrapper};
6use aqua_rs_sdk_core::verification::Linkable;
7use serde_json::json;
8// …
9 
10// Typed object revision in its own tree.
11let template_link = RevisionLink::from_bytes(AuditAgentToolCall::TEMPLATE_LINK);
12let mut tree = aquafier.create_object(
13 template_link,
14 None,
15 json!({
16 "signer_did": agent_did,
17 "turn_id": turn_id, // = the T1 object's revision hash
18 "seq_in_turn": 2,
19 "tool_name": "inventory.order.status",
20 "tool_args": { "order_id": "ORD-1042" },
21 "risk_level": "low",
22 "created_at": now,
23 }),
24 None,
25)?;
26let t4_hash = tree.get_latest_revision_link().ok_or("created tree has no tip")?;
27 
28// Anchor revision carrying the role-tagged compositional links.
29let mut anchor = Anchor::with_links(
30 t4_hash.clone(),
31 Method::Scalar,
32 Vec::new(), // structural links
33 vec![
34 CompositionalLink::new(t1_hash.clone(), "aqua:in_user_turn"),
35 CompositionalLink::new(t3_hash.clone(), "aqua:prev_artifact_in_turn"),
36 ],
37);
38let anchor_hash = anchor.calculate_link(HashType::Sha3_256)?;
39anchor.populate_leaves(HashType::Sha3_256)?;
40tree.revisions.insert(anchor_hash, AnyRevision::Anchor(anchor));
41 
42// One signature over the tip, with the agent's key.
43let signed = aquafier
44 .sign_aqua_tree(AquaTreeWrapper::new(tree, None, None), &credentials, None, None)
45 .await?;
46let t4_tree = signed.aqua_tree;

The object's revision hash (t4_hash) is the artifact's stable content identity — the value later artifacts link to and the round anchor commits to. T1–T3 and T5–T8 follow the identical pattern with their own templates, payloads, links, and signers.

Close the turn with a round anchor

The server commits to the turn's artifact hashes with the SDK's domain-separated Merkle helpers (RFC 9162, formerly RFC 6962: leaf = H(0x00 || data), node = H(0x01 || left || right)), then records the commitment as an audit_round_anchor object. Typed payload structs give compile-time field names plus a validate() pre-flight:

Code
rust
1use aqua_rs_sdk_core::primitives::merkle;
2use aqua_rs_sdk_core::schema::templates::AuditRoundAnchor;
3// …
4 
5let artifact_hashes = [&t2_hash, &t3_hash, &t4_hash, &t5_hash, &t6_hash, &t7_hash, &t8_hash];
6let leaves: Vec<Vec<u8>> = artifact_hashes
7 .iter()
8 .map(|h| merkle::batch_leaf_hash(&HashType::Sha3_256, h.as_ref()))
9 .collect();
10let merkle_root = format!(
11 "0x{}",
12 hex::encode(merkle::merkle_root(&leaves, &HashType::Sha3_256))
13);
14 
15let round_anchor = AuditRoundAnchor {
16 signer_did: server_did.clone(),
17 session_id: session_id.to_string(),
18 turn_id: turn_id.clone(),
19 turn_index: 0,
20 artifact_count: artifact_hashes.len() as u64,
21 leaf_hashes: artifact_hashes.iter().map(|h| h.to_string()).collect(),
22 merkle_root,
23 closed_at: now,
24};
25round_anchor.validate()?;
26// then create_object + anchor + sign as in the previous step, using the
27// audit_round_anchor template — see the runnable example's template_source helper

merkle::merkle_root panics on an empty slice: a turn with no artifacts has nothing to anchor. When the session ends, the server emits an audit_session_close object the same way, referencing the last turn_id and round anchor hash.

Export self-descriptively

audit_round_anchor and audit_session_close are never resolvable from the verifier's built-in catalog, so trees using them must carry their template revisions. export_tree embeds them by default, and missing_templates is the lint publishers can run in CI over everything they ship:

Code
rust
1use aqua_rs_sdk_core::{missing_templates, ExportOptions};
2// …
3 
4let anchor_tree = aquafier.export_tree(
5 &anchor_tree,
6 std::slice::from_ref(&round_template_tree), // the template's own single-revision tree
7 &ExportOptions::default(),
8)?;
9assert!(missing_templates(&anchor_tree).is_empty());

An exported tree references no type a receiver cannot resolve; it verifies standalone with no side inputs. Opt out per call with ExportOptions::bare().

Verify every tree

Each artifact runs independently through the full pipeline — structure, hash integrity, template schema, signatures:

Code
rust
1let result = aquafier
2 .verify_aqua_tree(AquaTreeWrapper::new(t4_tree.clone(), None, None), vec![])
3 .await?;
4assert!(result.is_verified());

In the runnable example all ten trees verify with no linked trees and no side inputs, and no compute stage is ever entered — the audit chain is pure data.

Selective disclosure for audit trails

Object revisions use the tree hashing method by default, so every payload field is a salted Merkle leaf that can be redacted independently while the revision hash stays provable. The pseudonymous preset is a fixed policy for audit trees: reveal structure, identity, and chronology; seal content. Signature and anchor revisions stay full (attestations remain verifiable), T1 stays full (it carries no sensitive content), and T2–T8 become field-redacted by per-template allow-lists (all paths under /payloads/, plus the forced /previous_revision). Each allow-list discloses the pseudonymous skeleton — signer_did, turn_id, created_at, plus per-template structural facts such as T4's tool_name and risk_level, T5's endpoint and status_code, or T7's decision — while free-text content stays sealed. The normative per-template allow-lists are in Selective Disclosure.

Everything else is sealed as salted commitments: prompt text, thinking text, tool arguments and results, response bodies, HITL prompt and rationale, attachment names and sizes — and the revision's structural leaves. Redacting and verifying a single revision:

Code
rust
1use aqua_rs_sdk_core::{redact_revision, verify_redacted_revision, DisclosurePolicy, RevisionDisclosure};
2// …
3 
4let policy = DisclosurePolicy::pseudonymous(&t2_tree);
5let disclosed_paths = match policy.revisions.get(&t2_hash) {
6 Some(RevisionDisclosure::FieldRedacted(paths)) => paths.clone(),
7 other => return Err(format!("unexpected T2 disclosure policy: {other:?}").into()),
8};
9let t2_revision = t2_tree.revisions.get(&t2_hash).ok_or("revision missing")?;
10 
11let redacted = redact_revision(t2_revision, &t2_hash, &disclosed_paths)?;
12verify_redacted_revision(&redacted)?; // still proves the original revision hash

For mixed trees that contain non-audit templates, DisclosureProfile::audit() applies the same rules in a closed world: unknown template families are sealed, never disclosed by accident.

Know the limits

The /nonce leaf must never be disclosed — all per-leaf salts derive from it, and disclosing it makes every sealed low-entropy value brute-forceable; the exporter rejects any policy that lists it. Field presence is public by design: pointer paths, array cardinalities, and the leaf count stay cleartext. And redacted verification proves hash integrity of the disclosed subset only — it cannot re-validate the payload schema or verify a signature over sealed bytes. Never present a selectively disclosed artifact as having passed full verification. See Selective Disclosure.

Getting the templates

The sanctioned distribution channel for the audit family is the Template Registry. The family is published as audit-set-v1 under the vendor inblockio, publisher DID did:key:z6MkqDxSY5Z3gMNR2qKzV9ZwZDLwUYi5DqevZWhR7vaDWLCN, with short aliases (prompt, tool_call, approval, …) alongside the full names. Subscribe with a pinned trust policy — there is no trust-on-first-use:

Code
bash
1cargo run --bin subscribe -- \
2 --registry http://127.0.0.1:8088 \
3 --publisher did:key:z6MkqDxSY5Z3gMNR2qKzV9ZwZDLwUYi5DqevZWhR7vaDWLCN \
4 --trust trust/inblockio-audit-set-v1.json \
5 --store ./imported \
6 --once

A hosted registry runs without SLA at https://aqua-registry.inblock.io; note that the bundled subscribe client speaks plain http:// only in this release, while the hosted HTTPS endpoint can be queried directly with curl.

Transitional: built-in catalog resolution

In this release, audit_artifact and T1–T8 still resolve from the verifier's built-in catalog, which is why the example runs offline with no registry. This convenience is scheduled to be removed in favor of registry-only distribution — do not depend on it. audit_round_anchor and audit_session_close are already never catalog-resolved; self-descriptive export covers all cases either way.

What to record, practically

Mapping the event stream of a typical agent framework onto the family:

Agent-framework eventRecord asSigner
User message arrives (turn starts)T1 audit_user_turn_marker, then T2 audit_user_promptServer, then user session
Model emits reasoningT3 audit_agent_thinkingAgent
Agent invokes a toolT4 audit_agent_tool_callAgent
External API call, independently observedT5 audit_api_response (prov:wasGeneratedBy → T4)API attestor
Tool output returned to the agentT6 audit_tool_result (prov:used → T5)Agent
Human approves or rejects an actionT7 audit_hitl_approvalUser session
Agent repliesT8 audit_agent_response (is_final: true)Agent
Turn completeaudit_round_anchor over the turn's artifact hashesServer
Session endsaudit_session_closeServer

Divergence between what an API returned (T5, attestor-signed) and what the agent claims it saw (T6, agent-signed) is detectable by construction — that is the point of splitting the two.

See also

  • aqua-rs-sdk-core — the crate that hashes, signs, verifies, and discloses these trees
  • Template Registry — publishing, resolving, and subscribing to the audit family
  • Selective Disclosure — redacted revisions, selective trees, and their verification rules
  • Templates — template identity, derivation, and the shipped catalog
  • Quick Start — first steps with aqua-rs-sdk-core
Edit this pageReport an issue
Previous
Template Registry
Next
Protocol Reference

Documentation

  • Getting Started
  • Protocol Reference

Community

  • GitHub

Copyright © 2026 inblock.io assets GmbH. All rights reserved.

On this page

The audit template familyThe session modelIdentitiesOne tree per artifactOne turn, end to endWalkthroughSelective disclosure for audit trailsGetting the templatesWhat to record, practicallySee also