Metadata-Version: 2.5
Name: waxseal
Version: 0.1.1
Summary: Tamper-evident, schema-evolution-safe audit hash chain for AI agent frameworks. Zero dependencies.
Project-URL: Homepage, https://github.com/cuongbphv/waxseal
Author: Bui Phan Viet Cuong
License-Expression: MIT
License-File: LICENSE
Keywords: ai-agents,audit,hash-chain,integrity,provenance,tamper-evident
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Libraries
Classifier: Typing :: Typed
Requires-Python: >=3.11
Provides-Extra: dev
Requires-Dist: hypothesis; extra == 'dev'
Requires-Dist: mypy; extra == 'dev'
Requires-Dist: psycopg[binary]>=3.1; extra == 'dev'
Requires-Dist: pytest-cov; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: ruff; extra == 'dev'
Description-Content-Type: text/markdown

# waxseal

**English** | [Tiếng Việt](README.vi.md) | [中文](README.zh.md)

**Tamper-evident, schema-evolution-safe audit hash chain for AI agent frameworks.**
Zero dependencies. MIT. Python ≥ 3.11.

waxseal gives your agent a cryptographic audit trail: every action is appended to a
SHA-256 hash chain, so any edit, deletion, insertion, or reordering of history is
detected. Schema evolution does not trigger false tampering alarms: old rows verify
under the fingerprint they were written with.

## Why another audit log?

Hash-chained logs break in practice for a boring reason: **the schema changes**. Two
real-world incidents shaped this library:

- A production system widened its hashed field set without a version identity — every
  historical row failed verification. A mass false tampering alarm.
- An agent-memory tool (beads v1.2.2, 08/2026) accidentally shipped a schema migration;
  the reverted binary hit *"schema version mismatch: database is at v65, binary knows up
  to v53"* and hard-failed. The only escape hatch disabled safety entirely.

Both are the same failure class: *ordinal version identity + unknown version treated as
an error*. waxseal makes that class unrepresentable:

1. **Envelope design** — the chain hashes only a fixed header
   (`seq, ts, hash_version, payload_type, payload_hash, prev_hash`). Your payload is
   arbitrary bytes; changing its schema never touches the chain.
2. **Automatic schema fingerprints** — `hash_version` is the SHA-256 of a canonical
   descriptor of the header schema. Widening the field set *cannot* keep the old
   identity; old rows always verify under their own fingerprint.
3. **Unknown fingerprint → "unverifiable by name"** — never "tampered", never a crash
   (the RFC 6962 principle: unrecognized types are opaque, not errors). Rollbacks
   degrade gracefully.

## Comparison with other hash-chain approaches

Every hash-chain library detects a flipped byte. These are the things the others
don't do (survey of Python audit-log libraries, August 2026 — see [DESIGN.md](DESIGN.md)
for the literature behind each choice):

|  | waxseal | typical audit-chain libs | DIY hash chain |
|---|---|---|---|
| Schema evolution without false tampering alarms (automatic fingerprints) | ✅ | ❌ manual version strings, or none | ❌ |
| Version rollback degrades gracefully (unverifiable ≠ tampered, exit 2 ≠ exit 1) | ✅ | ❌ unknown version = error | ❌ |
| Completeness reported separately: `dropped_writes`, `None` ≠ `0` | ✅ | ❌ chain-ok implies all-ok | ❌ |
| Fork-proof concurrent appends, with the mechanism documented **per backend** and a falsifiability-tested lock | ✅ | varies, usually single-writer assumed | ❌ |
| Byte-level SPEC (freeze planned for v1) + golden test vectors → portable to Go/Rust/TS | ✅ | ❌ format = whatever the code does | ❌ |
| Zero runtime dependencies (S3/Postgres clients are injected, never imported) | ✅ | often pulls crypto/serialization stacks | ✅ |
| Redact-before-hash (secrets never reach disk, hash commits to redacted bytes) | ✅ | sometimes | ❌ |
| Built-in external anchoring hook (`waxseal head`) against suffix-rewrite/truncation | ✅ | ❌ | ❌ |
| Forward-secure seals (key-evolving HMAC, stdlib only) + injected Ed25519 signatures | ✅ | ❌ | ❌ |

The first two rows are the failure class from the incidents above; see
[DESIGN.md](DESIGN.md) for the literature behind each row.

## How it works

**Data flow — every append:**

```mermaid
flowchart LR
    A["your agent<br/>append(payload)"] --> R["Redactor<br/>secrets → ***REDACTED***"]
    R --> C["canonical bytes<br/>payload_hash = sha256"]
    C --> H["EntryHeader built under<br/>the backend's lock<br/>(seq, prev_hash from tail)"]
    H --> EH["entry_hash =<br/>sha256(framed header)"]
    EH --> B[("backend<br/>JSONL · SQLite · Postgres · S3 · memory")]
    EH --> S["attestation sidecar<br/>fs-HMAC seal / Ed25519 signature"]
```

**The chain — why any edit is caught:**

