Skip to main content
Glama
README.md
# Pentimento

[![ci](https://github.com/RedRio-bit/pentimento/actions/workflows/ci.yml/badge.svg)](https://github.com/RedRio-bit/pentimento/actions/workflows/ci.yml)

**An open, local, single-file ledger for AI agent actions — with undo.**

> *Ogni azione dichiara la propria inversa.*
> *Every action declares its inverse.*

In painting, a *pentimento* is the visible trace of an earlier choice beneath the surface — proof that the artist changed their mind, preserved in the work itself. Pentimento is that, for AI agents: a **flight recorder** for everything an agent does on your machine, written as one plain, append-only `.pnt` file per session. Every action carries its timestamp, its target, and its **declared inverse**.

Not a service. Not a SaaS. Not a framework. A file format, a tiny CLI, and an MCP server. The SQLite move, not the platform move.

## The 30-second demo

```console
$ # an agent (via the pentimento-fs MCP server) deletes a folder…
$ pentimento log
   4  09:14:02  fs.delete  reversible  done  src/old

$ pentimento undo --last
undone action #4 (fs.delete src/old)

$ ls src/old        # the folder is back, byte for byte
a.txt  sub/
```

That is the entire pitch.

**Want to see it end to end?** [`examples/refactor_rescue/`](examples/refactor_rescue/) plays an agent doing a refactor through the real MCP server, then rescues what it broke — undo restoring a deleted tree, undo *refusing* to trample your edits, and a risky deploy parked as a draft. Run it: `python examples/refactor_rescue/demo.py`.

## How it works

- **One session = one file.** `.pentimento/sessions/<date>_<id>.pnt`, JSON Lines, append-only. An undo never rewrites history — it is a new event referencing the old one. The correction stays visible beneath the surface, like in the painting.
- **Record before effect.** The action event is durable on disk *before* the action touches the world. A flight recorder that loses exactly the crashes is not a flight recorder.
- **Snapshots are content-addressed.** Prior state goes into `.pentimento/objects/` keyed by SHA-256, git-style; identical content is stored once. Directories are snapshotted as tree manifests.
- **Undo never tramples.** Reverting requires proof that the world still matches the state the action left behind; otherwise the undo is refused — and the refusal is recorded too.
- **Reversibility is declared, never assumed.** Every action carries its class — `reversible`, `compensable`, `irreversible` — fixed by the spec, so that policy becomes possible.

Read the full format in [PENTIMENTO_SPEC.md](PENTIMENTO_SPEC.md).

## Install

```console
$ pip install .
```

Zero runtime dependencies — Python ≥ 3.10 standard library only.

## The CLI

```console
$ pentimento log              # timeline of the current session, with derived states
$ pentimento diff 7           # before/after (or evidence) of one action
$ pentimento undo 7           # revert one reversible action (post-state verified first)
$ pentimento undo --last      # revert the most recent revertible action
$ pentimento compensate 9     # present the declared compensation, run it on confirmation
$ pentimento approve 11       # present a drafted action, execute it on confirmation
$ pentimento discard 11       # kill a draft, visibly and terminally
$ pentimento status           # sessions, ledger size, snapshot store size
$ pentimento verify           # check the optional per-line hash chain, if present
$ pentimento gc [--dry-run]   # remove blobs no session on disk references
```

**Undo executes, compensation proposes.** Exact inverses run mechanically; a compensation — closing the ticket an agent created, deleting the resource it provisioned — is shown to you, command included, and runs only when you confirm. Either way, it lands in the ledger.

**And a draft proposes before the act.** An agent can journal a shell command or an HTTP request as a *draft*: fully on the record, not executed. `pentimento approve` runs it after you've seen it; `pentimento discard` kills it — and the discarded stroke stays visible in the ledger, a pentimento in the original sense.

## The `pentimento-fs` MCP server

Point any MCP client — Claude Code, Claude Desktop, Cursor — at `pentimento-fs` and every file operation it performs is journaled, snapshotted, and undoable, **with zero changes to the agent**.

```json
{
  "mcpServers": {
    "pentimento-fs": {
      "command": "pentimento-fs",
      "args": ["--root", "/path/to/your/project"]
    }
  }
}
```

Then tell the agent to prefer the journaled tools — one line in your `CLAUDE.md` (or equivalent):

> For file writes, deletions and moves in this project, use the `pentimento-fs` tools instead of native file access.

The server exposes eight tools. Four journaled filesystem tools — `write`, `delete`, `move`, `mkdir` — where each response names the action id, so the human always knows the undo handle: `journaled as action #7 (reversible: pentimento undo 7)`. Two executable outbound tools — `exec` and `http` — that run a command or a request *through* the journal, storing exit codes, output and response bodies as content-addressed evidence; both accept `draft: true` to journal without executing. And two for everything else: `record` journals an external action **before** it happens (compensable ones declare their compensation up front; irreversible ones are flagged for human approval first), and `report` appends the outcome afterwards.

### A note on secrets in evidence

`exec` and `http` store commands, headers, bodies, and outputs verbatim. That evidence is **local** — it never leaves your machine — but it is **not redacted by default**, so it can capture tokens, `Authorization` headers, or passwords that appear in arguments or output. Opt in to redaction and those values are stripped *before* they are hashed and stored, so the secret never reaches the ledger or the blob store (spec §14):

```console
$ pentimento-fs --redact                       # strip Authorization, Cookie, … headers
$ pentimento-fs --redact-pattern 'sk-[A-Za-z0-9]+'   # and any matching pattern
```

Redaction is visible (`pentimento diff` marks it), it applies to immediately-executed actions and to all evidence, and — because a draft must stay runnable until you approve it — a draft's own command and headers are stored verbatim.

## Running the tests

```console
$ PYTHONPATH=src python3 -m unittest discover -s tests
```

## Durability, safety, integrity

The reference implementation aims to match the spec's strongest promise under abrupt failure:

- **Crash durability.** Snapshot blobs, the files an action writes or restores, and every ledger append are `fsync`ed and atomically renamed, and their directory entry is `fsync`ed too — so a `started` event and its snapshot reach stable storage *before* the effect runs (spec §4).
- **Symlinks are refused before they are followed** — at every path component, not just the final target — so no link ever redirects where an action lands (spec §17).
- **Serialized appends.** A session may have more than one writer (an agent, plus you running `pentimento undo`); appends take a brief advisory lock and the event id is assigned under it, so ids never collide and lines never interleave (spec §4).
- **Optional integrity chain.** Opt in with `pentimento-fs --hash-chain` and each line hashes the previous one; `pentimento verify` reports the first break. It catches corruption and naive edits — not a determined actor with write access (spec §15).
- **No SSRF footgun by default.** Because `http` *performs* the request, it refuses targets with no legitimate HTTP use — link-local (`169.254.169.254` cloud metadata), multicast, reserved — while leaving loopback and private hosts reachable for local dev. `pentimento-fs --block-private` hardens further by also refusing loopback/private ranges (spec §11). It's a best-effort send-time check, not DNS-rebinding-proof.
- **Tunable timeouts.** `exec` and `http` default to 600s and 60s; override with `pentimento-fs --exec-timeout` / `--http-timeout` so a hung command can't pin a session forever.

## Status

Spec v0.3 (draft). The registry covers reversible filesystem actions and the executable outbound pair (`shell.exec`, `http.request` — irreversible unless a compensation is declared); everything else is journaled through `x.` extension kinds as compensable or irreversible — never anything rosier. Scope discipline is a feature.

*Draft v0.3 — written to be argued with. Issues and pull requests welcome.*

## License

MIT — spec and reference implementation. *A Red Rio Lab open spec.*