Skip to main content

Tombstone

delete() is a lie. Tombstone tracks where a data subject's data actually went — chunks, embeddings in several indexes, caches, training examples, fine-tuned adapter weights — erases it everywhere, and hands back a receipt that says exactly what was proven gone, at which level, and what was not.

It sits beside an existing LangChain / vector-store / fine-tuning pipeline. It selects nothing by similarity: erasure is a reachability query over a lineage graph captured at ingest. It never claims a verification level it did not achieve, and a receipt with no OUT_OF_SCOPE entries does not exist.

Start with what beats it: Attacks that work against Tombstone — measured, with rates, including the layer nobody can erase.

The lineage graph: source, chunk, embed, cache, train, adapter and memory artifacts, and the four verification levels with what each proves and what it does not.

Demo 1 — what a native delete() reaches

A RAG app over the benchmark corpus with two indexes, a semantic cache, a fine-tune dataset and a LoRA adapter. The operator deletes the way everyone deletes — vectorstore.delete(ids=[...]) on both indexes, source rows dropped — and believes the deletion is complete.

$ tombstone verify --subject S-0042 --after-native-delete
after native delete()
subject: hmac:f675…1d04   (raw id never logged)
artifacts descending from this subject: 23

  source  ×3 docs                   HIDDEN     logical PASS, physical FAIL
                                               bytes for 3/3 ids located in docs.sqlite
  chunk   ×4 docs                   HIDDEN     logical PASS, physical FAIL
                                               bytes for 4/4 ids located in docs.sqlite
  embed   ×4 chroma:kb-v2           HIDDEN     logical PASS, physical FAIL
                                               bytes for 4/4 ids located in chroma.sqlite3 + data_level0.bin
  embed   ×4 pgvector:kb-v1         HIDDEN     logical PASS, physical FAIL
                                               bytes for 4/4 ids located in documents_embedding_hnsw + pg_toast.pg_toast_17061
  cache   ×1 exact-cache            PRESENT    cached answer still returned
  cache   ×1 semantic-cache         PRESENT    cached answer still returned
  train   ×4 ft-dataset             PRESENT    still retrievable by id
  adapter ×2 lora/support-v3        GONE       no extraction prompts

verdict: NOT ERASED.  21/23 artifacts still hold recoverable content.
exit 2

The deletion was complete at exactly one layer. The full three demos, including the receipt that refuses to say "complete", are in docs/demo.md.

Headline numbers

Every number below is produced by a committed command from a committed JSON file (bench/results/). RESULTS.md is generated, never hand-edited.

  1. After a native delete(), 92.0%-100.0% of a subject's vectors are still physically recoverable from the index files, depending on the backend (chroma 99.8%, faiss 92.0%, qdrant 100.0%, pgvector 94.9%; 200 subjects each) — while every one of them is logically gone (100.0% exclusion). After tombstone erase, 0.0% of the subjects' own records remain and 0.0% of their vectors have bytes still attributable to them. A further 0.0%-8.0% have bytes that are byte-identical to records belonging to other subjects (shared boilerplate); a byte scan cannot tell those two copies apart, so they are reported separately and never counted as the erased record's residue. Source: bench/results/residue-latest.json, command uv run python bench/residue/run_residue.py --all.
  2. Exact shard unlearning vs approximate on Qwen/Qwen2.5-0.5B (20 subjects): before, canary extraction 17/20, MIA AUC 1.00 [1.00,1.00], held-out perplexity 298.7; exact shard retrain: canary extraction 0/20, MIA AUC 0.41 [0.23,0.61], held-out perplexity 231.0; NPO: canary extraction 0/20, MIA AUC 0.69 [0.51,0.84], held-out perplexity 82.4; gradient difference: canary extraction 5/20, MIA AUC 0.91 [0.76,1.00], held-out perplexity 103.5. Source: bench/results/unlearn-latest.json, command uv run python bench/unlearn/run_unlearn.py --all.
  3. The layer nobody can erase: after a full Tombstone erasure, an attacker estimating "was this subject ever here?" from retrieval-context drift (Ghost Echoes protocol) reaches paired-comparison accuracy 70.0% at budget 5, 72.5% at budget 10, 65.0% at budget 20, 70.0% at budget 40. Tombstone measures and reports this; it does not fix it. Source: bench/results/attacks-latest.json.