```mermaid
flowchart LR
    G["genesis<br/>prev_hash = 000…0"] --> E0["entry 0<br/>entry_hash₀"]
    E0 -- "prev_hash = entry_hash₀" --> E1["entry 1<br/>entry_hash₁"]
    E1 -- "prev_hash = entry_hash₁" --> E2["entry 2<br/>entry_hash₂"]
    E2 -. "waxseal head → anchor externally<br/>(OpenTimestamps / RFC 3161 / git)" .-> X["external<br/>trust domain"]
```

**Verification — every outcome is distinct, unknown is never tampered:**

```mermaid
flowchart TD
    V["waxseal verify"] --> Q1{"seq contiguous?"}
    Q1 -- "no" --> X1["BROKEN: seq_gap → exit 1"]
    Q1 -- "yes" --> Q2{"prev_hash links?"}
    Q2 -- "no" --> X2["BROKEN: prev_hash_mismatch → exit 1"]
    Q2 -- "yes" --> Q3{"fingerprint known?"}
    Q3 -- "no" --> U["unverifiable by name → exit 2<br/>NOT tampering (rollback-safe)"]
    Q3 -- "yes" --> Q4{"entry_hash & payload_hash match?"}
    Q4 -- "no" --> X3["BROKEN → exit 1"]
    Q4 -- "yes" --> OK["ok → exit 0"]
```

## Install

```bash
pip install waxseal
```

Until the first PyPI release: `pip install git+https://github.com/cuongbphv/waxseal`

## Usage

```python
from waxseal import AuditLog

log = AuditLog.open("~/.myagent/audit/trail.jsonl")   # or trail.db for SQLite

log.append(
    payload={"tool": "bash", "command": "ls -la", "exit_code": 0},
    payload_type="application/vnd.myagent.toolcall+json",
)

result = log.verify()
# VerifyResult(ok=True, checked=1, broken_seq=None, reason=None,
#              unverifiable=(), dropped_writes=0)
```

Redact secrets **before** they are hashed and stored:

```python
from waxseal.adapters.redactors import RegexRedactor

log = AuditLog.open("trail.jsonl", redactor=RegexRedactor())
log.append(payload={"cmd": "curl -H 'Authorization: Bearer sk-...'"},
           payload_type="application/vnd.myagent.toolcall+json")
# cleartext never reaches disk; the hash commits to the redacted payload
```

CLI:

```bash
waxseal verify trail.jsonl   # exit 0 intact / 1 broken / 2 unverifiable present / 3 no such trail
waxseal tail trail.jsonl -n 20
waxseal inspect trail.jsonl
waxseal head trail.jsonl     # print the chain head (seq + entry_hash) for anchoring
```

## Storage backends

Every backend enforces the same rule: read-tail + append is one critical section, so
concurrent writers can never fork the chain.

| Backend | Module | Serialization mechanism | Extra deps |
|---|---|---|---|
| JSONL file | `waxseal.adapters.jsonl` | cross-platform file lock | none |
| SQLite | `waxseal.adapters.sqlite` | `BEGIN IMMEDIATE` + `PRIMARY KEY(seq)` | none |
| In-memory | `waxseal.adapters.memory` | mutex | none |
| Amazon S3 | `waxseal.adapters.s3` | conditional PUT (`IfNoneMatch: *`) | inject your boto3 client |
| PostgreSQL | `waxseal.adapters.postgres` | `pg_advisory_xact_lock` + `PRIMARY KEY(seq)` | inject your psycopg connection |

```python
# S3 — the client is injected; waxseal itself stays dependency-free
import boto3
from waxseal import AuditLog
from waxseal.adapters.s3 import S3Backend

backend = S3Backend(boto3.client("s3"), bucket="my-audit", prefix="agent-1")
log = AuditLog(backend)

# PostgreSQL — same pattern with a connection factory
import psycopg
from waxseal.adapters.postgres import PostgresBackend

log = AuditLog(PostgresBackend(lambda: psycopg.connect("postgresql://...")))
```

> Note on Kafka: compacted topics delete old records (tombstones), so they are **not**
> append-only — do not use them as a tamper-evidence store.

## Metadata sources

Beyond agent actions, chain any file/document history:

```python
from waxseal.sources.files import record_file, current_matches_last

record_file(log, "SPEC.md", doc_id="spec")          # snapshot content hash into the chain
current_matches_last(log, "SPEC.md", doc_id="spec")  # True / False / None (never recorded)
```

## Signatures & forward-secure seals

A keyless hash chain can be recomputed by anyone with write access. The attestation
layer closes that gap — without adding a single dependency:

**Forward-secure seals (stdlib HMAC, Bellare–Yee / Schneier–Kelsey construction):**
the seal key evolves one-way per entry (`A_{j+1} = SHA-256(A_j)`) and the old key is
discarded, so an attacker who compromises the machine at epoch *t* cannot forge or
re-seal anything written before *t* — a consistently rewritten suffix now FAILS
verification instead of passing:

```mermaid
sequenceDiagram
    participant W as writer
    participant K as sealkey (0600, atomic replace)
    participant S as .attest sidecar
    W->>K: read A_j
    W->>S: seal_j = HMAC-SHA256(A_j, entry_hash_j)
    W->>K: A_j+1 = SHA-256(A_j) — A_j is gone
    Note over K,S: compromise at epoch t ⇒ seals < t unforgeable
```

```python
from waxseal import AuditLog
from waxseal.adapters.attest import FileAttestor
from waxseal.domain.sealing import generate_key

k0 = generate_key()                      # escrow A_0 with your verifier, off this machine
log = AuditLog.open("trail.jsonl",
                    attestor=FileAttestor("trail.jsonl", initial_key=k0))
log.append(payload={...}, payload_type="application/vnd.myagent.toolcall+json")

log.verify_attestations(initial_key=k0)  # AttestResult(ok=True, checked=1, ...)
```

**Real digital signatures (Ed25519 etc.)** — the signer is injected, waxseal never
imports a crypto library:

```python
# any object with .algorithm, .key_id, .sign(bytes) -> bytes
log = AuditLog.open("trail.jsonl",
                    attestor=FileAttestor("trail.jsonl", signer=my_ed25519_signer))
log.verify_attestations(verifier=my_ed25519_verifier)
```

Attestations live in a `.attest` sidecar (no backend schema changes; old logs stay
readable), and an attestation scheme the verifier doesn't know is reported
unverifiable-by-name — the same never-cry-wolf rule the chain itself follows.
Verification also bakes in the lessons from the systemd-journald FSS CVEs
(2023-31437/38/39): seals are bound to their position in both directions,
cross-checked against hashes recomputed from the trail, and **tail truncation of
trail + sidecar together is detected** — the keyfile epoch is one-way and cannot be
rolled back.
Limits: Python cannot zeroize memory, and entries written *after* compromise
are attacker-controlled under any scheme — see [DESIGN.md](DESIGN.md) §6.

## Integrations

Audit hooks for seven agent frameworks and coding tools. Each one is
verified against the target's current hook contract (version noted in its README),
records dispatch *before* execution, redacts secrets before hashing, clips huge
outputs visibly, and **can never block or veto the host's work** — every failure
degrades to a labelled, counted dropped write.

Everything ships in the wheel — no source checkout, no file copying:

```bash
pip install waxseal
waxseal install hermes        # or claude-code / codex / cursor / hermes-gateway
```

`install` writes thin shims into the host's config directory (importing
`waxseal.integrations.*`, so `pip install -U waxseal` upgrades hook behavior in
place) and prints any settings snippet the host still needs. The LangChain,
CrewAI, and OpenAI Agents integrations need no install step at all — import
them directly, e.g. `from waxseal.integrations.langchain import
WaxsealCallbackHandler`.

| Target | Mechanism | Directory |
|---|---|---|
| Claude Code | hooks (`PreToolUse` / `PostToolUse` / `UserPromptSubmit`) | [integrations/claude-code/](integrations/claude-code/) |
| Codex CLI | lifecycle hooks (`hooks.json`, ≥ 0.149.0) | [integrations/codex/](integrations/codex/) |
| Cursor | Agent Hooks (`.cursor/hooks.json`) | [integrations/cursor/](integrations/cursor/) |
| LangChain / LangGraph | `BaseCallbackHandler` | [integrations/langchain/](integrations/langchain/) |
| CrewAI | event listener (`crewai.events`) | [integrations/crewai/](integrations/crewai/) |
| OpenAI Agents SDK | `RunHooks` | [integrations/openai-agents/](integrations/openai-agents/) |
| hermes-agent | plugin + gateway hook | [integrations/hermes/](integrations/hermes/) |

Scope note for the coding tools: these hooks give you a parallel,
tamper-evident, **secret-free** record of every action. They do not (and cannot)
rewrite the tool's own transcript files — if a key lands there, rotate it; the
waxseal trail is the copy you can keep, share, and verify.

## Guarantees and non-guarantees

- Detects: edited entries, deleted entries (seq gap), inserted/reordered entries
  (prev-hash break), payload substitution.
- **Chain integrity ≠ trail completeness**: a write dropped before it reaches storage
  leaves no gap. `dropped_writes` reports this separately; `None` means *not measured* —
  never conflated with `0`.
- Concurrent writers cannot fork the chain (see backends table).
- waxseal is tamper-*evident*, not tamper-*proof*: an attacker with write access can
  rewrite the whole suffix of a chain. Use `waxseal head` to anchor the head hash in
  an external trust domain — an OpenTimestamps proof, an RFC 3161 timestamp, or a git
  commit pushed to a remote — to bound that attack.

## Spec & design

- [SPEC.md](SPEC.md) — byte-level format (lp64v1 encoding, PAE-style framing,
  fingerprint construction; freeze planned for v1) with golden test vectors — portable
  to any language.
- [DESIGN.md](DESIGN.md) — algorithm choices and the academic literature behind them.

## License

MIT
