Skip to content

Reference

Everything technical: every CLI command, the evidence-log format, hypothesis gating, the Rust API, the security model, and troubleshooting. For a guided first run, start with Getting Started. For the on-disk format, see FORMAT.md.

Why Litatoli?

SOC 2, HIPAA, IEC 62304 and the rest all assume the person who approved your code understood it. AI-generated code strains that assumption, and when the model writes both the implementation and the tests, the coverage reflects the model's reading of the requirements rather than an independent check of them.

Litatoli records the difference — structurally, not on the model's word:

  • Command execution with evidence — captures exit code, stdout/stderr, FS diffs, network activity, all keyed-BLAKE3-signed
  • Git verification — proves a commit exists, a push reached the remote, commit details match expectations
  • Hypothesis gating — blocks costly actions (cargo build, docker-compose up, database migrations) unless chain-validated prior evidence exists
  • Message scanning — detects success claims in agent output and verifies them against reality
  • Shell injection prevention — POSIX-compliant lexer rejects injection attempts before execution
  • Database state verification — hashes query output to prove database state at a point in time
  • Network activity capture — monitors /proc/net/tcp{,6} to record new outbound TCP connections during execution
  • Append-only evidence log — keyed-BLAKE3-signed, chain-linked JSONL for forensic audit trails

Command walkthrough

Run a command and capture evidence

litatoli run echo "hello world"

Output:

{
  "overall_ok": true,
  "specs": [
    {
      "id": "run:echo hello world",
      "ok": true
    }
  ],
  "metrics": {
    "command": "echo hello world",
    "exit_code": 0,
    "stdout": "hello world\n",
    "stderr": "",
    "elapsed_ms": 5,
    "verified": true,
    "evidence_type": "command_run",
    "blake3_signature": "a1b2c3d4...",
    "network_activity": {
      "new_connections": [],
      "outbound_count": 0
    }
  }
}

Verify a file exists

litatoli verify-file ./README.md
{
  "overall_ok": true,
  "specs": [{ "id": "verify-file:./README.md", "ok": true }],
  "metrics": {
    "evidence_type": "file_existence",
    "details": { "path": "./README.md", "exists": true, "size": 1234, "content_hash": "abc123..." }
  }
}

Verify with expected hash

litatoli verify-file ./output.json --expected-hash 5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03

Returns "hash_match": true and exit code 0 if the SHA-256 matches, or "hash_match": false and exit code 1 if not.

Capture evidence to a log file

litatoli run --log-file evidence.jsonl --watch-dir ./src make test

This creates a chain-linked JSONL evidence file. Each entry is keyed-BLAKE3-signed and linked to the previous entry.

Gate a costly action

litatoli evaluate-hypothesis \
  --hypothesis "The code compiles" \
  --proposed-command "cargo build --release" \
  --log-file evidence.jsonl

If no prior related evidence (e.g., a passing cargo check) exists in the log, Litatoli blocks the action:

{
  "overall_ok": false,
  "specs": [{ "id": "evaluate-hypothesis", "ok": false, "errors": ["1. Run: cargo check\n2. Run: cargo test"] }],
  "metrics": {
    "action_blocked": true,
    "detected_pattern": "cargo-build-release",
    "cost_estimate_secs": 180,
    "required_proof_steps": ["cargo check", "cargo test", "cargo clippy"]
  }
}

CLI reference

Every command outputs a JSON report to stdout. Logs go to stderr.

Global flag: --log-level <LEVEL> (default: warn, options: trace, debug, info, warn, error)

Exit codes: 0 = verified/success, 1 = fabrication/mismatch/blocked, 2 = I/O or parse error.

run — Execute with evidence capture

litatoli run [OPTIONS] <CMD> [ARGS...]
Option Default Description
--cwd <PATH> . Working directory
--timeout <SECS> Kill after N seconds (SIGTERM, then SIGKILL after 2s grace)
--watch-dir <PATH> Snapshot directory before/after for FS diff
--log-file <PATH> Append to BLAKE3-signed JSONL evidence log

Captures: exit code, stdout, stderr, elapsed time, BLAKE3 signature, filesystem diff (if --watch-dir), new TCP connections (Linux).

attest — Sign a pre-captured result

echo '{"command":"echo hi","exit_code":0,"stdout":"hi\n","stderr":""}' | litatoli attest
Option Default Description
--log-file <PATH> Append to evidence log
--run-id <ID> Correlation id (ADR-0005) sealed into the entry's signed top-level run_id field, so the whole run is queryable with query --run-id

Reads JSON from stdin (max 100MB). Signs the result with keyed BLAKE3 without re-executing the command. Evidence type: command_attest.

attest-ot-run — Sign an OT simulation run

echo '{"simulator_image_hash":"abc...","agent_code_hash":"def...","modbus_blocks_count":3,"iec62443_sl_target":"SL-T 1","outcome":"success","sandbox_id":"a1b2c3d4...","inspection_interface":"tap-ot-a1b2c3d4","raw_log":null}' | \
  litatoli attest-ot-run \
    --simulator-image-hash "abc..." \
    --agent-code-hash "def..." \
    --modbus-blocks-count 3 \
    --iec62443-sl-target "SL-T 1"
Option Default Description
--simulator-image-hash <SHA256> required SHA-256 of the Ghost PLC Docker image (must match SandboxProfile.ghost_plc_image_hash)
--agent-code-hash <SHA256> required SHA-256 of the agent code under test
--modbus-blocks-count <N> required Number of Modbus writes blocked by the eBPF filter during the run
--iec62443-sl-target <LEVEL> required IEC 62443 Security Level target (e.g. "SL-T 1")
--log-file <PATH> Append to evidence log