Known limitations — read these first

  • Tombstone can only erase what it saw arrive. Data ingested before capture was enabled is a lineage gap; the tool refuses to call it erased (attacks, 7.1).
  • Third-party mentions are not erased. A subject named inside another subject's document is listed for human review, not deleted, because deleting it would be a different violation (7.4).
  • Semantic residue is not erasable at the application layer. Correct deletion in a proximity-graph index leaves measurable retrieval drift, and rebuilds do not remove it (Ghost Echoes, arXiv 2608.20352). Tombstone measures and reports it; it does not claim to fix it (7.5).
  • Approximate unlearning is approximate. NPO and gradient-difference leave residual extractability and can be reversed by light continued training. Exact shard retraining avoids this only if the dataset was sharded by subject before training (7.6).
  • Managed databases cannot be physically verified without maintenance rights. The receipt says UNVERIFIED-managed, and that is the correct answer.
  • Backups, snapshots, replicas, WAL and provider-side logs are out of scope and always listed as such (7.7).
  • A receipt checked against its own key proves it was not altered, not who wrote it. The signing key travels inside the receipt, so verification without --public-key is integrity only; pass the operator's .tombstone/keys/public.pem (and --ledger) to make it a statement about origin. The verifier says which of the two you got (threat model).
  • A receipt is a record, not a legal instrument. It states what was done and checked. Whether that satisfies a regulator is a question for a lawyer, and the tool says so in its output.
  • Derived content the app does not stamp is invisible (7.2). Single scope per erasure. Four vector backends, two caches, one training framework, small models only in the benchmark.

The mechanism, in one paragraph

Deletion is a graph problem, not a search problem. stamp() attaches a subject and a source to every document at ingest; capture hooks on the vector-store wrapper, the caches and the dataset builder propagate that to every derived artifact as nodes and edges in a small append-only SQLite graph. trace() is a pure reachability query over a snapshot of that graph — same inputs, byte-identical output, forever. erase is a journaled, resumable saga over the traced set: write a tombstone marker that every retrieval path honours immediately, then physically reclaim per store, then probe each artifact at the strongest level the store permits — logical (not retrievable), physical (bytes not in storage), semantic (drift measured against a same-cluster control), model (canaries not extractable, membership inference at chance). The receipt is Ed25519-signed, hash-chained, replayable from the journal, and checkable by an independent verifier with no access to the lineage database.

Install

uv tool install tombstone-erase            # CLI: tombstone
uv pip install 'tombstone-erase[chroma,faiss,qdrant,pgvector,langchain]'   # store adapters
uv pip install 'tombstone-erase[train]'    # the model leg (torch, transformers, peft)
uv pip install 'tombstone-erase[mcp]'      # the MCP server

pip install works the same way. To run against unreleased main instead:

uv tool install git+https://github.com/Poojan6216/tombstone

The core install pulls only pydantic, cryptography and pyyaml — every backend, the training leg and the MCP server are extras, so import tombstone stays fast and drags in nothing heavy.

Ten-minute path (scripted in tests/test_first_use.py):

tombstone init                                   # detects LangChain, Chroma/FAISS/Qdrant dirs, PG_DSN, adapters; writes tombstone.yaml
from tombstone.integrations.langchain import TombstoneVectorStore
from tombstone.lineage.stamp import stamp

vs = TombstoneVectorStore.from_config("chroma:kb-v2", embeddings)   # wrap the store in one line
docs = stamp(docs, subject_id="S-0417", source_id="crm/417.pdf", scope="default", pepper=pepper)
index(docs, record_manager, vs, cleanup="incremental", source_id_key="source")  # stock LangChain

Then, when a deletion request arrives:

tombstone forget S-0417 --reason dsr-2026-0912    # traces, shows what it found, asks once, erases
47 things exist because of subject hmac:99d5…83ec   (raw id never stored)

  chroma:kb-v2                     10
  docs                             15
  exact-cache                       1
  faiss:kb-v1                      10
  ft-dataset                       10
  semantic-cache                    1

  kinds: cache×2, chunk×10, embed×20, source×5, train×10
  lineage gaps: none

erase all 47 of these? this cannot be undone  [y/N]

The pieces are still there when you want them, and everything else the tool does:

tombstone trace --subject S-0417                  # look without touching anything
tombstone erase --trace <trace-id> --reason dsr-2026-0912 --confirm
tombstone receipt
tombstone verify --receipt .tombstone/receipts/<id>.json --public-key .tombstone/keys/ed25519.pub   # independent check
tombstone replay                                  # re-derive every receipt from the journal
tombstone ui                                      # a local page for whoever handles the request
tombstone mcp                                     # MCP server; erase requires an elicitation confirmation

From your own code

A deletion request arrives in your product, not in a terminal — a customer clicks "delete my account", a ticket lands in a queue. So the erasure belongs in the handler you already have:

