{"slug": "show-hn-mnestiq-tamper-evident-logs-of-what-an-ai-agent-saw-and-did", "title": "Show HN: Mnestiq – tamper-evident logs of what an AI agent saw and did", "summary": "A developer released Mnestiq, an open-source \"flight recorder\" for AI agents that produces tamper-evident, signed records of what an agent saw, decided and did, aimed at incident response rather than dashboards. Mnestiq hash-chains records using RFC 8785 canonical JSON and SHA-256 and periodically seals them with Ed25519-signed Merkle checkpoints under RFC 9162 trees, so edits, deletions, insertions and reordering fail verification. The tool captures provenance tags for system, user, model, tool_output, retrieved_doc, web, agent_msg and memory context, records outbound HTTP and raw TCP connections with local and remote IP and port, and ships as a drop-in SDK wrapping the Anthropic and OpenAI clients plus a `mnestiq proxy` mode that records model calls without code changes.", "body_md": "**A flight recorder for AI agents.** Tamper-evident, signed evidence of what an agent\nsaw, decided and did, built for incident response rather than dashboards.\n\nWhen an agent emails the customer list to an attacker, observability traces tell you\n*that* it happened. Evidence tells you *which* run and step did it, that the\ninstruction came from a web page rather than the user, that an auto-approve policy\nwaved it through, and lets you prove none of that was edited after the fact.\n\n``` php\nfirewall: 10.0.4.17:53120 -> 198.51.100.9:25 (50 bytes)   [no agent identity]\n\nmnestiq:  agent=support-bot run=c7831ff7 step=6 sandbox=container:7f3a9c operator=acme-helpdesk\n          untrusted input earlier in the run: step 4: web via fetch_page\n          authorized by policy:email-auto-approve (policy): recipient not checked against customer domain\n```\n\n- **Attribution:** every event carries`run_id` ,`step_id` ,`parent_run_id` ,`agent_id` ,`operator_id` and`sandbox_id` (auto-detected for containers and k8s pods).\n- **Egress identity:** outbound HTTP gets`X-Mnestiq-Run-Id` /`X-Mnestiq-Egress-Id` headers, and every outbound request*and* raw TCP connection (SMTP, databases, sockets)\nis recorded with its local and remote IP and port: the join key to flow and firewall logs.\n- **Provenance:** every piece of model context is tagged at capture time:`system` ,`user` ,`model` ,`tool_output` ,`retrieved_doc` ,`web` ,`agent_msg` ,`memory` . That's what makes prompt-injection analysis possible afterwards.\n- **Tamper evidence:** records are hash-chained (RFC 8785 canonical JSON, SHA-256)\nand periodically sealed by Ed25519-signed Merkle checkpoints (RFC 9162 trees).\nEdits, deletions, insertions and reordering fail verification.\n- **Redaction that keeps the proof:** strip prompts, tool arguments and tool output\nbefore handing evidence to an auditor; the chain still verifies.\n- **Drop-in SDK capture:** wraps the Anthropic and OpenAI clients (sync and async,\nChat Completions and Responses APIs). Recording fails open: it never breaks the agent.\n- **No code changes needed:**`mnestiq proxy` records an agent's model calls from outside it,\nwhatever language or framework it is written in. Point its base URL at the proxy.\n\n```\npip install \"mnestiq[anthropic]\"     # or mnestiq[openai]\nmnestiq keygen --out keys\npython\nimport anthropic\nfrom mnestiq import FileSink, Recorder\nfrom mnestiq.keys import load_private_key\nfrom mnestiq.egress import instrument_egress\nfrom mnestiq.integrations.anthropic import instrument_anthropic\n\nrecorder = Recorder(FileSink(\"evidence.jsonl\"), agent_id=\"support-bot\",\n                    operator_id=\"acme-helpdesk\", signing_key=load_private_key(\"keys/signing.key\"))\n\n@recorder.tool(source=\"web\")          # output of this tool is untrusted web content\ndef fetch_page(url: str) -> str: ...\n\nclient = instrument_anthropic(anthropic.Anthropic(), recorder)\ninstrument_egress()                   # stamp + record outbound connections during runs\n\nwith recorder.run(effective_permissions={\"email\": \"send\"}):\n    client.messages.create(model=\"claude-sonnet-5-5\", max_tokens=1024, messages=[...])\n\nrecorder.close()                      # writes the final signed checkpoint\nmnestiq verify evidence.jsonl --trusted-key keys/signing.pub\nmnestiq inspect evidence.jsonl\nmnestiq redact evidence.jsonl evidence.redacted.jsonl\n```\n\nA key file the agent can read is fine for trying it out. In production, keep the key where\nthe agent can't read it: `signer=SignerClient.from_env()` with `mnestiq signer serve` running\nunder another account, or `signer=AzureKeyVaultSigner(...)`. See [docs/signing.md](https://github.com/mnestiq/mnestiq/blob/main/docs/signing.md).\n\n```\nmnestiq proxy evidence.jsonl --agent support-bot --key keys/signing.key\nexport OPENAI_BASE_URL=http://127.0.0.1:8787/openai/v1      # and/or\nexport ANTHROPIC_BASE_URL=http://127.0.0.1:8787/anthropic\n```\n\nThe agent runs unchanged. Every model call it makes is written to `evidence.jsonl`, signed.\nWhat the proxy can and cannot see, and how to deploy it so an agent can't go around it:\n[docs/proxy.md](https://github.com/mnestiq/mnestiq/blob/main/docs/proxy.md).\n\nA scripted support agent reads a poisoned web page and emails customer data out. You then investigate the evidence it left behind.\n\n```\ngit clone https://github.com/mnestiq/mnestiq && cd mnestiq\npython -m venv .venv && . .venv/bin/activate        # Windows: .venv\\Scripts\\activate\npip install -e python httpx\npython examples/injection_incident.py --out evidence\nmnestiq dashboard evidence --trusted-key evidence/incident.pub\nmnestiq dashboard evidence/ --trusted-key keys/signing.pub\n```\n\nA local investigation view at the link it prints (`http://127.0.0.1:8765/?key=...`, whose key keeps\nother accounts on the machine out): runs and sub-runs, a step-by-step\ntimeline with every piece of model context colored by provenance, findings (below), the\nverification status of every record (sealed, unsealed, or tampered), and a connection\nfinder: paste an `ip:port` from a firewall or flow log and jump to the agent step that\nopened it. It live-updates while agents write.\n\n| Rule | Name | Fires when | \n|---|---|---|\n| `MNQ-001` | untrusted argument | A tool call's arguments carry an email address, URL, account number or long number that appears in untrusted content and nowhere in the system prompt or the user's messages. | \n| `MNQ-002` | auto-approval | An action was approved without a human after untrusted content reached the model, and the approved call carries a value from that content. A policy approving a routine reply is not a lead. | \n| `MNQ-003` | egress after taint | An outbound connection (not to loopback) followed untrusted content reaching the model. | \n| `MNQ-004` | untrusted sensitive action | A sensitive action (credentials, permissions, deletion, cancellations, account changes, payments, bookings) ran with an argument value that appears in untrusted content and not in the user's or system's words. Mark tools with `@recorder.tool(sensitive=True)` . Unmarked tools are judged by name. | \n| `MNQ-005` | untrusted link in answer | The model's answer carries an image, or a link with data in its query, to a host that appears in untrusted content and not in the user's or system's words. An image is fetched as soon as the answer is shown, so it can carry data out with no tool call. | \n| `MNQ-006` | internal data to new recipient | A tool sent data from the company's own systems (tool output labelled `internal` ) to an address or host that appears neither in those systems nor in the system prompt, such as the price list to a \"new customer\". No injection is needed. | \n\n`MNQ-001` is the signature of a hijack: an injected instruction has to put the attacker's\naddress, account or URL into a tool call. `MNQ-004` covers hijacks with no destination, such as\n\"change the password to ...\", \"delete file 13\" or \"cancel order 4100\". `MNQ-005` covers leaks\nthrough the answer itself, such as an injected image link that carries the customer's email\naddress. `MNQ-006` covers data handed to someone who simply asked for it. Findings describe what was observed, not a\nverdict: an agent that visits a link a colleague posted triggers `MNQ-001` too. The same\nrules are available in Python as `mnestiq.findings`.\n\nIt stays on your machine: it binds to loopback only, rejects non-local `Host` headers,\nrenders evidence strictly as text, and its Content-Security-Policy forbids the page from\nloading or contacting anything but the local server.\n\nTo see it with live data (no API keys needed):\n\n```\npython examples/live_agent.py --out evidence           # keeps a scripted agent running\nmnestiq dashboard evidence --trusted-key evidence/live.pub\n```\n\nSee [`examples/injection_incident.py`](https://github.com/mnestiq/mnestiq/blob/main/examples/injection_incident.py) for a full\nincident: an injected web page leads to exfiltration over a raw TCP connection; a\nfirewall log entry is traced back to the exact agent step; a tampered approval is\ncaught; a redacted copy is shared.\n\nThe recorder runs inside your agent, so it is built to never be the thing that breaks it.\n\n| Concern | Behaviour | \n|---|---|\n| **Recording fails** (disk full, closed sink, odd values) | The agent carries on. Failures are counted in `recorder.failures` , logged on the`mnestiq` logger, and warned about once. Pass`strict=True` to raise instead, if an unrecorded action is worse than a failed one. | \n| **Process dies mid-write** | On restart, `FileSink` moves the torn last line to`<file>.torn-<time>` (never deleted), the chain resumes from the last complete record, and a`note` event records the recovery. A write that fails part-way is truncated, so the file never holds half a record. | \n| **Huge prompts or tool output** | Values over `max_content_bytes` (default 1 MiB) are stored as their hash only and listed in`x-omitted` ; the chain still verifies. | \n| **Worker threads** | Threads don't inherit the active run. Wrap work with `run.bind(fn)` , e.g.`pool.submit(run.bind(fetch), url)` . | \n| **Signer down** (signing service or Key Vault) | Records are still written. The failure is counted and signing is tried again after `retry_after` seconds, covering everything since the last checkpoint. | \n| **Recorder stopped or killed** | `Recorder(..., checkpoint_interval=300)` writes and signs a`heartbeat` when nothing was signed for 5 minutes. A recorder that stopped then shows as a gap:`verify` warns about any silence longer than the heartbeats allow, and wherever checkpoints are sent (a`HeadFile` , the Evidence Vault) can alert while it is happening. | \n| **Timestamp authority down** | The checkpoint is still signed and written; only the RFC 3161 token is skipped, with a warning. | \n| **Power loss** | `FileSink(path, fsync=True)` forces each record to disk before the agent continues. | \n| **Someone deletes the end of the file** | `Recorder(..., on_checkpoint=HeadFile(\"/other/disk/agent.head.json\"))` keeps the latest checkpoint in a second place;`mnestiq verify evidence.jsonl --head /other/disk/agent.head.json` then reports a file cut back to an earlier checkpoint. | \n| **Sensitive content** | New evidence files are created owner-read/write only on Linux and macOS. On Windows a file takes its folder's access list, so keep evidence in a folder only the agent's account can read. Use `mnestiq redact` before sharing. | \n| **Large evidence in the dashboard** | The dashboard verifies the whole file but shows the most recent 20,000 records. | \n\n| Path | What | \n|---|---|\n| [`spec/SPEC.md`](https://github.com/mnestiq/mnestiq/blob/main/spec/SPEC.md) | The evidence format, hashing and verification rules. | \n| [`spec/schema/`](https://github.com/mnestiq/mnestiq/blob/main/spec/schema) | JSON Schema (2020-12) for records. | \n| [`python/`](https://github.com/mnestiq/mnestiq/blob/main/python) | Recorder, verifier and CLI. | \n| [`examples/`](https://github.com/mnestiq/mnestiq/blob/main/examples) | Runnable demos. | \n\nThe format makes evidence **tamper-evident after it is written**. It does not make\na compromised host tell the truth at write time. On its own, a file can be cut back\nto any earlier checkpoint without detection; keep the latest checkpoint somewhere\nelse with `HeadFile` and verify with `--head` to close that. Keep the signing key\nout of the agent's reach, in a signing service or Azure Key Vault\n([docs/signing.md](https://github.com/mnestiq/mnestiq/blob/main/docs/signing.md)), so it can't be copied and used to re-sign history.\nA signature only means something if you **pin the recorder's public key** with\n`--trusted-key`; otherwise someone who rewrites the whole file can re-sign it. Keys change\nonly by a signed hand-over. See [SPEC.md, sections 2 and 6](https://github.com/mnestiq/mnestiq/blob/main/spec/SPEC.md), and the\n[threat model](https://github.com/mnestiq/mnestiq/blob/main/docs/threat-model.md): every attack in it is a test, and you can try your own\non the [break-it kit](https://github.com/mnestiq/mnestiq/blob/main/examples/break-it/README.md).\n\nv0.2 draft. Done: spec, verifier, Python recorder, Anthropic + OpenAI capture, egress identity propagation (httpx, requests, raw TCP), local dashboard, signing outside the agent (signing service, Azure Key Vault) with signed key hand-overs, heartbeats, RFC 3161 timestamps from public authorities, checked by the verifier, and a model API proxy that records agents in any language from outside them.\n\nNext:\n\n1. External anchoring of checkpoint heads; S3 Object Lock sink.\n2. Automatic run propagation into executors (today: `run.bind` ); aiohttp; subprocess egress.\n3. OpenAI Agents SDK and LangGraph capture; streaming output capture.\n4. TypeScript SDK, an MCP proxy, and an OpenTelemetry (GenAI) trace receiver.\n5. Evidence test vectors for other implementations; Merkle inclusion proofs for selective disclosure.\n\n```\ncd python\npython -m venv .venv && source .venv/bin/activate    # Windows: .venv\\Scripts\\activate\npip install -e \".[dev,anthropic,openai,azure,timestamps]\" httpx requests ruff mypy\npytest\nruff check src tests\nmypy src\n```\n\nThe test suite never touches the network beyond this machine: `tests/_netguard.py`\nblocks any connection or DNS lookup that isn't loopback, so a test that tried would\nfail rather than quietly reach the internet.\n\nPlease report vulnerabilities privately; see [SECURITY.md](https://github.com/mnestiq/mnestiq/blob/main/SECURITY.md).\n\nApache-2.0. See [LICENSE](https://github.com/mnestiq/mnestiq/blob/main/LICENSE).", "url": "https://wpnews.pro/news/show-hn-mnestiq-tamper-evident-logs-of-what-an-ai-agent-saw-and-did", "canonical_source": "https://github.com/mnestiq/mnestiq", "published_at": "2026-10-21 13:01:54+00:00", "updated_at": "2026-10-21 13:12:34.375344+00:00", "lang": "en", "topics": ["ai-agents", "ai-safety", "ai-tools", "developer-tools", "artificial-intelligence"], "entities": ["Mnestiq", "Anthropic", "OpenAI", "RFC 8785", "RFC 9162", "Ed25519", "Azure Key Vault"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/show-hn-mnestiq-tamper-evident-logs-of-what-an-ai-agent-saw-and-did", "markdown": "https://wpnews.pro/news/show-hn-mnestiq-tamper-evident-logs-of-what-an-ai-agent-saw-and-did.md", "text": "https://wpnews.pro/news/show-hn-mnestiq-tamper-evident-logs-of-what-an-ai-agent-saw-and-did.txt", "jsonld": "https://wpnews.pro/news/show-hn-mnestiq-tamper-evident-logs-of-what-an-ai-agent-saw-and-did.jsonld"}}