Skip to content

Releases: aileron-sh/aileron

v0.1.5 - rules stop scanning payloads they cannot match

Choose a tag to compare

@k3vs3c k3vs3c released this 25 Aug 00:48

Performance release. Matching the bundled rule pack against a 32 KB tool
call was 23 ms in 0.1.4. It is now 2.8 ms.

It also corrects something 0.1.4 got wrong. That release shipped a README
claiming sub-millisecond median overhead, a figure measured when the pack
had 2 rules, alongside the 32 rules it actually released with. Anyone running
python scripts/benchmark.py, the command the README itself gives, would have
seen the contradiction immediately. The claim is gone and the numbers are
published in full, including the row that looks worst.

Rules skip regexes that cannot match

A rule looking for auditctl cannot fire on a payload with no auditctl in it,
but it was scanning every byte to find that out. Each pattern is now read once
and reduced to the literals it requires, and cheap substring searches decide
whether the regex runs at all. On a benign 32 KB call, 17 of the 19 patterns
that would otherwise scan the payload never run.

Requirements are conjunctions. Rule aileron-162 needs systemctl near
disable near auditd, so requiring only the first set meant ordinary prose
containing the word "service" still paid for a full scan.

This changes speed and nothing else. The risk is one-sided: skipping a regex
that would have matched is a rule that silently stops firing while the journal
still looks clean. So anything the extractor cannot fully read returns "run the
regex", and AILERON_NO_PREFILTER=1 disables it entirely.

Case folding was the trap. The prefilter needs a case-insensitive
containment test agreeing with re.IGNORECASE, and neither obvious choice
works. re.search("s", "ſ", re.I) matches but "ſ".lower() does not change,
and re.search("i", "ı", re.I) matches but "ı".casefold() does not change
either. Worse, U+0130 casefolds to two codepoints, splitting a
multi-character literal apart. Exactly four codepoints in Unicode are case-equal
to an ASCII character, and a test proves over all 1,114,112 of them that each
folds to exactly the character it equals.

Soundness is checked by a differential run over all 337 rule-pack examples, a
property test on generated patterns, a mutation fuzz over 31,570 mutants that
still match their pattern, and an independently written adversarial attack that
verified 2,324,740 matching pairs. None found a verdict difference.

The benchmark was flattering itself

It sent "x" * n, which is the friendliest possible input both to a regex
engine and to a literal prefilter. That overstated the result by about 3x, and
the README publishes those numbers. It now sends fixed text that looks like real
tool arguments, with a test asserting no bundled rule fires on it.

Measured on the CI runner with all 32 rules loaded:

tool arguments added by proxy added by rules added total
64 B 0.16 ms 0.31 ms 0.47 ms
4 KB 0.20 ms 0.46 ms 0.66 ms
32 KB 0.55 ms 2.24 ms 2.79 ms

The baseline now records the rule count and the payload shape, so changing
either is reported as more work rather than as slower code.

Fixed

The adoption metrics counted our own snapshot bot as an external contributor,
so the headline read 1 when the truth was 0. That figure exists to show whether
anyone outside the project cares, so it inflating itself was the worst place
for the bug to be.

Upgrading

No format changes, no API changes, no action required. Journals written by
0.1.2 through 0.1.4 verify unchanged.

pip install --upgrade aileron

v0.1.4 - Aileron is now an MCP server too

Choose a tag to compare

@k3vs3c k3vs3c released this 20 Aug 02:59

Added

aileron serve — a read-only MCP server over your journals.

Aileron sits in front of MCP servers. This makes it one, so you can ask an assistant what an agent did and have it read the answer out of the tamper-evident record instead of scrolling an HTML timeline.

Three tools: verify_journal, query_events, explain_rule.

Read-only is load-bearing, not cosmetic. The agent being recorded is the untrusted party, so a write or delete tool would hand the suspect the evidence locker. There is none, and a test enforces it.

Four defences beyond that, each for a specific attack:

  • Paths are confined to --root and only .jsonl opens, because verify_journal(path) would otherwise be an arbitrary file read.
  • Every answer carries its own integrity status. Confinement stops an agent reading files it should not. It does not stop one writing a plausible journal inside the root and handing you invented history, so each reply states whether the chain verifies and whether a signed checkpoint agrees.
  • Recorded values are treated as hostile. Tool names are attacker-chosen, so they are stripped of control characters, truncated, and labelled untrusted. A tool named IGNORE PREVIOUS INSTRUCTIONS... is evidence to report, not an instruction to follow.
  • Replies are byte-capped, the same reasoning as MAX_MESSAGE_BYTES in the proxy.

32 bundled detection rules, up from 2. Credential theft, cloud metadata abuse, exfiltration, supply chain, persistence, anti-forensics, database destruction, and agent-specific abuse. Every rule ships with the calls it must catch and the ordinary work it must ignore. A false positive fails the build.

server.json for the official MCP Registry. Aileron was not a server before this release, so listing it earlier would have been miscategorised.

An incident replay and writeup in examples/incident_replay.py and docs/what-did-it-touch.md, built around the July 2026 Hugging Face agent intrusion. The replay is honest about what the rules miss: the actual exfiltration walks past them, because it stages credentials to a temp file first. That is the argument for the journal rather than against it.

212 tests. Full detail in CHANGELOG.md.

v0.1.3 — verify now uses the evidence next to it

Choose a tag to compare

@k3vs3c k3vs3c released this 02 Aug 18:34

Closes a gap found by an external security scan of 0.1.2. No new vulnerability — this is a case where the tool could detect tampering but did not report it.

The problem

Truncating a journal's tail leaves a perfectly valid, shorter hash chain. So aileron verify returned:

OK: 3 events verified

…even when the signed <log>.checkpoints.jsonl sitting right next to the log attested that 6 events had existed. The proof of truncation was on disk and the tool walked past it.

That matters more than the severity suggests: verify is the command in the quickstart, and the one an operator reaches for under pressure. Returning green while adjacent evidence says otherwise is the worst possible answer.

The fix

When a checkpoints file is present, aileron verify now compares event count and tip hash against every checkpoint and exits 2 on a contradiction, saying whether the journal appears truncated or rewritten:

error: log has 3 events but a checkpoint attests to 6 — the journal appears truncated
TAMPERED: chain is internally valid but contradicts run.chain.jsonl.checkpoints.jsonl
note: signatures were not checked here — run 'aileron verify-checkpoint' with the public key you trust.

aileron report applies the same check, so a truncated journal can no longer render a VERIFIED badge. Both accept --skip-checkpoint-check for deliberate log rotation.

Scope of the guarantee

The cross-check is unauthenticated by designverify takes no key, so it compares structure rather than verifying signatures. It defeats naive truncation; an attacker who also rewrites the checkpoint file is not stopped by it. aileron verify-checkpoint with a public key you obtained out of band remains the cryptographic guarantee, and verify now points you there in its own output. SECURITY.md states the distinction plainly.

122 tests. Full detail in CHANGELOG.md.

v0.1.2 — critical security release

Choose a tag to compare

@k3vs3c k3vs3c released this 02 Aug 06:02

Critical security release. Upgrade from 0.1.1 and 0.1.0.

A follow-up adversarial audit confirmed the 0.1.1 fixes hold, but found a critical enforcement bypass shared by both earlier versions.

The issue

The proxy policed the parsed message but forwarded the raw bytes verbatim. Because a child can split that byte range differently than the proxy parsed it, a tools/call could execute without ever being policed or journaled — and aileron verify still reported OK, because nothing was tampered with; the call was simply never recorded.

Two working variants, both reproduced end-to-end against the published wheel:

  • Header smuggling — every line before the blank line was accumulated as "headers" and forwarded. A JSON-RPC message parked on its own line there is invisible to policy but executed by a newline-delimited child. 148 bytes was enough.
  • Body re-splitting — a Content-Length body may legally contain raw newlines, so the proxy saw one frame where the child saw several.

The damaging shape: with a block rule demonstrably active, a smuggled shell call exfiltrating ~/.ssh/id_rsa executed on the child while the journal recorded a different shell call as blocked. An auditor reading that journal would conclude enforcement worked.

The fix

Structural, not a patch on the specific attacks: the proxy now forwards a re-serialization of the message it policed — compact separators, ASCII-escaped, no raw newlines — in both directions. The message boundary the child sees is the same object the policy engine inspected, by construction. Header lines are validated against RFC 7230 and the header block is bounded.

Also in this release

  • Checkpoints are chained (signed index + prev_checkpoint_hash), so deletion, duplication, or reordering within the sequence is detected. Deleting the newest checkpoint remains tail truncation — now stated explicitly in SECURITY.md.
  • verify() reports raw invalid UTF-8 as tampering instead of raising.
  • The report badge escapes count and first_bad_seq.
  • A non-mapping params no longer crashes the proxy; MAX_MESSAGE_BYTES bounds every read path; pending is capped; the child wait is bounded so the shutdown drain always runs.
  • A blocked JSON-RPC batch is now answered with a batch response and blames the call that actually matched.