Reads OtRunInput JSON from stdin (max 100MB). All four CLI arguments are authoritative — the handler cross-checks each against the corresponding stdin field and fails with exit code 1 (fabrication) on any mismatch. The signed OtRunOutcome record is built from the validated CLI values, cryptographically binding the evidence to the CLI invocation contract. Evidence type: ot_simulation_run.

attest-ai-inference — Seal an AI inference session

litatoli attest-ai-inference --bim-digest blake3:<hex> --model-digest blake3:<hex> \
  [--log-file <PATH>] [--sync-every <N>]  < records.jsonl

Reads AiInferenceRecord JSONL from stdin — one record per line — and appends a chained, signed ai_inference entry per record as it arrives. One long-lived process seals a whole session, so per-inference cost is an append, not a process spawn. Every record's bim_digest/model_digest must match the CLI arguments (a session is pinned to ONE build); any mismatch or parse error fails closed. --sync-every N fsyncs the log every N records (default 1 = every record durable before its ack; N>1 amortizes fsync cost but a crash may drop records since the last boundary).

verify-file — Check file existence and hash

litatoli verify-file <PATH> [--expected-hash <SHA256>]

Without --expected-hash: verifies file exists and reports its SHA-256. With --expected-hash: additionally verifies the hash matches.

verify-git-commit — Verify a commit exists

litatoli verify-git-commit <HASH> [--cwd <PATH>]

Runs git show --no-patch and returns commit details (author, date, message). Returns exit 1 (Fabrication) if the commit doesn't exist.

verify-git-push — Verify a commit reached a remote

litatoli verify-git-push <REMOTE> <BRANCH> <HASH> [--cwd <PATH>]

Runs git ls-remote and verifies the commit hash matches the remote branch head.

evaluate-message — Scan agent text for claims

litatoli evaluate-message <MESSAGE> [OPTIONS]
Option Default Description
--cwd <PATH> . Git repo root
--remote <NAME> Override remote for push verification
--branch <NAME> Override branch for push verification
--file-path <PATH> File to verify for creation claims

Detects success keywords (success, completed, pushed, committed, created, deployed, done, etc.) and routes to the appropriate verifier:

Claim type Detection Verification
Git push Message contains "push" + hex hash git ls-remote against remote
Git commit Message contains "commit" + hex hash git show for commit details
File creation Message contains file creation keywords File existence + SHA-256
Generic success Any other success keyword Returns requires_retry: true

evaluate-hypothesis — Gate costly actions

litatoli evaluate-hypothesis --hypothesis <TEXT> --proposed-command <CMD> [--log-file <PATH>] [--patterns <TOML>]

Blocks costly actions unless prior chain-validated evidence exists. Proof must be both semantically related and chain-validated — a passing cargo check unblocks cargo build, but an unrelated echo hello does not, and a tampered or reordered log entry is rejected.

Option Default Description
--log-file <PATH> JSONL evidence log to check for prior proof
--patterns <TOML> Custom costly action patterns (merged with built-in)

check-proof — Read-only proof query

litatoli check-proof --proposed-command <CMD> --log-file <PATH> [--patterns <TOML>]

Unlike evaluate-hypothesis (which blocks), check-proof is a pure query: it reports whether sufficient chain-validated proof exists for a proposed command, without blocking anything. This enables predictive workflows where an orchestrator checks proof availability before planning, so agents know upfront which lightweight steps to run first — zero wasted LLM roundtrips on reactive rejections.

{
  "overall_ok": true,
  "metrics": {
    "proof_found": true,
    "matching_entry_seq": 7,
    "detected_pattern": "cargo-build-release",
    "cost_estimate_secs": 180,
    "proof_keywords": ["cargo check", "cargo test", "cargo clippy"]
  }
}

When proof_found is false, the response includes required_proof_steps — the lightweight commands to run first.

query — Return one run by its correlation id

litatoli query --log-file ev.jsonl --run-id <id> [--json]

Returns every entry whose signed top-level run_id (ADR-0005) matches — a structured-field match, not a text search of the payload. Exits 1 when no entry carries the id. --json emits the matching entries as a JSON array instead of a summary. This command only SELECTS; pair it with verify-chain for authenticity.

list-patterns — Export gating rules

litatoli list-patterns

Outputs all costly action patterns as a JSON array. Enables orchestrators to discover gating rules at boot time and generate tool schemas with evidence pre-conditions.

[
  {
    "label": "maturin-develop",
    "substring": "maturin develop",
    "cost_estimate_secs": 90,
    "proof_steps": ["Run an isolated unit test...", "Confirm with cargo test..."],
    "proof_keywords": ["cargo test", "cargo check", "rustc"]
  }
]

Custom patterns (TOML)

Both evaluate-hypothesis and check-proof accept --patterns <file.toml> to load custom costly action patterns. Custom patterns are merged with built-in patterns; if a custom pattern has the same label as a built-in, it replaces the built-in.

[[patterns]]
label = "terraform-apply"
substring = "terraform apply"
cost_estimate_secs = 120
proof_steps = ["Run terraform plan first", "Validate with terraform validate"]
proof_keywords = ["terraform plan", "terraform validate"]

[[patterns]]
label = "npm-publish"
substring = "npm publish"
cost_estimate_secs = 30
proof_steps = ["npm test"]
proof_keywords = ["npm test", "npm run test"]

verify-db-state — Verify database query result

litatoli verify-db-state \
  --connection-string "postgres://user:pass@localhost/db" \
  --query "SELECT count(*) FROM users" \
  --expected-hash <SHA256>

Runs the query via psql (PostgreSQL) or sqlite3 (SQLite), hashes the raw output with SHA-256, and compares against the expected hash. Connection strings are redacted in output — credentials never appear in reports.

Backend Detection Tool
SQLite URL starts with sqlite:, or ends with .db/.sqlite/.sqlite3 sqlite3
PostgreSQL anything else psql -t -A -c