from tombstone import trace, forget

held = trace("S-0417")                       # what do we hold? reads only
print(held.count, held.by_store, held.gaps)

result = forget("S-0417", reason=f"dsr-{ticket_id}")    # destructive
if not result.ok:
    alert_privacy_team(result.report)        # something could not be confirmed; it says what

Calling forget is the confirmation — a call written in your own source is already a deliberate act, and a boolean people paste without reading protects nobody. It returns the same receipt the CLI writes, tombstone replay re-derives it identically, and it raises rather than reporting "nothing to delete" for a subject it has no lineage for. Both functions resolve lazily, so import tombstone still pulls in nothing heavy.

For whoever actually handles the request

tombstone ui        # http://127.0.0.1:7878 — search a person, see what is held, erase, read the receipt

Support and legal receive the DSR and do not use a terminal. The page binds loopback only, and every request to it must carry a per-run token that is printed with the URL, so a page the operator happens to be browsing cannot drive their deletion tool. It refuses lineage gaps, asks for a reason and writes the same journal and receipt as the CLI.

Driving it from an AI assistant (MCP)

tombstone mcp exposes six tools — tombstone.forget, tombstone.trace, tombstone.verify, tombstone.erase, tombstone.receipt, tombstone.status — over stdio or streamable HTTP. Point any MCP client at it:

{
  "mcpServers": {
    "tombstone": {
      "command": "tombstone",
      "args": ["mcp", "--config", "/absolute/path/to/tombstone.yaml"]
    }
  }
}

The assistant can then trace a subject, inspect a receipt, or ask for coverage, and tombstone.forget does the whole thing from a subject id in one call. Neither forget nor erase can execute without an explicit confirmation carried in the request, on every protocol revision, and forget erases exactly the set that confirmation listed. An agent cannot talk its way into deleting your data.

One thing this does not remove: Tombstone can only trace what it saw arrive. If the app was not wrapped at ingest, trace reports a lineage gap and refuses to claim an erasure it cannot back up — see the attacks, 7.1.

Results

backend method logical exclusion physical residue Recall@5 before → after wall/erasure
chroma B0 native delete() 100.0% 99.8% 98.5% → 98.0% 0.03s
chroma B1 delete + vendor compact 100.0% 100.0% 98.5% → 98.5% 0.03s
chroma B2 delete + full rebuild 100.0% 78.6% 98.5% → 98.5% 6.74s
chroma B3 Tombstone suppress only 100.0% 100.0% 98.5% → 98.0% 11.41s
chroma B4 Tombstone full 100.0% 0.0% 98.5% → 98.5% 20.98s
faiss B0 native delete() 100.0% 92.0% 94.0% → 94.0% 0.05s
faiss B1 delete + vendor compact 100.0% 92.0% 94.0% → 94.0% 0.04s
faiss B2 delete + full rebuild 100.0% 0.0% 94.0% → 98.5% 0.46s
faiss B3 Tombstone suppress only 100.0% 100.0% 94.0% → 94.0% 8.99s
faiss B4 Tombstone full 100.0% 0.0% 94.0% → 98.5% 11.22s
qdrant B0 native delete() 100.0% 100.0% 98.5% → 98.5% 0.01s
qdrant B1 delete + vendor compact 100.0% 100.0% 98.5% → 98.5% 0.01s
qdrant B2 delete + full rebuild 100.0% 0.0% 98.5% → 98.5% 6.16s
qdrant B3 Tombstone suppress only 100.0% 100.0% 98.5% → 98.5% 10.47s
qdrant B4 Tombstone full 100.0% 0.0% 98.5% → 98.5% 18.92s
pgvector B0 native delete() 100.0% 94.9% 82.0% → 99.0% 0.00s
pgvector B1 delete + vendor compact 100.0% 93.2% 89.5% → 99.0% 0.04s
pgvector B2 delete + full rebuild 100.0% 92.7% 85.5% → 99.0% 0.78s
pgvector B3 Tombstone suppress only 100.0% 100.0% 86.5% → 86.5% 7.96s
pgvector B4 Tombstone full 100.0% 0.0% 88.5% → 99.0% 10.81s

Full matrices, the hyperparameter grid, the anti-results and the attacks that beat Tombstone: RESULTS.md. Attacks first: Attacks that work against Tombstone.

Docs

Security

The core install carries no packages with open advisories; the optional extras do, and SECURITY.md lists each one, what it affects, and the two we have not fixed and why. Subject ids are HMACs under a per-installation pepper, and no content of any kind reaches the lineage database, journal, ledger or receipts — enforced by a test that is never skipped.

