The full upstream README, mirrored here for reference. Install config, tool schemas, adoption signals, and an original overview live on the Aileron Journal listing page.
Aileron is a flight recorder for AI agents.
Not another tracer. Aileron produces a tamper-evident, replayable record of every tool call your agents make - evidence you can verify offline, not telemetry you have to trust.
aileron verify says exactly where
the chain broke.allow / alert / block actions, applied before execution via the
MCP stdio proxy or the SDK decorator. A blocked tool call never runs; the
attempt is logged anyway.The demo runs in the default digest-only mode: the destructive shell call is blocked by a content rule and flagged by the behavioral baseline, yet the journal on disk contains only argument digests - never the raw command.
| Feature | What you get |
|---|---|
| Hash-chained journal | Append-only JSONL; each event's prev_hash links to the previous event's SHA-256 hash; genesis is 0x00…00 |
| Signed checkpoints | Ed25519 signature over the chain tip, verifiable offline against the public key (aileron sign-checkpoint / verify-checkpoint). Checkpoints cover a prefix: appending later events never invalidates them; truncating or rewriting the signed prefix does |
| Policy rules | 32 bundled rules covering credential theft, cloud metadata abuse, exfiltration, supply chain, persistence, anti-forensics, database destruction, and prompt-injection artifacts. Sigma-like YAML; substring, regex, and dotted-key matchers. Rules are evaluated against the full call in memory, so content rules fire even in digest-only mode |
| Behavioral anomaly detection | Rolling baselines flag first-seen tools, rate spikes (>3x baseline), and novel tool-call sequences - live via the SDK (baseline=) or offline via aileron detect |
| MCP stdio proxy | Sits between any MCP client and server; logs and mediates every tools/call before it reaches the child process. Verified against the official filesystem and memory servers, not just test doubles |
| MCP server mode | aileron serve exposes your journals read-only, so an assistant can answer "what did the agent touch?" from the record. Listed in the official MCP Registry as io.github.aileron-sh/aileron |
| OTel GenAI export | Events export as gen_ai.*-aligned span dicts (aileron export) for your existing collector |
| HTML incident reports | Single file, inline CSS, no external assets, verification badge (VERIFIED / TAMPERED at seq N) |
| Privacy by default | Tool arguments/results are recorded as digests only, unless you opt in with --capture-content |
@track decoratorRules see the full arguments in memory at decision time; the journal still
stores digests only. Turn on capture_content=True only when you want raw
arguments persisted for forensics.
track_agent sessionWrap any MCP server. Every tools/call is logged and policy-checked before
the child process sees it:
A blocked call returns a JSON-RPC error (-32000: blocked by aileron rule <id>) to the client; the child is never invoked.
Verified against real MCP servers, not just test doubles. Aileron has been
run in front of the official @modelcontextprotocol/server-filesystem
(secure-filesystem-server 0.2.0, 14 tools) and @modelcontextprotocol/server-memory
(0.6.3, 9 tools): the handshake completes, tools list normally, real calls work,
a blocked write never reaches the server, and the journal verifies. That check
ships as a test (tests/test_real_mcp_server.py, run with
AILERON_LIVE_MCP=1).
The proxy itself costs well under a millisecond per tools/call.
Matching content rules against large payloads costs more, and how much is
yours to choose: see Performance for the split, measured.
The proxy speaks both newline-delimited and Content-Length-framed
JSON-RPC. Content rules
(tool.arguments_contains, _regex) work in the default digest-only mode -
--capture-content changes what is persisted, not what is enforced. Calls
still in flight when the child dies are journaled with status=error, so a
crash never erases the attempt.
Aileron sits in front of MCP servers. It is also one. Point it at a directory of journals and an assistant can read the record for you:
Three tools, all read only: verify_journal (is this record intact),
query_events (what happened, filtered by tool, status, or time), and
explain_rule (what does aileron-130 catch).
There is no write, delete, or sign tool, and there should never be. The agent being recorded is the untrusted party, so giving it a way to edit the journal would hand the suspect the evidence locker.
Four things follow from that, and they are the reason this is more than a
wrapper around aileron verify:
--root and only .jsonl opens. Otherwise
verify_journal(path) is an arbitrary file read.IGNORE PREVIOUS INSTRUCTIONS... is evidence to report, not an
instruction to follow.capture_content governs what the journal
stores. It never widens what this server hands back, and errors never echo
file contents.Dry-run rules against a recorded session: aileron rules test rules/ run.chain.jsonl
Tampering with any event breaks the hash link at the first modified
sequence; verify reports first_bad_seq and exits non-zero. The journal
is local-only and self-contained - verification needs no network and no
trusted third party.
The proxy adds well under a millisecond per tools/call. Matching the full
32-rule bundled pack against the payload is a separate cost that grows with
payload size, and it is reported separately below, because the two scale
differently and you choose your own rules.
Every number is reproducible with one command:
Method. scripts/benchmark.py drives an identical
stdio MCP child server three ways - directly, through aileron proxy with no
rules, and through aileron proxy with all 32 bundled rules - and subtracts.
The deltas are the proxy's true cost, so you never have to trust an absolute
figure. The absolute baseline is printed alongside so the subtraction can be
checked. 2,000 sequential calls per configuration after 200 discarded warmup
calls; digest-only journaling. Overhead covers JSON-RPC parsing, policy
evaluation, hash-chain append, re-serialization, and the extra process hop.
About the payload. The tool arguments are fixed text that looks like real tool arguments: English words, paths, flags, quotes and punctuation. That matters more than it sounds. This benchmark used to send a run of one repeated character, which is the friendliest possible input both to the regex engine, which fails on the first character everywhere, and to the literal prefilter described below, which finds nothing anywhere. It was flattering the result by about 3x. A test asserts no bundled rule fires on the filler, so these numbers are the ordinary path and not the alert path.
tools/call (milliseconds)Linux x86_64 - GitHub Actions ubuntu-latest (2 shared vCPU), Python
3.12.14. Re-measured by CI on every push:
| tool arguments | direct | through proxy | + 32 rules | added by proxy | added by rules | added total |
|---|---|---|---|---|---|---|
| 64 B | 0.077 | 0.305 | 0.546 | 0.228 | 0.242 | 0.469 |
| 4 KB | 0.078 | 0.348 | 0.740 | 0.270 | 0.392 | 0.662 |
| 32 KB | 0.241 | 0.799 | 3.030 | 0.558 | 2.231 | 2.789 |
Shared CI runners vary between runs, by as much as 1.6x on the small-payload row. This table quotes the slower of two consecutive measurements. The regression baseline uses the faster one, so a slow runner cannot quietly widen the guard.
macOS arm64 - Apple M2 Pro, Python 3.13.7, idle machine. The worst of three
passes, quoted whole, so added = (proxy & rules) - direct holds exactly
within the run:
| tool arguments | direct | through proxy | + 32 rules | added by proxy | added by rules | added total |
|---|---|---|---|---|---|---|
| 64 B | 0.016 | 0.089 | 0.195 | 0.073 | 0.105 | 0.178 |
| 4 KB | 0.034 | 0.157 | 0.507 | 0.123 | 0.350 | 0.474 |
| 32 KB | 0.156 | 0.457 | 2.604 | 0.300 | 2.147 | 2.447 |
The proxy is cheap and nearly flat. Interception, journaling, and re-serialization cost about 0.23 ms on a small call and about 0.56 ms on a 32 KB one, on the slowest hardware tested.
Rules cost more on big payloads, and the cost is yours to choose. Content rules are matched against the payload, so their cost grows with payload size. With all 32 bundled rules loaded that is 0.24 ms on a small call and 2.23 ms at 32 KB.
Most of that work is skipped before it starts. A rule looking for
auditctl cannot fire on a payload with no auditctl in it. Each pattern is
read once and reduced to the literals it requires, and cheap substring searches
decide whether the regex runs at all. Requirements are conjunctions, so a rule
needing systemctl near disable near auditd is skipped on ordinary prose
that merely contains the word "service". On a benign 32 KB call, 17 of the 19
patterns that would otherwise scan the whole payload never run. It changes
speed and nothing else, and AILERON_NO_PREFILTER=1 turns it off if you want
it ruled out during an investigation.
In context. A real MCP server call is typically 10 to 1000 ms. At 2.8 ms for a 32 KB argument with every rule loaded, and 0.47 ms for an ordinary small one, mediation is a small fraction of the call it is mediating.
Caveats, stated plainly. These are sequential stdio round-trips, one call in flight at a time, which is how an agent actually calls tools. This is not a concurrent-client benchmark; a many-client run is on the roadmap. The tool reports mean, median, p95 and p99; these tables quote medians, because medians are stable across runs and p95 is not - tail latency swings with scheduling. Linux is the slower machine because a shared-vCPU CI runner is slower than an idle laptop, and those are the conservative figures CI enforces. Measure on your own hardware before quoting a number.
CI enforces this: a job fails if median overhead regresses more than 2x against
scripts/benchmark_baseline.json, so
performance cannot decay silently. It re-measures once before failing, so a
single slow runner does not cry wolf. The baseline records both the rule-pack
size and the payload shape it was measured against, because a change to either
is more work rather than slower code, and the guard says so instead of
reporting a regression that is not there.
aileron export emits gen_ai.operation.name /
gen_ai.tool.name / gen_ai.agent.name span attributes plus
aileron.event.hash, so Aileron sits beside your existing tracing
stack as the evidence layer, not instead of it.@track is a plain
decorator; track_agent accepts a free-form framework= label. No
framework dependency is required.capture_content=True / --capture-content. You get a verifiable record
of what happened without persisting secrets or PII by accident.
Policy rules and the anomaly detector still see the full call in memory
at decision time - capture only controls what is persisted, never what
is enforced.@track wraps the functions you
decorate; code paths you don't instrument are not recorded. For
enforcement that agent code cannot skip, use the MCP proxy - mediation
happens in a separate process on the tool-call path.rm -rf, id_rsa, exfil patterns); they will not
reliably detect novel malicious reasoning. Detection-of-effect
complements detection-of-intent tools (garak, PromptGuard); it does not
replace them.aileron-rules community rule repo - Sigma-for-agents: community
detection rules mapped to the OWASP Agentic Security Initiative's threat
taxonomy, CI-validated against recorded incident traces.Contributions are welcome - see CONTRIBUTING.md. Good
starting points: new detection rules under src/aileron/rules/examples/ and new
framework adapters under examples/. DCO sign-off, no CLA. Security
issues: see SECURITY.md.
Apache License 2.0 - see LICENSE.