batch-verify — Parallel verification

echo '[{"type":"file","path":"./out.json"},{"type":"git-commit","hash":"abc1234","cwd":"."}]' | litatoli batch-verify
Option Default Description
--spec <PATH> stdin JSON file path (use - for explicit stdin)

Runs all specs in parallel via rayon. Supported spec types:

Type Required fields Optional fields
file path expected_hash
git-commit hash cwd (default .)
git-push remote, branch, hash cwd (default .)
message text cwd (default .)
db-state connection_string, query, expected_hash

export-attestations — in-toto / DSSE interop export

litatoli export-attestations --log-file evidence.jsonl --out attestations.jsonl [--dsse] [--sigstore-bundle] [--chain-summary]

Projects the evidence log into in-toto v1 Statements (JSONL, one per entry), optionally wrapped in DSSE envelopes (--dsse) signed with the configured Ed25519 key, or in Sigstore bundles (--sigstore-bundle) that cosign verify-blob-attestation --bundle reads directly. Plain DSSE is the portable default — any DSSE / in-toto tool reads it; the Sigstore bundle is a cosign-targeted wrapping of the same signed envelope.

Option Default Description
--log-file required Append-only JSONL evidence log to export
--out required Destination file (attestations, one per line)
--dsse off Wrap each Statement in a signed DSSE envelope (requires an Ed25519 key — see Key resolution)
--sigstore-bundle off Wrap each signed DSSE envelope in a Sigstore bundle (v0.3), read directly by cosign --bundle. Implies --dsse.
--chain-summary off Append one Statement covering the whole chain (file digest, head MAC, seq range, signer pubkeys, trailing checkpoint)

