How the Agent Action Ledger Audit Chain Works

Agent Action Ledger interface displayed on a silver laptop, showing a terminal-based action ledger with verified entries, capability events, and execution status against a dark purple background.

Most AI agent deployments log what agents do. Those logs are written by the agent, by the orchestrator, or by the application layer that the agent called. They are append-only by convention. They record what each component chose to record. The agent can write any value it wants into a requester or authorized_by field. Nothing in the stack enforces that the agent’s identity claim is accurate, that its authorization claim is verifiable, or that the record produced was not modified afterward.

That is not a tamper-evident audit trail. That is a collection of contemporaneous notes.

The distinction matters the moment someone needs to certify the records are authentic – in a regulatory audit, a discovery proceeding, or an incident investigation. Advisory logs are defensible as best-effort operational telemetry. They are not defensible as a chain of custody.

Agent Action Ledger is built around a different model: enforcement and evidence on the same path. This post covers the technical construction of that model.

The record structure

Every action AALD processes produces a ledger entry. The core fields:

sequence       integer          monotonically increasing, no gaps
agent_id       string           authenticated agent identifier
capability     string           declared capability name (e.g. send_email)
status         string           EXECUTED | REJECTED | ERROR
authorized_by  string           derived from agent registration - not agent-supplied
params_hash    hex string       SHA-256 of the canonical parameter payload
params_enc     bytes            AES-256-GCM encrypted parameter payload
timestamp      ISO 8601 UTC     recorded at execution time
prev_hash      hex string       SHA-256 of the previous entry's canonical form
entry_hash     hex string       SHA-256 of this entry's canonical form

Each entry links to the one before it through prev_hash. The first entry hashes a fixed genesis value. The result is a chain: modifying any entry changes its hash, which invalidates every subsequent entry’s prev_hash. The break is detectable at any point in a forward verification pass.

Canonical JSON and why the normalization step is not optional

The hash of a JSON object is not stable unless the serialization is deterministic. Two records can be semantically identical – same keys, same values – but produce different hashes if key order varies between serializations, if one includes trailing whitespace, or if floating-point values serialize differently across platforms.

AALD normalizes before hashing. Canonical form: keys sorted lexicographically at every nesting level, no insignificant whitespace, no trailing newline. The same logical record always produces the same byte sequence. The hash is deterministic across platforms, runtimes, and time.

This is not a novel construction. It follows the same principle as JWS (RFC 7515) and JSON Canonicalization Scheme (RFC 8785). The implementation is explicit rather than delegated to a serialization library that may change behavior across versions.

The verification path uses the same normalization. You do not need AALD running to verify the chain – you need the record store and the normalization spec.

The hash construction in full

For a given entry at sequence n:

canonical_json = canonicalize({
    sequence:      n,
    agent_id:      ...,
    capability:    ...,
    status:        ...,
    authorized_by: ...,
    params_hash:   ...,
    timestamp:     ...,
    prev_hash:     entry_hash[n-1]
})

entry_hash[n] = SHA-256(canonical_json)

prev_hash is included in the canonical form before hashing. This means the chain is not just a linked list of independent records – each entry’s hash is a function of every entry that came before it. You cannot rewrite entry 500 of 3,000 and produce a consistent chain without recomputing every subsequent hash. You also cannot insert a record between entries 500 and 501 without the same recomputation.

A partial rewrite that maintains chain consistency would require recomputing up to thousands of sequential SHA-256 operations and rewriting the stored values. That is detectable through out-of-band checkpointing, which AALD runs on a configurable background interval.

Verification output

[chain verify] entries: 3,847  |  status: OK
[chain verify] segment checkpoints: 38  |  all consistent
[chain verify] elapsed: 0.4s

If verification fails:

[chain verify] entries: 3,847  |  status: FAIL
[chain verify] break detected at sequence: 1,204
[chain verify] expected prev_hash: 9f2c44a1d7b03e52...
[chain verify] found prev_hash:    a1b2c3d4e5f60718...

Startup integrity verification runs before AALD accepts any requests. Background verification runs on a configurable interval. The TUI surfaces chain status in the header on every refresh. A break surfaces immediately – not during forensic review.

The parameter payload: hash plus encrypted content

Action parameters present a design tension. Including them in full in the chain record makes the chain readable without additional tooling, but exposes potentially sensitive data – email recipients, API payloads, internal identifiers – in the ledger. Excluding them makes the chain easier to store but leaves it without recoverable forensic content.

AALD uses both representations.

params_hash is SHA-256 of the canonical parameter payload. It is stored in the chain record and included in the hash construction. This makes the parameters verifiable – you can confirm that a given payload matches the recorded hash – without decrypting anything. The hash is part of the chain and shares its tamper-evidence guarantees.

params_enc is the parameter payload encrypted with AES-256-GCM under an operator-held key, stored alongside the chain record. It is not part of the hash input. Decryption requires the operator key; the key never enters the chain store. For forensic or legal review, the payload is decrypted on demand and its hash verified against params_hash to confirm the decrypted content matches the original.

This means: a chain verifier can confirm integrity without access to the encryption key. A forensic examiner can recover the original parameters by decrypting with the key and verifying against the chain.

Authorization derivation

In most agent stacks, authorized_by is a string the agent supplies at call time. The agent asserts its own authorization. The log records that assertion. Under review, you cannot distinguish between a field the agent was told to set and a field that reflects a verified authorization record.