Prior art — who is already here, and where they stop

  • Ghost Vectors (arXiv 2606.18497) and Ghost Echoes (arXiv 2608.20352), Trinity College, 2026: soft-deleted embeddings are physically recoverable and invertible; correct deletion still leaves measurable retrieval drift; rebuilds do not remove it. They are attacks and measurements, not a deletion system. Our residue benchmark reproduces their drift measurement on our corpus and credits their methodology; the measurement is theirs.
  • vector-forget (PyPI): hard-delete plus physical-residue verification for pgvector (pgstattuple, REINDEX + VACUUM) — the right approach, which we use and credit. pgvector only; no lineage; explicitly no backups, replicas, WAL, caches, other stores or fine-tuned weights. We treat it as a collaborator.
  • forgetlayer (PyPI): cascade-delete for agent memory (Mem0/Zep) with Ed25519 receipts and an independent verifier — "a store grading its own homework convinces no auditor", which is exactly right and is where our independent verifier comes from. Agent memory only; no lineage capture at ingest, no vector-store physical layer, no training layer.
  • sura-rag (PyPI): delete → probe → guardrail → certify for RAG, local-only. No lineage, single store, and its wording over-claims relative to what a probe can show; that word does not appear in this repository.
  • LangChain's indexing API (index, RecordManager): genuine source → chunk lineage and the substrate we build on. It calls the store's soft delete(), knows nothing about caches, second indexes or training data, has no subject dimension, and cannot verify anything.
  • Machine unlearning literature: SISA (Bourtoule et al. 2021), NPO (arXiv 2404.05868), TOFU and MUSE, and the verification literature — Verification of Machine Unlearning is Fragile (arXiv 2408.00929), Dual-View Inference Attack (arXiv 2512.16126), SMI (arXiv 2602.01150) — whose main finding is that verification is easy to fool. That finding is why Phase 7 attacks our own unlearning verification.
  • Data-lineage tooling (OpenLineage, DataHub, Atlas): dataset- and job-level lineage; none model chunk → embedding → cache → training-example edges or execute deletion.

Tombstone's contribution: the first installable tool that captures subject-level lineage across the whole derived-artifact graph of an LLM application, executes erasure as a fan-out over that graph rather than a search, verifies every artifact at the strongest layer the store permits and reports the layer, and includes the fine-tuned model as a first-class artifact with measured unlearning, published together with the attacks that make that measurement unreliable.

Hard rules the code is held to

No LLM in the decision or verification path. Never claim a level you did not achieve. Targets come from lineage, never from similarity. Fail loud on missing lineage. Suppression before reclaim. Resumable saga, never partial success. Never log raw personal data. Scope isolation. Determinism and replay. Never delete what you did not trace. No telemetry, no hosted components, no accounts. Never report a number you did not measure. Each one is enforced by a test in tests/ — see tests/test_repo_checks.py, tests/test_secrets.py, tests/test_erase.py and tests/test_verify.py.

License

Apache-2.0.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tombstone_erase-0.1.0.tar.gz (166.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tombstone_erase-0.1.0-py3-none-any.whl (201.6 kB view details)

Uploaded Python 3

File details

Details for the file tombstone_erase-0.1.0.tar.gz.

File metadata

  • Download URL: tombstone_erase-0.1.0.tar.gz
  • Upload date:
  • Size: 166.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for tombstone_erase-0.1.0.tar.gz
Algorithm Hash digest
SHA256 cd5110061b9133d7403256023678a9b17a897058a06db86ffba7775898d44191
MD5 8435981a12c3b7996a41b7c2dc533395
BLAKE2b-256 0932315892e7df8db6cd0f40466343b70e07fd62412fc5624f1e41078c5d09d7

See more details on using hashes here.

Provenance

The following attestation bundles were made for tombstone_erase-0.1.0.tar.gz:

Publisher: release.yml on Poojan6216/tombstone

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file tombstone_erase-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: tombstone_erase-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 201.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for tombstone_erase-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 abc8a2c88629c8522fdc09fa561919646301a4030aa755f447320bdd63b51ee8
MD5 7b7d6a9671debe03c690b90f1b7c8173
BLAKE2b-256 e3240ae45d64674375959043577b4bddd62f7112315349b0c9193024662487d0

See more details on using hashes here.

Provenance

The following attestation bundles were made for tombstone_erase-0.1.0-py3-none-any.whl:

Publisher: release.yml on Poojan6216/tombstone

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page