Mapping: subject[0] is the entry itself (unkeyed BLAKE3 digest of the exact raw log line — re-derivable by anyone holding the log, no MAC key needed); entries whose payload carries model_digest / bim_digest get extra model / bim subjects so attestations can be looked up by artifact digest; the predicate is the original entry verbatim (predicateType: https://litatoli.dev/attestations/evidence/v1).

cosign interop. Two automated tests in cli/tests/integration.rs cover it. test_export_attestations_dsse_signature_verifies recomputes the DSSE Pre-Authentication Encoding and verifies the Ed25519 signature with the exported public key on every run. test_cosign_binary_verifies_dsse_attestation runs the actual cosign binary against an exported attestation — gated on cosign being installed, so it runs wherever cosign is present (verified against cosign v3.1.3) and is skipped otherwise. Reproduce it by hand:

litatoli export-attestations --log-file ev.jsonl --out att.jsonl --sigstore-bundle
head -1 att.jsonl > attestation.sigstore.json  # first bundle (entry seq 1)
head -1 ev.jsonl | tr -d '\n' > line1          # the raw line it attests

cosign verify-blob-attestation --bundle attestation.sigstore.json --new-bundle-format \
  --key litatoli.pub --insecure-ignore-tlog \
  --type https://litatoli.dev/attestations/evidence/v1 \
  line1                                        # -> Verified OK

--sigstore-bundle emits the v0.3 bundle cosign reads with --bundle — the format cosign is standardising on, not the older --signature flag it has deprecated toward removal. --new-bundle-format is transitional: cosign is making that format the default, at which point the flag goes away. (litatoli.pub is the Ed25519 public key from export-pubkey, converted to PEM/SPKI; --insecure-ignore-tlog because these attestations are not uploaded to a public transparency log.)

Plain --dsse stays the portable default. A Sigstore bundle is a cosign-targeted wrapping: it couples to Sigstore's evolving schema, blanks the self-describing keyid (cosign matches by --key), and expects a transparency log litatoli does not use — while a plain DSSE envelope is read by any DSSE / in-toto tool. Use --sigstore-bundle for cosign, --dsse for everyone else.

The full on-disk format is specified in FORMAT.md — precisely enough to build an independent verifier with no litatoli code (a ~900-line Python reference lives at pimatika/tools/verify_bundle.py).

Scope: this is a presentation-layer projection for interop and auditor familiarity. A DSSE verifier checks that each attestation is signed by the expected key; it does not understand seq/prev_hash continuity, Merkle checkpoints or BIM cross-anchoring — chain integrity still requires verify-chain. The canonical JSONL format is unchanged. Export fails closed: any unparseable log line aborts the whole export (never a silent partial view).

sign-attestation — DSSE-sign an external in-toto Statement

litatoli sign-attestation --statement statement.json --out envelope.dsse.json

Wraps an existing in-toto v1 Statement built by any producer (e.g. nazelo-trust's runtime-enforcement attestation) in a DSSE envelope signed with the configured Ed25519 key. Fail-closed: refuses anything that is not a well-formed in-toto v1 Statement. (export-attestations projects this log into Statements; sign-attestation signs a Statement you already hold.)

secure-run — execution without a shell

litatoli secure-run <COMMAND> [OPTIONS]
Option Default Description
--cwd <PATH> . Working directory
--timeout <SECS> Kill after N seconds
--no-verify off Skip post-execution verification
--unsafe-shell off Pass raw string to /bin/sh -c (DANGEROUS)

What this is, and where it stops. The command is tokenized and executed directly — no shell interprets it, so there is nothing for a metacharacter to mean. That refuses shell injection; it does not make the command safe, and --unsafe-shell hands the raw string to /bin/sh -c and gives all of it back.

Commands are tokenized with a POSIX-compliant shell lexer. Shell metacharacters (;, |, &, `, $()) are treated as literal data, preventing injection attacks.

Post-execution verification is automatic for critical commands:

Command pattern Verification
git push Verifies commit reached remote via git ls-remote
git commit Verifies commit exists via git show
rm Exit code check
mv Exit code check
cp Exit code check

Git push parsing is token-aware — flags (--force, -u, --set-upstream, --force-with-lease) in any position are correctly skipped when extracting remote/branch targets.

The evidence chain, end to end

The commands above produce records. This section is how those records become something an auditor can act on.

keygen — Create the two keys

litatoli keygen

Sealing needs two secrets and this makes both:

Key Where What it does
Ed25519 LITATOLI_ED25519_KEY_FILE / ~/.config/litatoli/ed25519.key, or --out The public-key signature a third party verifies. This is what an auditor pins.
keyed BLAKE3 LITATOLI_KEY_FILE / ~/.config/litatoli/signing.key, or --mac-out The MAC that chains entries together. Never leaves your trust domain.

The MAC key is skipped when LITATOLI_SIGNING_KEY is already set — the env var wins at load, so writing a file that is never read would be a lie on disk. --force rotates. Nothing is ever auto-generated: a key that appears by itself has no rotation or distribution story.

Publish the Ed25519 public key. litatoli export-pubkey prints it.

checkpoint — Commit to everything so far

litatoli checkpoint --log-file evidence.jsonl

Appends a signed entry carrying the Merkle root of every entry before it. verify-chain validates checkpoints automatically, which catches in-place tampering below one. It is not an anti-backdating defence on its own — someone holding the signing key can re-sign a whole fabricated history, checkpoints included. Timestamp it (stamp) or witness it to fix it in time.

verify-chain — What an auditor runs

litatoli verify-chain --log-file evidence.jsonl \
    --expected-pubkey <64 hex chars> \
    --expected-head   <blake3_signature of the last entry>
Option Why
--expected-pubkey Required unless you pass --allow-unpinned. A log signed end to end by one attacker's key is perfectly self-consistent and passes every internal check.
--trust-anchor + --key-registry Pin a ROOT key instead of each leaf, so signer keys can rotate without re-pinning.
--expected-head The only thing that detects entries cut from the END. See "The completeness gap" below.
--allow-unpinned Self-consistency only. Proves nothing about who signed.

Read proves, not overall_ok. overall_ok is the same true whether you pinned anything or not; proves names what the pass actually established, and the command prints the log's head so you can pin it next time.

witness-submit — Anchor a head outside the log

litatoli checkpoint --log-file evidence.jsonl
litatoli witness-submit --log-file evidence.jsonl --witness https://witness.example/witness

Backends: file:<path> (local), http(s)://<url> (self-hosted or managed), or rekor: (the public Sigstore transparency log).

A witness is an append-only registry of heads that only ever moves forward per workload. That single rule is what makes a rolled-back or truncated log detectable: the cut version reports a head behind what the witness already recorded, and is refused.

Running a witness

Three commands, for the service side rather than the producer side:

# Receive a head over the wire, check monotonicity, sign and record it.
echo '<Head JSON>' | litatoli witness-record --witness file:/var/lib/litatoli/witness.jsonl

# Serve the latest head for a workload (raw WitnessedHead JSON).
litatoli witness-latest --witness file:/var/lib/litatoli/witness.jsonl --workload-id blake3:…

# Sign a head WITHOUT storing it — for a witness whose registry is a database.
echo '<Head JSON>' | litatoli witness-sign

witness-sign enforces nothing: monotonicity is a property of a registry and it has none. A caller that stores without checking first will have the witness sign a rollback.

Exit codes are distinct because a service maps them to HTTP: 1 = the evidence says no (non-monotonic submission, or no record for that workload), 2 = bad input or a broken backend.

litatoli-saas ships the HTTP service these back, over Postgres.

verify-ed25519 — Verify per-entry signatures (pinned)

litatoli verify-ed25519 --log-file ev.jsonl --expected-pubkey <64-hex>
# key rotation: pin the ROOT and authorize leaves via a signed registry
litatoli verify-ed25519 --log-file ev.jsonl --trust-anchor <root-64-hex> --key-registry registry.json

Verifies the Ed25519 signature on every entry that carries one. A trust anchor is REQUIRED by default: pin the trusted key with --expected-pubkey (or the LITATOLI_ED25519_PUBKEY env var). Without one, verification only proves each entry is self-consistent with whatever key it embeds — an attacker who rewrote the whole log with their own keypair would pass; pass --allow-unpinned to run that weaker self-consistency-only check explicitly. Unlike verify-chain, this catches per-line forgery only — it does not check seq/prev_hash continuity, so it will not detect an entry deleted or reordered within the log. With --trust-anchor + --key-registry, each entry's signing key must be authorized (role evidence) by a registry signed by that root and valid at the entry's timestamp — this is what allows key rotation without re-pinning.

stamp — RFC 3161 trusted timestamp

litatoli stamp --log-file ev.jsonl --seq <N> --tsa-ca ca.pem [--tsa <URL>]

Stamps entry <seq> with an RFC 3161 timestamp from a TSA (default http://timestamp.digicert.com; requires the curl binary). The token is persisted as a sidecar at <log_dir>/timestamps/<seq>.tsr. With --tsa-ca the response is FULLY validated — message imprint + nonce echo + CMS signature + TSA certificate chain, via openssl ts -verify — and a token that fails is rejected, never stored (requires openssl). Without --tsa-ca you must pass --allow-unverified-tsa (only the PKIStatus is checked), so an unvalidated stamp can't be mistaken for a verified RFC 3161 timestamp — validate the .tsr yourself later.

Key registry — rotate signers under one pinned root

The trust-anchor mode of verify-ed25519 / verify-chain reads a signed registry of authorized signer keys, so keys can rotate without re-pinning every leaf. The ROOT key signs the registry; auditors pin the root, not each key.

# Create: the root signs a list of {pubkey, role, not_before, not_after?, revoked_at?} read on stdin.
echo '[{"pubkey":"<64-hex>","role":"evidence","not_before":"2026-01-01T00:00:00Z"}]' \
  | litatoli registry-create --out registry.json

# Rotate a new key in (its own validity window), re-signed with the root:
litatoli registry-add-key --registry registry.json --pubkey <64-hex> \
  --role evidence --not-before 2026-06-01T00:00:00Z

# Revoke a key (sets revoked_at) and re-sign:
litatoli registry-revoke-key --registry registry.json --pubkey <64-hex> [--at <ISO-8601>]

# Auditor path — verify the registry against the pinned root (no secret needed):
litatoli registry-verify --registry registry.json --trust-anchor <root-64-hex>

registry-create resolves the root Ed25519 key the usual way (LITATOLI_ED25519_KEY / _KEY_FILE / ~/.config/litatoli/ed25519.key); the registry's root_pubkey is that key's own public half — pin THAT as the trust anchor.

Attributing findings to a clause

Provenance answers who wrote what, and what the tests said. It does not say which clause of IEC 62304 this satisfies. That is litatoli-scan, a companion package:

litatoli-scan ./src --standard iec62304

It is not installable from public PyPI yet — its two engines (lisaba for the standards registry, muundo for the code graph) are on no public index. See scan/README.md for the real install path — both are built from their repositories.

It scans a source tree against the standard's structural rules and seals every clause claim into a chain — including a claim that says "nothing found", since a clean scan is a result someone will want signed.

--log-file strict error handling

When --log-file is provided, evidence persistence is mandatory:

  • Log open failure → exit code 2 with structured error report
  • Log append failure → exit code 2 with structured error report

Evidence is never silently skipped. If the caller requests a log file, a write failure means the run is not trustworthy.

Evidence log format

Litatoli writes keyed-BLAKE3-signed, chain-linked JSONL (one entry per line):

{"seq":1,"timestamp":"…","evidence_type":"command_run","payload":{…},"blake3_signature":"a1b2…","key_id":"f03e…","algorithm":"blake3-keyed"}
{"seq":2,"prev_hash":"a1b2…","timestamp":"…","evidence_type":"git_push","payload":{…},"blake3_signature":"d4e5…","key_id":"f03e…","algorithm":"blake3-keyed"}

Entry fields

Field Type Description
seq u64 1-based consecutive sequence number
prev_hash string blake3_signature of the previous entry; omitted on the first entry (seq=1)
timestamp string ISO-8601 UTC
evidence_type string command_run, command_attest, git_push, git_commit, etc.
payload object Event-specific data
blake3_signature string 64-char hex keyed BLAKE3 MAC
key_id string First 16 hex chars of BLAKE3(key) — identifies which key signed
algorithm string Always blake3-keyed

Signing

Every entry is signed over a canonical signing payload: a compact JSON object with a fixed field order (a struct, not a map):

{"seq":…,"prev_hash":…,"timestamp":…,"evidence_type":…,"payload":…,
 "key_id":…,"algorithm":"blake3-keyed"[,"ed25519_alg":"ed25519",
 "ed25519_pubkey":…][,"run_id":…]}

prev_hash is always present here (JSON null on the first entry) even though it is omitted from the stored entry. The ed25519_* and run_id fields appear only when the entry carries them. payload is serialized with recursively sorted keys and compact separators. Both the keyed-BLAKE3 MAC and the optional Ed25519 signature cover exactly these bytes. The full rule is in FORMAT.md §2.

Chain integrity

Entries form a hash chain. On read, Litatoli verifies:

  1. Signature — the keyed BLAKE3 MAC matches the recomputed value
  2. Sequenceseq values are consecutive starting from 1
  3. Linkage — each entry's prev_hash equals the previous entry's signature

Any tampering, deletion, insertion, or reordering breaks the chain. Verification stops at the first invalid entry — all subsequent entries are rejected regardless of their content.

Key resolution

The 32-byte BLAKE3 MAC key is resolved from (in order):

  1. LITATOLI_SIGNING_KEY environment variable (hex-encoded, 64 chars)
  2. File at LITATOLI_KEY_FILE env var path (raw 32 bytes)
  3. ~/.config/litatoli/signing.key (raw 32 bytes, mode 0600) — read if present

There is no auto-generation: if none of these yields a key, signing and log-write operations fail with an error. LITATOLI_SIGNING_KEY (or a key file) must be configured — a silently auto-minted per-host MAC key has no rotation or distribution story. In production (ENVIRONMENT=production or staging) the CLI fails loudly at startup (exit code 2) unless at least one signing key is set — BLAKE3 (LITATOLI_SIGNING_KEY) or Ed25519 (LITATOLI_ED25519_KEY).

Every entry carries the keyed-BLAKE3 MAC — it is mandatory and chains the entries. When an Ed25519 key is configured, the entry also carries an Ed25519 signature over the same bytes: the non-repudiable, publicly verifiable one (its public key is embedded in every entry, so logs verify without any shared secret). With no Ed25519 key provisioned, a log is BLAKE3-only.

Hypothesis gating

Litatoli prevents agents from executing costly actions without evidence. Two modes are available:

  • Reactive (evaluate-hypothesis): Blocks the action and returns proof steps. Best as a safety net.
  • Predictive (check-proof): Read-only query — reports whether proof exists. Best for orchestrator planning: the agent learns what it needs before attempting the action, saving LLM roundtrips.

Costly action patterns (built-in)

Pattern Estimated cost Valid proof commands
cargo build --release 180s cargo check, cargo test, cargo clippy
cargo build 60s cargo check, cargo test, cargo clippy
cargo compile 60s cargo check, cargo test, cargo clippy
maturin develop 90s cargo test, cargo check, rustc
maturin build 120s cargo test, cargo check, rustc
make compile 60s make check, make lint, make test, cargo check
docker-compose up 45s docker-compose ps, docker ps, docker-compose config
docker-compose run --rm 60s docker-compose exec, docker exec, docker-compose ps
dropdb / DROP DATABASE 30s psql, SELECT, pg_dump
CREATE DATABASE 15s psql, SELECT, createdb

Proof requirements

Proof must satisfy all of: - Exists in the evidence log — a command_run or command_attest entry - Semantically related — the command contains a recognized proof keyword for the pattern - Successfulexit_code == 0 - Chain-validated — the entry is part of an unbroken chain (tampered/reordered entries don't count)

An unrelated passing command (e.g., echo hello) does not unblock cargo build.

Instead of letting agents hit the gate reactively, orchestrators should use the predictive pattern:

  1. At boot, call litatoli list-patterns to discover all gating rules
  2. Inject rules into tool schemas / system prompts so agents know upfront
  3. Before executing a costly command, call litatoli check-proof to verify evidence exists
  4. If proof_found: false, inject the required_proof_steps into the agent's plan — not as a rejection error

This avoids wasted LLM tokens on reactive block-retry cycles.

Library usage (Rust)

use std::path::Path;
use litatoli_core::{
    executor::run_command,
    git::verify_commit,
    evidence_log::AppendOnlyLog,
    hitl_triggers::evaluate_hypothesis,
    check_proof, list_patterns, load_patterns_from_file,
    secure_shell::SecureShell,
};

// Execute with evidence capture
let outcome = run_command("make", &["test".into()], Path::new("."), Some(60), Some(Path::new("./src")))?;
assert!(outcome.exit_code == 0);
println!("BLAKE3 signature: {}", outcome.blake3_signature.unwrap());

// Verify a git commit
let commit = verify_commit(Path::new("."), "abc1234")?;
println!("Author: {}, Date: {}", commit.author, commit.date);

// Append to evidence log (keyed BLAKE3, chain-linked)
let mut log = AppendOnlyLog::open(Path::new("evidence.jsonl"))?;
log.append_evidence("command_run", serde_json::to_value(&outcome)?)?;

// Command execution without a shell
let shell = SecureShell::new(Path::new("."));
let result = shell.run("git push origin main", true, Some(30), false);
println!("verified: {}, exit: {}", result.verified, result.exit_code);

// Predictive proof check (read-only, no blocking)
let proof = check_proof("cargo build --release", Path::new("evidence.jsonl"));
if !proof.proof_found {
    println!("Need proof first: {:?}", proof.required_proof_steps);
}

// Discover all gating rules
let patterns = list_patterns();
for p in &patterns {
    println!("{}: costs ~{}s, needs {:?}", p.label, p.cost_estimate_secs, p.proof_keywords);
}

// Load custom patterns from TOML
let custom = load_patterns_from_file(Path::new("my_patterns.toml"))?;

API reference

CommandOutcome (from executor::run_command)

CommandOutcome {
    command: String,                           // Reconstructed command string
    exit_code: i32,                            // 0 on success, -1 on internal error
    stdout: String,                            // Captured stdout
    stderr: String,                            // Captured stderr
    elapsed_ms: u64,                           // Execution time in milliseconds
    verified: bool,                            // true iff exit_code == 0
    evidence_type: String,                     // "command_run" or "command_attest"
    stdout_truncated: bool,                    // stdout was truncated to the output cap
    stderr_truncated: bool,                    // stderr was truncated to the output cap
    stdout_total_bytes: u64,                   // Bytes written to stdout before truncation
    stderr_total_bytes: u64,                   // Bytes written to stderr before truncation
    fs_diff: Option<FsDiff>,                   // Added/modified/removed files (if --watch-dir)
    network_activity: Option<NetworkActivity>, // New TCP connections (Linux only)
    blake3_signature: Option<String>,          // Keyed BLAKE3 MAC
}

SecureExecutionResult (from SecureShell::run)

SecureExecutionResult {
    command: String,                                    // Original command string
    exit_code: i32,                                     // -1 for parse/exec errors
    stdout: String,
    stderr: String,
    verified: bool,                                     // Independent verification success
    verification_result: Option<VerificationResult>,    // Structured evidence (if verified)
    verification_error: Option<String>,                 // Error description (if not verified)
    blake3_signature: Option<String>,                   // Keyed BLAKE3 MAC
}

FsDiff (from fs_monitor)

FsDiff {
    added: Vec<String>,      // Files created during execution
    modified: Vec<String>,   // Files changed (SHA-256 hash differs)
    removed: Vec<String>,    // Files deleted during execution
}

NetworkActivity (from net_monitor)

NetworkActivity {
    new_connections: Vec<NetworkConnection>,  // TCP connections opened during execution
    outbound_count: usize,                   // Total new outbound connections
}

NetworkConnection {
    local_addr: String,    // e.g., "192.168.1.5:54321"
    remote_addr: String,   // e.g., "140.82.121.4:443"
    state: String,         // TCP state: ESTABLISHED, SYN_SENT, etc.
}

HypothesisResult (from evaluate_hypothesis)

HypothesisResult {
    hypothesis: String,                    // Agent's stated belief
    proposed_command: String,              // The command being gated
    action_blocked: bool,                  // Should the action be blocked?
    required_proof_steps: Vec<String>,     // Steps to complete first
    retry_prompt: Option<String>,          // Numbered guidance for the agent
    cost_estimate_secs: Option<u64>,       // Estimated execution time saved
    detected_pattern: Option<String>,      // e.g., "cargo-build-release"
}

OtRunInput (stdin for attest-ot-run)

OtRunInput {
    simulator_image_hash: String,    // Must match --simulator-image-hash CLI arg
    agent_code_hash: String,         // Must match --agent-code-hash CLI arg
    modbus_blocks_count: u64,        // Must match --modbus-blocks-count CLI arg
    iec62443_sl_target: String,      // Must match --iec62443-sl-target CLI arg
    outcome: String,                 // "success" | "ebpf_blocked" | "crash"
    sandbox_id: String,              // Unique sandbox identifier
    inspection_interface: String,    // "lo" for V1 (OTSandboxBackend), or a
                                     // dynamically generated tap device name
                                     // for V2 (FirecrackerOTBackend) of the
                                     // form "tap-ot-<sandbox-id-prefix>"
                                     // (e.g. "tap-ot-a1b2c3d4"). There is
                                     // no fixed "tap0".
    raw_log: Option<String>,         // Optional captured test log
}

OtRunOutcome (evidence record for attest-ot-run)

OtRunOutcome {
    simulator_image_hash: String,    // From CLI arg (authoritative)
    agent_code_hash: String,         // From CLI arg (authoritative)
    modbus_blocks_count: u64,        // From CLI arg (authoritative)
    iec62443_sl_target: String,      // From CLI arg (authoritative)
    outcome: String,                 // "success" | "ebpf_blocked" | "crash"
    sandbox_id: String,              // Unique sandbox identifier
    inspection_interface: String,    // Network interface used for eBPF TC filter.
                                     // V1 = "lo"; V2 = per-sandbox dynamic tap
                                     // device "tap-ot-<sandbox-id-prefix>"
                                     // (created by FirecrackerOTBackend at
                                     // execute() time — never a fixed "tap0").
    raw_log: Option<String>,         // Optional captured test log
    timestamp: String,               // ISO 8601 timestamp
    evidence_type: String,           // Always "ot_simulation_run"
    overall_ok: bool,                // true iff outcome == "success"
}

Error types

enum EvidenceError {
    Fabrication(String),       // Agent's claim contradicts reality
    VerificationFailed(String), // Technical error prevented verification
    Io(std::io::Error),        // File/system I/O error
    Other(String),             // Generic error
}

Fabrication maps to exit code 1; all others map to exit code 2.

Configuration reference

Variable Required Description
LITATOLI_SIGNING_KEY Yes¹ 64-hex-char (32-byte) BLAKE3 MAC key
LITATOLI_KEY_FILE No Path to a raw 32-byte BLAKE3 key file (alternative to the env var)
LITATOLI_ED25519_KEY Recommended² 128-hex-char (64-byte) Ed25519 keypair — primary, non-repudiable signature
LITATOLI_ED25519_KEY_FILE No Path to a raw 64-byte Ed25519 key file (alternative to the env var)
ENVIRONMENT No production/staging enable the startup signing-key guard (fail fast, exit 2, if no key is set)

¹ Required unless a key file is present. There is no auto-generation — an unconfigured BLAKE3 key now errors instead of silently minting a per-host key.

² Not auto-generated. Create one with litatoli keygen (it also writes the BLAKE3 MAC key), or set it explicitly for a stable, shareable audit key (and to satisfy the production startup guard). With no Ed25519 key configured, logs are signed with the BLAKE3 MAC only.

Default BLAKE3 key location: ~/.config/litatoli/signing.key (raw 32 bytes, mode 0600) — read if present, but never created automatically.

Key ID: First 16 hex chars of BLAKE3(key). Included in every evidence entry so verifiers know which key to use.

Security model

What Litatoli verifies

Claim type How it's verified Trust basis
Command ran successfully Exit code + keyed BLAKE3 signature Process execution observed directly
File exists stat() + SHA-256 hash Filesystem read
Git commit exists git show --no-patch Local git object database
Git push reached remote git ls-remote Network query to remote
DB state matches psql/sqlite3 + SHA-256 of output Database query execution
Hypothesis has proof Chain-validated evidence log search Cryptographic chain integrity
OT simulation run CLI vs stdin cross-check of image hash, agent hash, Modbus blocks, SL target Mismatch = fabrication (exit 1); signed record uses CLI values

What Litatoli does NOT verify

  • Whether the command did what the agent intended (only exit code and side effects)
  • Whether git commits contain correct code (only that commits exist)
  • Whether the agent's interpretation of output is accurate
  • That a log is complete. See below — this one decides how much a green verify-chain is worth.

The completeness gap, and the two things that close it

Cut entries off the end of a log and every remaining check still passes. seq still runs 1..n, every prev_hash still matches its predecessor, every signature is still genuine — because they are genuine. Nothing was forged; something was removed, and removal leaves no trace inside the file. Checkpoints do not help, since the same cut takes them too.

Deletion in the middle is different: it breaks both seq and prev_hash, and verify-chain rejects it.

So the answer has to come from outside the log. Two ways, and you want at least one:

  1. Pin the head. Record the last entry's blake3_signature when you close a log — verify-chain prints it as head — and pass it back as --expected-head. Any cut then fails. Cheap, and enough when you control the record.
  2. Use a witness. Submit each checkpoint head to a freshness witness (witness-submit). A witness only ever moves a workload forward, so a rolled-back or truncated log reports a head behind what the witness already saw and is rejected. This is also what gives an auditor a signature from something other than the tool under review.

verify-chain says which of these you got. Read the proves field rather than overall_ok: without a pinned head it states plainly that completeness was not established, and it warns on stderr.

Shell injection prevention

The secure-run command tokenizes input with a POSIX-compliant lexer (shell_split). Shell metacharacters are treated as literal data:

# These are SAFE — semicolons, pipes, backticks are literal tokens:
litatoli secure-run "echo safe; rm -rf /"    # "safe;" is a literal argument
litatoli secure-run "echo data | cat /etc/passwd"  # "|" is a literal argument

# This is DANGEROUS — only use for trusted commands:
litatoli secure-run --unsafe-shell "echo hello | wc -w"  # passes to /bin/sh -c

Credential safety

  • DB connection strings are redacted in all output (credentials stripped)
  • Evidence log entries contain hashes, not raw file contents
  • Signing keys are stored with mode 0600 and never logged

Limitations

  • Linux-only for network monitoring/proc/net/tcp{,6} parsing requires Linux. On other platforms, network_activity is None
  • TCP connection tracking only — UDP, ICMP, and Unix sockets are not monitored
  • FS monitoring is snapshot-based--watch-dir takes SHA-256 snapshots before/after execution; rapid intermediate changes are not captured
  • Symlinks skipped in FS monitor — prevents directory traversal loops but means symlinked files are not tracked
  • Git verification requires local repoverify-git-commit and verify-git-push need the git repo on disk
  • DB verification shells out — requires psql or sqlite3 binaries on PATH
  • Hypothesis matching is substring-basedproposed_command is matched against patterns via case-insensitive substring match, not AST parsing
  • Evidence log is single-writer — concurrent appends from multiple processes to the same log file may corrupt the chain

Troubleshooting

Exit code 2 on --log-file

Both log-open and log-append failures produce exit code 2 with a structured error:

{
  "overall_ok": false,
  "specs": [{ "id": "error", "ok": false, "errors": ["Failed to open evidence log '/path': Permission denied"] }]
}

Fix: Check file permissions, parent directory existence, and disk space.

Fabrication detected: commit not found

The git commit hash doesn't exist in the local repository.

Fix: Ensure you're running in the correct git repo (--cwd), and that the commit has been fetched locally.

Could not parse remote/branch from command

The secure-run git push verifier couldn't extract remote and branch from the command.

Fix: Ensure the command includes both remote and branch: git push origin main. Flags in any position are supported.

Hypothesis blocks my command

The command matches a costly action pattern and no proof exists in the evidence log.

Fix: Run a lighter-weight proof command first and log it:

litatoli run --log-file evidence.jsonl cargo check
# Now this will be allowed:
litatoli evaluate-hypothesis --hypothesis "Code compiles" --proposed-command "cargo build" --log-file evidence.jsonl

Key provisioning

There is no auto-generation of the BLAKE3 MAC key — provision it explicitly:

# Generate and export a stable 32-byte BLAKE3 key (share it across machines)
export LITATOLI_SIGNING_KEY="$(python3 -c 'import os; print(os.urandom(32).hex())')"

# Optional: a stable Ed25519 keypair for non-repudiable, publicly verifiable
# signatures. It is NOT auto-generated — create one with `litatoli keygen`
# (or set LITATOLI_ED25519_KEY). With no Ed25519 key, logs carry only the
# BLAKE3 MAC.
export LITATOLI_ED25519_KEY="<128 hex chars>"

In production (ENVIRONMENT=production/staging) the CLI refuses to start (exit 2) unless at least one signing key is configured.

Integration with AI agent frameworks

Litatoli is a standalone binary — plug it into any agent framework:

import subprocess, json

def verify_agent_claim(message: str) -> dict:
    result = subprocess.run(
        ["litatoli", "evaluate-message", message],
        capture_output=True, text=True
    )
    return json.loads(result.stdout)

# In your agent loop:
agent_output = agent.run("Fix the bug and push to main")
verification = verify_agent_claim(agent_output)
if not verification["overall_ok"]:
    print("Agent claim not verified — rolling back")
Package Purpose
nazelo Sandbox engine — 4-tier isolation with BLAKE3 capability tokens and eBPF egress
nazelo-critical Multi-domain critical-systems sandbox plugin — Ghost PLC, Ghost Chip, serial, CAN, Firecracker backends covering ICS/OT, aerospace, automotive, medical, rail, maritime, energy, and identity
pimatika In-process governance enforcer — judges egress against a signed Build Intent Manifest. Writes its decisions to an Litatoli-format evidence log via LitatoliEvidenceWriter (depends on litatoli-core)
jagora-critical Multi-domain critical-systems orchestration — blueprints, TRIZ, framework compliance assessment for the 9 critical domains
Jagora AIDE Full orchestrator — code generation, compliance, TalaSala developer HUD

Integration with Jagora AIDE

Litatoli is the evidence engine used natively by Jagora AIDE:

  • EvidenceVerifier wraps the Litatoli binary for Python-native access
  • Agent contracts (pre/post checks) route through Litatoli for verification
  • TalaSala HUD displays verification status (Trust badge) in the Control Room
  • Compliance reports (EU AI Act, IEC 62443, ISO 27001) consume evidence entries
  • Hypothesis gate integrated into the autonomous orchestrator
  • SecureShell delegates to Litatoli's secure-run to execute without a shell
  • OT attestation (attest-ot-run) signs nazelo-critical sandbox outcomes for auditors

Jagora AIDE layers orchestration, compliance interpretation, developer HUD, and multi-agent governance on top.

Positioning

NaZelo          = "I don't trust the code"                          (sandboxed execution)
Litatoli      = "I don't trust what the agent claims"             (evidence verification)
nazelo-critical = "I don't trust the critical-systems environment"  (multi-domain industrial protocol isolation)
pimatika        = "I don't trust runtime drift from build intent"   (in-process governance enforcement, evidence emission)
Jagora        = "I control all of these, in a complete workflow"

Roadmap

  • Sigstore/in-totoexport-attestations ships in-toto v1 / DSSE export today, and witness-submit --witness rekor: anchors each checkpoint head to a public Sigstore transparency log for the freshness tier. Emitting SLSA provenance at seal time remains on the roadmap
  • Custom evidence backends — Pluggable EvidenceRecorder implementations for SIEM/compliance platforms
  • Async network monitoring — Real-time connection tracking instead of snapshot-based diffs
  • Extended DB support — Native client libraries instead of shelling out to psql/sqlite3

Plans & pricing

See litatoli.dev for current tiers, quotas, and features. litatoli-core and litatoli-cli are free OSS; the managed evidence aggregation service (litatoli-saas) is a paid tier.