AALD removes agent input from this field entirely.

When an agent is registered, its owning principal – a user account or service identity – is recorded in the agent registry. At execution time, AALD copies authorized_by from the registry entry for the calling agent. The agent has no parameter that contributes to this field. An agent cannot misrepresent its authorization because it has no mechanism to do so.

The practical result: authorized_by in a ledger entry is a fact about the agent’s registration, not a claim the agent made about itself. Under review, that distinction is the difference between “the agent asserted it was authorized by this user” and “this agent was registered to this user on this date, and that relationship is part of the verifiable chain.”

Agent authentication

Agent identity is verified with bcrypt before any capability evaluation or ledger write occurs. Each registered agent has a bcrypt-hashed key. The agent presents its key; AALD verifies against the stored hash. An agent that fails authentication does not reach capability evaluation. A failed authentication attempt is written to the security events trail.

bcrypt is used rather than a faster digest algorithm specifically because key verification in this context is not a high-frequency hot path. The cost factor makes offline brute-force attacks against a compromised key store expensive. Key rotation is an administrative operation with a recorded timestamp.

Rejected actions are on the record

An agent that requests a capability it is not authorized for does not fail silently. The attempt is evaluated, rejected, and written to both the main ledger chain (status: REJECTED) and the security events trail. The security events trail is a separate append-only record aligned by timestamp with the main chain.

This means a complete picture of agent behavior includes what agents were denied, not only what they executed. Under review, that matters: an agent that was rejected 400 times before succeeding on the 401st is a different security story than an agent with a clean execution record.

MCP integration

AALD ships with an MCP (Model Context Protocol) server. If your agents already use MCP, configured capabilities are exposed as MCP tools. The agent calls the tool through the MCP interface; AALD receives the request, authenticates the agent, evaluates the capability grant, writes to the chain, executes through server-held credentials, and returns the response.

The MCP layer is a transport. Enforcement and ledgering happen in the daemon regardless of how the request arrived – MCP, REST, or direct call. There is no MCP-specific code path that bypasses the authentication and ledger write steps.

The MCP server component is open source under MIT. It can be connected to any AALD instance and used to observe the enforcement model – rejections, chain writes, authorization derivation – on your own infrastructure before any purchase decision.

Air-gapped operation

License validation runs at daemon startup against a locally stored license file. If validation passes, the daemon starts and begins accepting requests. No subsequent outbound connection to the license server is required during operation. The daemon can run indefinitely in an air-gapped environment after initial activation.

Air-gapped activation is a separate process: the license file is issued offline and transferred to the target environment. Activation does not require the daemon host to reach the license server. This is an explicit design constraint, not a deferred feature – the daemon was built from the start to operate without a persistent connection home.

The chain store is SQLite. There is no external database, no control plane, no telemetry relay, and no vendor-side copy of ledger records. The chain lives where you put the binary.

What “tamper-evident” means in practice

Tamper-evident is a property, not a guarantee of tamper-prevention. The chain does not prevent modification – a sufficiently privileged attacker with access to the store can overwrite records. What the chain guarantees is that modification is detectable: any change to a historical entry breaks the hash linkage from that point forward, and that break is surfaced on the next verification pass.

Detection is meaningful when verification is independent of the storage path being protected. AALD’s verification is a read-only pass over the chain records using the same normalization and hash construction as the write path. It requires no trust in the writing process. An external auditor with the record store, the normalization spec, and the SHA-256 implementation can verify the chain without AALD running.

That is what makes it useful for legal and compliance review: the chain can be verified by the party asking questions, not only by the party that produced it.

Open source MCP server: github.com/mmediasoftwarelab/aald-mcp
Agent Action Ledger: mmediasoftwarelab.com/product/aald/

// real.developer.js
const approach = {
investors: false,
buzzwords: false,
actualUse: true,
problems: ['real', 'solved']
};
// Ship it.

Built by People Who Actually Use the Software

M Media software isn't venture-funded, trend-chasing, or built to look good in pitch decks. It's built by developers who run their own servers, ship their own products, and rely on these tools every day.

That means fewer abstractions, fewer dependencies, and fewer "coming soon" promises. Our software exists because we needed it to exist — to automate real work, solve real problems, and keep systems running without babysitting.

We build software the way it used to be built: practical, durable, and accountable. If a feature doesn't save time, reduce friction, or make something more reliable, it doesn't ship.

Every feature solves a problem we actually had
No investor timelines forcing half-baked releases
Updates add value, not just version numbers
Documentation written by people who got stuck first

This is software designed to stay installed — not be replaced next quarter.

Subscription Hell
  • • Payment fails? App stops
  • • Need online activation
  • • Forced updates
  • • Data held hostage
M Media Way
  • • Buy once, own forever
  • • Works offline
  • • Optional updates
  • • You control your data

Simple Licensing. No Games.

We don't believe in dark patterns, forced subscriptions, or holding your data hostage. M Media software products use clear, upfront licensing with no hidden traps.

You buy the software. You run it. You control your systems.

Licenses are designed to work offline, survive reinstalls, and respect long-term use. Updates are optional, not mandatory. Your tools don't suddenly stop working because a payment failed or a server somewhere changed hands.

One-time purchase, lifetime access
No "cloud authentication" breaking your workflow
Upgrade when you want to, not when we force you
Software empowers its owner — not rent itself back