Full detail in CHANGELOG.md. 117 tests.

v0.1.1 — security release

Choose a tag to compare

@k3vs3c k3vs3c released this 02 Aug 04:55

Security release. Upgrade from 0.1.0.

An adversarial audit found six high-severity issues, all present in 0.1.0. The most serious: the MCP proxy failed open. Policy was applied only to messages matching dict + method == "tools/call" + a present id; everything else was forwarded to the child unchecked and unjournaled.

Four shapes of an ordinary blocked call slipped through:

  • a JSON-RPC batch array
  • a tools/call with no id
  • a payload CPython's JSON parser rejects but a child accepts (integer literal over 4300 digits)
  • a payload with one invalid UTF-8 byte

No malformed framing or exotic encoding required — plain, valid JSON. Because nothing was journaled, the bypassed calls were also invisible to aileron verify.

Also fixed: checkpoint rollback via reordering the checkpoints file, verify-checkpoint taking its trust anchor from the directory under audit, audit records silently dropped on duplicate JSON-RPC ids, duplicate-JSON-key and non-canonical-number smuggling past verify(), and an exception string persisted despite capture_content=False.

⚠️ Breaking change

Canonical JSON is now ASCII-escaped and rejects NaN/Infinity. This makes the integrity check total — a peer-supplied lone surrogate could previously raise inside verify() and suppress a TAMPERED verdict — but it changes the hash of any event containing non-ASCII content. Journals written by 0.1.0 containing non-ASCII will not verify under 0.1.1. Re-sign or archive them before upgrading.

Performance

The proxy read one byte at a time. Reading line-wise cuts overhead ~11x on 32 KB tool calls (2.16 ms → 0.19 ms). Measured overhead is now 0.08 ms (p50) at 64 B and 0.33 ms at 32 KB, via the new reproducible benchmarks/bench_proxy.py.

Full detail in CHANGELOG.md. 112 tests.

v0.1.0 — first public release

Choose a tag to compare

@k3vs3c k3vs3c released this 01 Aug 17:02

Changelog

All notable changes to this project will be documented in this file.

The format is based on Keep a Changelog,
and this project adheres to Semantic Versioning
once 1.0 is reached; 0.x releases may change APIs between minor versions.

[0.1.0] — 2026-07-23

Initial public release.

Added

  • Hash-chained audit journal (chainlog, events): append-only JSONL
    log of tool_call / llm_call / agent_start / agent_end /
    policy_decision / alert events; SHA-256 hash chain over canonical
    JSON; verify() with first_bad_seq reporting.
  • Ed25519 checkpoints (signing): generate_keypair,
    sign_checkpoint, verify_checkpoint for offline-verifiable log tips.
    Checkpoints use prefix semantics: events appended after signing never
    invalidate a checkpoint; truncating or rewriting the signed prefix does.
  • Policy engine (policy): Sigma-like YAML rules with
    allow / alert / block actions; dotted-key equality, _contains,
    _regex, and severity_gte matchers; example rules
    (rules/examples/destructive-shell.yml, secrets-exfil.yml). Rules are
    evaluated against the full call in memory, so content matchers fire even
    in the default digest-only mode — capture_content controls what is
    persisted, never what is enforced.
  • Behavioral anomaly detection (detect): rolling baselines flagging
    first-seen tools, rate spikes (>3x baseline), and novel tool-call
    sequences; live via the SDK baseline= hook or offline via
    aileron detect.
  • SDK instrumentation (sdk): @track decorator recording tool calls
    (digest-only by default; capture_content opt-in) and enforcing policy
    via PolicyBlocked; track_agent session context manager.
  • MCP stdio proxy (proxy): JSON-RPC 2.0 interception (newline- and
    Content-Length-framed) with pre-execution policy mediation; blocked calls
    return -32000 without invoking the child. In-flight calls that never
    receive a response (child crash/exit) are journaled with status=error
    on shutdown, so a crash cannot erase the attempt.
  • OTel GenAI export (otel): gen_ai.*-aligned span dicts
    (to_otel_spans) and OTLP/JSON export (to_otlp_spans, export_json)
    in the proto3 JSON mapping, suitable for OTLP/HTTP ingestion.
  • HTML incident reports (report): single-file, no-external-asset
    incident timeline with VERIFIED / TAMPERED verification badge.
  • CLI (aileron): init, verify, sign-checkpoint,
    verify-checkpoint, report, export, detect, rules test, proxy,
    demo.
  • Privacy posture: no telemetry anywhere; tool arguments/results stored
    as digests unless content capture is explicitly enabled.