{
  "markdown": "# Aileron\n\n<!-- mcp-name: io.github.aileron-sh/aileron -->\n\n[![License: Apache-2.0](https://img.shields.io/badge/License-Apache--2.0-blue.svg)](LICENSE)\n[![Tests](https://github.com/Aileron-sh/aileron/actions/workflows/ci.yml/badge.svg)](https://github.com/Aileron-sh/aileron/actions/workflows/ci.yml)\n[![Python](https://img.shields.io/badge/python-3.10%2B-blue)](pyproject.toml)\n\n**Aileron is a flight recorder for AI agents.**\n\nNot another tracer. Aileron produces a tamper-evident, replayable record of\nevery tool call your agents make - evidence you can verify offline, not\ntelemetry you have to trust.\n\n- **Tamper-evident audit trail.** Every agent action is appended to a\n  SHA-256 hash-chained JSONL journal with Ed25519-signed checkpoints. Edit,\n  delete, or reorder a single line and `aileron verify` says exactly where\n  the chain broke.\n- **Policy enforcement on tool calls.** Sigma-like YAML rules with\n  `allow` / `alert` / `block` actions, applied *before* execution via the\n  MCP stdio proxy or the SDK decorator. A blocked tool call never runs; the\n  attempt is logged anyway.\n- **Forensic incident replay.** One command turns a journal into a\n  self-contained HTML incident report with a verification badge and a\n  filterable timeline - the answer to \"what did the agent actually touch?\"\n\n## 60-second quickstart\n\n```console\n$ pip install aileron\n$ aileron demo            # scripted fake-agent session (no network, no keys needed)\ndemo: wrote 8 events to demo.chain.jsonl\ndemo: chain VERIFIED (8 events)\ndemo: blocked shell call by rule aileron-001\ndemo: 2 anomaly alert(s) emitted\n$ aileron verify demo.chain.jsonl\nOK: 8 events verified in demo.chain.jsonl\n$ aileron report demo.chain.jsonl -o incident.html   # open it in a browser\n$ aileron serve --root .                             # or ask an assistant instead\n```\n\nThe demo runs in the default digest-only mode: the destructive shell call is\nblocked by a content rule and flagged by the behavioral baseline, yet the\njournal on disk contains only argument digests - never the raw command.\n\n## Features\n\n| Feature | What you get |\n|---|---|\n| Hash-chained journal | Append-only JSONL; each event's `prev_hash` links to the previous event's SHA-256 hash; genesis is `0x00…00` |\n| 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 |\n| 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 |\n| 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` |\n| 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 |\n| 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](https://registry.modelcontextprotocol.io) as `io.github.aileron-sh/aileron` |\n| OTel GenAI export | Events export as `gen_ai.*`-aligned span dicts (`aileron export`) for your existing collector |\n| HTML incident reports | Single file, inline CSS, no external assets, verification badge (`VERIFIED` / `TAMPERED at seq N`) |\n| Privacy by default | Tool arguments/results are recorded as digests only, unless you opt in with `--capture-content` |\n\n## Usage\n\n### SDK: `@track` decorator\n\n```python\nfrom aileron import ChainLog, track, PolicyBlocked, bundled_rules_dir\nfrom aileron.policy import load_rules\n\nlog = ChainLog(\"run.chain.jsonl\")            # capture_content=False by default\nrules = load_rules(bundled_rules_dir())      # or load_rules(\"rules\") after `aileron init`\n\n@track(log=log, rules=rules)\ndef shell(cmd: str) -> str:\n    ...  # your tool implementation\n\nshell(\"ls /tmp\")            # -> tool_call event, status=ok, args recorded as digest\nshell(\"rm -rf /\")           # -> PolicyBlocked raised; blocked attempt is logged\n```\n\nRules see the full arguments in memory at decision time; the journal still\nstores digests only. Turn on `capture_content=True` only when you want raw\narguments *persisted* for forensics.\n\n### SDK: `track_agent` session\n\n```python\nfrom aileron import track_agent\n\nwith track_agent(\"research-agent\", framework=\"langchain\", log=log):\n    shell(\"ls /tmp\")   # inherits the session's agent identity and session_id\n# agent_start / agent_end events bracket the run automatically\n```\n\n### MCP proxy: framework-agnostic interception\n\nWrap any MCP server. Every `tools/call` is logged and policy-checked *before*\nthe child process sees it:\n\n```console\n$ aileron init                       # seeds a ./rules directory with starter rules\n$ aileron proxy --log run.chain.jsonl --rules rules -- \\\n    npx -y @modelcontextprotocol/server-filesystem /tmp\n```\n\nA blocked call returns a JSON-RPC error (`-32000: blocked by aileron rule\n<id>`) to the client; the child is never invoked.\n\n**Verified against real MCP servers**, not just test doubles. Aileron has been\nrun in front of the official `@modelcontextprotocol/server-filesystem`\n(`secure-filesystem-server` 0.2.0, 14 tools) and `@modelcontextprotocol/server-memory`\n(0.6.3, 9 tools): the handshake completes, tools list normally, real calls work,\na blocked write never reaches the server, and the journal verifies. That check\nships as a test (`tests/test_real_mcp_server.py`, run with\n`AILERON_LIVE_MCP=1`).\n\nThe proxy itself costs **well under a millisecond per `tools/call`**.\nMatching content rules against large payloads costs more, and how much is\nyours to choose: see [Performance](#performance) for the split, measured.\n\nThe proxy speaks both newline-delimited and `Content-Length`-framed\nJSON-RPC. Content rules\n(`tool.arguments_contains`, `_regex`) work in the default digest-only mode -\n`--capture-content` changes what is persisted, not what is enforced. Calls\nstill in flight when the child dies are journaled with `status=error`, so a\ncrash never erases the attempt.\n\n### MCP server: ask your assistant what the agent did\n\nAileron sits in front of MCP servers. It is also one. Point it at a directory\nof journals and an assistant can read the record for you:\n\n```console\n$ aileron serve --root ./journals\n```\n\nThree tools, all read only: `verify_journal` (is this record intact),\n`query_events` (what happened, filtered by tool, status, or time), and\n`explain_rule` (what does aileron-130 catch).\n\nThere is no write, delete, or sign tool, and there should never be. The agent\nbeing recorded is the untrusted party, so giving it a way to edit the journal\nwould hand the suspect the evidence locker.\n\nFour things follow from that, and they are the reason this is more than a\nwrapper around `aileron verify`:\n\n- **Paths are confined to `--root`** and only `.jsonl` opens. Otherwise\n  `verify_journal(path)` is an arbitrary file read.\n- **Every answer carries its own integrity status.** Confinement stops an agent\n  reading files it should not; it does not stop one *writing* a plausible\n  journal inside the root and handing you an invented history. So each reply\n  says whether the chain verifies and whether an adjacent signed checkpoint\n  agrees.\n- **Recorded values are treated as hostile.** Tool names and rule ids were\n  chosen by the agent under investigation, so they reach an assistant labelled\n  as untrusted data, stripped of control characters, and truncated. A tool\n  named `IGNORE PREVIOUS INSTRUCTIONS...` is evidence to report, not an\n  instruction to follow.\n- **Digest-only stays digest-only.** `capture_content` governs what the journal\n  stores. It never widens what this server hands back, and errors never echo\n  file contents.\n\n### Policy rules\n\n```yaml\n# a policy rule (see the bundled rules/examples/destructive-shell.yml)\nid: aileron-001\ntitle: Block destructive shell commands\nseverity: high\nmatch:\n  type: tool_call\n  tool.name: shell\n  tool.arguments_contains: [\"rm -rf\", \"DROP TABLE\", \":(){ :|:& };:\"]\naction: block\n```\n\nDry-run rules against a recorded session: `aileron rules test rules/ run.chain.jsonl`\n\n## How it works\n\n```\nagent ──tool call──► [ SDK @track ] ──┐\n                     [ MCP proxy  ] ──┼─► policy decide (allow/alert/block)\n                                      │        │ block? ──► call never executes,\nMCP client ──JSON-RPC──► proxy ───────┘        │        attempt still logged\n                                               ▼\n                              append to chain log (JSONL)\n\n  event 0           event 1                      event N\n ┌──────────────┐  ┌──────────────┐        ┌──────────────┐\n │ seq: 0       │  │ seq: 1       │        │ seq: N       │\n │ prev: 0000…  │─►│ prev: H(e0)  │─► … ──►│ prev: H(eN-1)│\n │ hash: H(e0)  │  │ hash: H(e1)  │        │ hash: H(eN)  │──► Ed25519 checkpoint\n └──────────────┘  └──────────────┘        └──────────────┘    signature over tip\n\n  H(e) = sha256(canonical_json(e \\ hash))\n  aileron verify          → recompute every hash + link (exit 2 on tamper)\n  aileron verify-checkpoint → re-verify chain tip against Ed25519 signature\n```\n\nTampering with any event breaks the hash link at the first modified\nsequence; `verify` reports `first_bad_seq` and exits non-zero. The journal\nis local-only and self-contained - verification needs no network and no\ntrusted third party.\n\n## Performance\n\nThe proxy adds **well under a millisecond** per `tools/call`. Matching the full\n32-rule bundled pack against the payload is a **separate** cost that grows with\npayload size, and it is reported separately below, because the two scale\ndifferently and you choose your own rules.\n\nEvery number is reproducible with one command:\n\n```console\n$ python scripts/benchmark.py\n```\n\n**Method.** [`scripts/benchmark.py`](scripts/benchmark.py) drives an identical\nstdio MCP child server three ways - directly, through `aileron proxy` with no\nrules, and through `aileron proxy` with all 32 bundled rules - and subtracts.\nThe deltas are the proxy's true cost, so you never have to trust an absolute\nfigure. The absolute baseline is printed alongside so the subtraction can be\nchecked. 2,000 sequential calls per configuration after 200 discarded warmup\ncalls; digest-only journaling. Overhead covers JSON-RPC parsing, policy\nevaluation, hash-chain append, re-serialization, and the extra process hop.\n\n**About the payload.** The tool arguments are fixed text that looks like real\ntool arguments: English words, paths, flags, quotes and punctuation. That\nmatters more than it sounds. This benchmark used to send a run of one repeated\ncharacter, which is the friendliest possible input both to the regex engine,\nwhich fails on the first character everywhere, and to the literal prefilter\ndescribed below, which finds nothing anywhere. It was flattering the result by\nabout 3x. A test asserts no bundled rule fires on the filler, so these numbers\nare the ordinary path and not the alert path.\n\n### Added latency per `tools/call` (milliseconds)\n\n**Linux x86_64** - GitHub Actions `ubuntu-latest` (2 shared vCPU), Python\n3.12.14. Re-measured by CI on every push:\n[![Benchmark](https://github.com/Aileron-sh/aileron/actions/workflows/benchmark.yml/badge.svg)](https://github.com/Aileron-sh/aileron/actions/workflows/benchmark.yml)\n\n| tool arguments | direct | through proxy | + 32 rules | added by proxy | added by rules | **added total** |\n|---|---|---|---|---|---|---|\n| 64 B | 0.077 | 0.305 | 0.546 | 0.228 | 0.242 | **0.469** |\n| 4 KB | 0.078 | 0.348 | 0.740 | 0.270 | 0.392 | **0.662** |\n| 32 KB | 0.241 | 0.799 | 3.030 | 0.558 | 2.231 | **2.789** |\n\nShared CI runners vary between runs, by as much as 1.6x on the small-payload\nrow. This table quotes the slower of two consecutive measurements. The\nregression baseline uses the faster one, so a slow runner cannot quietly widen\nthe guard.\n\n**macOS arm64** - Apple M2 Pro, Python 3.13.7, idle machine. The worst of three\npasses, quoted whole, so `added = (proxy & rules) - direct` holds exactly\nwithin the run:\n\n| tool arguments | direct | through proxy | + 32 rules | added by proxy | added by rules | **added total** |\n|---|---|---|---|---|---|---|\n| 64 B | 0.016 | 0.089 | 0.195 | 0.073 | 0.105 | **0.178** |\n| 4 KB | 0.034 | 0.157 | 0.507 | 0.123 | 0.350 | **0.474** |\n| 32 KB | 0.156 | 0.457 | 2.604 | 0.300 | 2.147 | **2.447** |\n\n### What these numbers mean\n\n**The proxy is cheap and nearly flat.** Interception, journaling, and\nre-serialization cost about 0.23 ms on a small call and about\n0.56 ms on a 32 KB one, on the slowest hardware tested.\n\n**Rules cost more on big payloads, and the cost is yours to choose.** Content\nrules are matched against the payload, so their cost grows with payload size.\nWith all 32 bundled rules loaded that is 0.24 ms on a small\ncall and 2.23 ms at 32 KB.\n\n**Most of that work is skipped before it starts.** A rule looking for\n`auditctl` cannot fire on a payload with no `auditctl` in it. Each pattern is\nread once and reduced to the literals it requires, and cheap substring searches\ndecide whether the regex runs at all. Requirements are conjunctions, so a rule\nneeding `systemctl` near `disable` near `auditd` is skipped on ordinary prose\nthat merely contains the word \"service\". On a benign 32 KB call, 17 of the 19\npatterns that would otherwise scan the whole payload never run. It changes\nspeed and nothing else, and `AILERON_NO_PREFILTER=1` turns it off if you want\nit ruled out during an investigation.\n\n**In context.** A real MCP server call is typically 10 to 1000 ms. At\n2.8 ms for a 32 KB argument with every rule loaded, and\n0.47 ms for an ordinary small one, mediation is a small\nfraction of the call it is mediating.\n\n**Caveats, stated plainly.** These are *sequential* stdio round-trips, one\ncall in flight at a time, which is how an agent actually calls tools. This is\nnot a concurrent-client benchmark; a many-client run is on the roadmap. The\ntool reports mean, median, p95 and p99; these tables quote medians, because\nmedians are stable across runs and **p95 is not** - tail latency swings with\nscheduling. Linux is the slower machine because a shared-vCPU CI runner is\nslower than an idle laptop, and those are the conservative figures CI enforces.\nMeasure on your own hardware before quoting a number.\n\nCI enforces this: a job fails if median overhead regresses more than 2x against\n[`scripts/benchmark_baseline.json`](scripts/benchmark_baseline.json), so\nperformance cannot decay silently. It re-measures once before failing, so a\nsingle slow runner does not cry wolf. The baseline records both the rule-pack\nsize and the payload shape it was measured against, because a change to either\nis more work rather than slower code, and the guard says so instead of\nreporting a regression that is not there.\n\n## Integrations & ecosystem\n\n- **OpenTelemetry GenAI** - `aileron export` emits `gen_ai.operation.name` /\n  `gen_ai.tool.name` / `gen_ai.agent.name` span attributes plus\n  `aileron.event.hash`, so Aileron sits *beside* your existing tracing\n  stack as the evidence layer, not instead of it.\n- **LangChain / CrewAI / any Python framework** - `@track` is a plain\n  decorator; `track_agent` accepts a free-form `framework=` label. No\n  framework dependency is required.\n- **MCP** - the proxy wraps any stdio MCP server regardless of which client\n  or framework drives it.\n- **Community rules** - rule contributions are the intended contribution\n  unit (see Roadmap).\n\n## Telemetry & privacy\n\n- **Aileron sends no telemetry.** No analytics, no phone-home, no network\n  calls anywhere in the library or CLI. If that ever changes, it will be\n  opt-in only, behind a documented RFC - for a security tool, anything less\n  is disqualifying.\n- **Content capture is off by default.** Tool arguments and results are\n  recorded as SHA-256 digests; raw content is only stored when you pass\n  `capture_content=True` / `--capture-content`. You get a verifiable record\n  of *what happened* without persisting secrets or PII by accident.\n  Policy rules and the anomaly detector still see the full call in memory\n  at decision time - capture only controls what is *persisted*, never what\n  is *enforced*.\n\n## Honest limitations\n\n- **SDK instrumentation is bypassable.** `@track` wraps the functions you\n  decorate; code paths you don't instrument are not recorded. For\n  enforcement that agent code cannot skip, use the MCP proxy - mediation\n  happens in a separate process on the tool-call path.\n- **Policy rules are pattern matching, not intent classification.** They\n  catch known-bad shapes (`rm -rf`, `id_rsa`, exfil patterns); they will not\n  reliably detect novel malicious reasoning. Detection-of-effect\n  complements detection-of-intent tools (garak, PromptGuard); it does not\n  replace them.\n- **Tamper-evidence is not tamper-proof.** The chain proves modification\n  after the fact; an attacker with write access can truncate or rewrite the\n  whole log and forge it forward. Signed checkpoints make forgery require\n  the private key - keep keys off the host being recorded, and anchor\n  checkpoints externally (see Roadmap) if you need non-repudiation.\n\n## Roadmap\n\n- **`aileron-rules` community rule repo** - Sigma-for-agents: community\n  detection rules mapped to the OWASP Agentic Security Initiative's threat\n  taxonomy, CI-validated against recorded incident traces.\n- **Sigstore/Rekor checkpoint anchoring** - publish signed checkpoints to a\n  public transparency log for non-repudiable, third-party-verifiable\n  timestamps.\n- **OCSF export** - emit Open Cybersecurity Schema Framework events for\n  direct SIEM ingestion (Splunk/Elastic quickstarts).\n- **Out of scope for v1: eBPF / kernel-level interception.** Aileron stays\n  at the MCP-proxy and SDK layer where the semantic meaning of a tool call\n  is still visible; syscall-level tracing is Falco/Cilium territory and\n  would trade agent semantics for volume.\n\n## Contributing\n\nContributions are welcome - see [CONTRIBUTING.md](CONTRIBUTING.md). Good\nstarting points: new detection rules under `src/aileron/rules/examples/` and new\nframework adapters under `examples/`. DCO sign-off, no CLA. Security\nissues: see [SECURITY.md](SECURITY.md).\n\n## License\n\nApache License 2.0 - see [LICENSE](LICENSE).\n",
  "bytes": 18725,
  "sha": "8ff4d843b6c303d985fce56d1105774eaa86361027bd9a811ff34d2d6ae82355",
  "repo_slug": "aileron-sh/aileron",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_aileron_sh_aileron_872c1539/readme"
}