Tamper-evident audit records for MCP tool-call decisions.
Project description
krono
Tamper-evident audit records for MCP tool-call decisions. Python 3.11+, stdlib only, MIT.
What krono is
krono is a Python library that appends one HMAC-chained JSONL record per allow/deny decision an MCP server makes at its tool boundary. A separate krono verify CLI re-walks the file with the same key and detects per-entry tampering (including the last entry), reordering, middle deletion, sequence rewriting, and schema violations — from (log file, key) alone, with no external state.
What krono is NOT
- Not a policy / allow-deny engine. The integrator decides allow vs. deny;
kronorecords the decision. There is no built-in policy DSL, no approval workflow, no rule store. - Not a multi-process append target. One
AuditLoginstance per file per process. Multi-process writes against the same file are undefined behavior in v1. - Does NOT detect tail truncation. If an adversary deletes the final N entries, the verifier walks the remaining prefix and returns
ok=True. Detecting tail truncation requires an external anchor (sidecar signed-head file), which is deferred future work — seedocs/HONEST-CLAIMS.md. - Not a SaaS product. Library-only — no daemon, no exporter, no dashboard, no policy engine. Released on PyPI as
krono-py(see Install); a small portfolio-scope library, not a hosted service.
Install
pip install krono-py
# or, with the MCP integration extra used by examples/note_server.py:
pip install "krono-py[mcp]"
Note the distribution name is krono-py (the bare krono name on PyPI
was already taken). The import name is still krono:
from krono import AuditLog, Decision, verify
Released from https://pypi.org/project/krono-py/. Release/publishing process
documented in docs/PUBLISHING.md.
Quickstart
# 1. Generate an HMAC key (32 raw bytes, hex-encoded).
export KRONO_AUDIT_KEY=$(python -c "import secrets; print(secrets.token_bytes(32).hex())")
# 2. Record one event and verify it (~10 lines of Python).
python - <<'PY'
import os, tempfile, pathlib
from krono import AuditLog, Decision, Identity, verify
log = pathlib.Path(tempfile.mkdtemp()) / "demo.jsonl"
with AuditLog(log) as audit:
# v0.1.x two-string form still works:
audit.record(tool_name="read_note", decision=Decision.ALLOW,
arguments={"id": "1"}, declared_identity="me",
authenticated_identity=None, reason="demo")
# v0.2.0 adds an Identity dataclass as a constructor-side
# convenience — on-disk format is byte-identical to v0.1.x:
audit.record(tool_name="read_note", decision=Decision.ALLOW,
arguments={"id": "2"},
identity=Identity(declared="me"),
reason="demo-with-identity")
print("log:", log)
print("verify:", verify(log))
PY
# 3. Verify from the CLI too.
krono verify <log-path-from-above>
Building from source (for development) instead:
git clone https://github.com/kronoguard/krono-py-lib.git && cd krono-py-lib
uv sync --all-extras
make quality && make test # full suite incl. §17 acceptance gate
uv run python examples/note_server.py
HMAC key management
krono uses HMAC-SHA256. The key is required, must be at least 32 raw bytes after hex decoding (64 hex characters), and is sourced from $KRONO_AUDIT_KEY by default. An explicit key: bytes may be passed to AuditLog(...) or verify(...) to override.
Generation (one-time, per deployment):
export KRONO_AUDIT_KEY=$(python -c "import secrets; print(secrets.token_bytes(32).hex())")
Fail-loud behavior (FR-02): if $KRONO_AUDIT_KEY is unset, empty, non-hex, or shorter than 32 bytes, AuditLog(...) and verify(...) raise MissingKeyError before any filesystem operation. The library never generates an ephemeral key as a fallback — the ChronoGuard secret_key or secrets.token_bytes(32) anti-pattern is explicitly forbidden. An audit chain signed with a key only the dead process ever knew is the same as having no chain at all.
The exception message references only the env-var name, never the key material itself. The key is never written to the audit file, never logged, never echoed.
MCP integration example
examples/note_server.py is the reference Pattern-1 integration — one audit.record(...) call inline in each tool body. The script mocks a tiny note server with two tools and produces two events end-to-end:
audit = AuditLog(os.environ["KRONO_LOG_PATH"])
def read_note(audit, note_id, client_name):
# Pattern 1: record BEFORE running the tool body.
audit.record(
tool_name="read_note",
decision=Decision.ALLOW,
arguments={"id": note_id},
declared_identity=client_name, # caller-asserted
authenticated_identity=None, # auth boundary did not run
reason="default-allow read tool",
)
# Equivalent v0.2.0 form using the Identity dataclass (FR-41/42) —
# on-disk bytes are identical to the two-string call above:
# audit.record(..., identity=Identity(declared=client_name),
# reason="default-allow read tool")
return f"<note id={note_id}>" # real tool body would do the DB read
def delete_note(audit, note_id, client_name):
audit.record(
tool_name="delete_note",
decision=Decision.DENY,
arguments={"id": note_id},
declared_identity=client_name,
authenticated_identity=None,
reason="destructive",
)
return f"DENIED: cannot delete note {note_id} (destructive)"
Three other integration patterns are illustrated under examples/:
examples/audit_singleton.py— module-level singleton with restart-resume (FR-16 in action).examples/fastmcp_dispatch.py— hook-style dispatch wrapper: one central place records every tool call before the body runs.examples/with_bearer_auth.py— auth-boundary identity wiring: how to keepdeclared_identityandauthenticated_identitydistinct.
See docs/USAGE.md for a deeper walkthrough of each pattern.
krono verify
The CLI is a single subcommand:
krono verify [--key-env VAR] [--json] <log_path>
Output (text mode, success):
✓ krono audit verified: 247 entries (sequence 0..246)
note: tail truncation not detectable from log alone (see HONEST-CLAIMS.md)
Output (text mode, failure):
✗ krono audit FAILED at line 132 (sequence 131): content_tampered
current_hash mismatch
expected: 6c2d5849d22e3c4d...
actual: 41a733eac1fd344a...
--json mode emits one JSON object on stdout matching the schema in spec/SPEC_KRONO_PY_LIB.md §Interfaces; exit codes are unchanged.
Exit codes:
| Code | Meaning |
|---|---|
| 0 | Verified (what is present) |
| 1 | Tampering detected |
| 2 | Usage error (bad/missing args; argparse default) |
| 3 | Configuration or I/O error (key missing, file unreadable) |
--key-env VAR overrides the env-var name (default KRONO_AUDIT_KEY) — useful in multi-tenant deployments where one machine holds several keys.
Honest claim boundary
krono makes a narrow, deliberately small set of claims and documents what it does not detect. The summary:
- Detected: per-entry tampering on any byte of any entry (including the last); reordering; middle deletion; sequence-number rewriting; payload permutation; schema violations; invalid JSON.
- NOT detected: tail truncation (the last N entries deleted with no gap left behind); whole-file deletion; forgery by an attacker holding the HMAC key; integrators passing fabricated
authenticated_identityvalues; argument-secret leakage viatool_nameorreason(onlyargumentsis hashed). - Investigability trade-off: raw arguments are hashed (FR-07) and never written to the log. The hash proves a known arguments value matches a given event but does not enable browsing what was sent.
Full enumeration with FailureKind values and test names lives in docs/HONEST-CLAIMS.md. The single phrase tail truncation appears there, in the CLI success message, and in this README's "What krono is NOT" — by design. Audit libraries that hide their limits invite the kind of misplaced trust the mcp-firewall reviewer flagged; we make the limits load-bearing instead.
License
MIT. See LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file krono_py-0.2.0.tar.gz.
File metadata
- Download URL: krono_py-0.2.0.tar.gz
- Upload date:
- Size: 151.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f206a2e4518914c68497fece0ffbfbf36354cb890cc9467df8b996abaa80787c
|
|
| MD5 |
5f571b1ccaab27d292246b2d23f0f662
|
|
| BLAKE2b-256 |
120b9447a4fa822750bf3a16d78150bde925d3ca77713763c58f2ddb517f0659
|
Provenance
The following attestation bundles were made for krono_py-0.2.0.tar.gz:
Publisher:
pypi.yml on kronoguard/krono-py-lib
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
krono_py-0.2.0.tar.gz -
Subject digest:
f206a2e4518914c68497fece0ffbfbf36354cb890cc9467df8b996abaa80787c - Sigstore transparency entry: 1631032521
- Sigstore integration time:
-
Permalink:
kronoguard/krono-py-lib@06804321958bb41e755471c3bb8bb4e5a4dd2f61 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/kronoguard
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi.yml@06804321958bb41e755471c3bb8bb4e5a4dd2f61 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file krono_py-0.2.0-py3-none-any.whl.
File metadata
- Download URL: krono_py-0.2.0-py3-none-any.whl
- Upload date:
- Size: 25.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fe4c22ddc796944fe82e8221526cf4b5f9a85495ef9d73f20fdd3b9628dbeba3
|
|
| MD5 |
6cc70e8a74595e46926fe95f912fb0b8
|
|
| BLAKE2b-256 |
6e43118a7fdbe2121fce7ca3f04d65947b3534d7c6273dd8c05d605caeae307e
|
Provenance
The following attestation bundles were made for krono_py-0.2.0-py3-none-any.whl:
Publisher:
pypi.yml on kronoguard/krono-py-lib
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
krono_py-0.2.0-py3-none-any.whl -
Subject digest:
fe4c22ddc796944fe82e8221526cf4b5f9a85495ef9d73f20fdd3b9628dbeba3 - Sigstore transparency entry: 1631032529
- Sigstore integration time:
-
Permalink:
kronoguard/krono-py-lib@06804321958bb41e755471c3bb8bb4e5a4dd2f61 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/kronoguard
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
pypi.yml@06804321958bb41e755471c3bb8bb4e5a4dd2f61 -
Trigger Event:
workflow_dispatch
-
Statement type: