Governance & Verifiable Receipts
The proof boundary: provable governance with cryptographically-signed receipts anyone can verify offline, with a pinned signer key.
Three GDPR rights underpin Shomei Governed Memory's governance model:
- Art. 17 (right to be forgotten): subject-scoped governed crypto-erasure
- Art. 18 (right to restriction): reversible, non-destructive hold on recall
- Art. 15 (right of access): portable subject-access export with proof
Every governed operation — deletion, restriction, export — emits an ed25519-signed, content-free receipt. Signed context traces use the same receipt envelope for recall integrity. A key-holder (DPO, General Counsel, auditor, or data subject) verifies the receipt offline, locally, using only public-key crypto and a pinned signer key. No access to the operator's systems. No network calls.
The Verification Ladder
Tier 1 (metadata): "we deleted it" — operator-reported, unverified.
Tier 2 (shipped today): Signed receipts + offline verification. A receipt authenticates the operator's signed statement and makes later byte tampering detectable. It does not independently observe that deletion happened. Verification requires pinning the signer's public key out-of-band, but no access to the operator's systems. This is what ships today.
Tier 3 (Business target; canary pipeline, product integration NOT shipped): A dedicated
application KMS key plus an independently retrievable destruction bundle. An auditor independently
retrieves AWS CloudTrail's rotating public key and verifies the AWS-signed digest, the referenced log
hash, the AWS-internal DeleteKey event, and the PCLD-style key-state root. This proves deletion of
the designated KMS key. It does not prove that plaintext was never copied or that every artifact was
exclusively governed by that key.
Tier 4 (Enterprise target, NOT shipped): TEE-attested key handling plus Tier 3 destruction evidence. A measured enclave would bind plaintext key release and use to approved code, while the same independently verified KMS evidence closes the root-key lifecycle. Independently attested enumeration is a separate requirement; today's receipts attest the delivered set as of the signed timestamp, not that a dishonest operator's enumeration was complete.
Key custody is the proof boundary. A receipt is only as trustworthy as the pinned public key. Pin the signer key out-of-band (CISO/DPO infrastructure), not from the service you are auditing.
Crypto-Erasure (Art. 17): Subject Forgetting
The flow:
from mochi_memory import RemoteMemory
m = RemoteMemory.from_env() # SHOMEI_BASE_URL, SHOMEI_API_KEY (admin-scoped)
# Erase all live memories for a subject (Art. 17)
result = m.forget(user_id="dana_smith")
# {
# "subject": "dana_smith",
# "erased_count": 41,
# "deferred_count": 1, # under legal hold; auto-erases on hold release
# "external_delete_pending_count": 0, # direct provider rows still owed
# "source_external_delete_pending_count": 0, # inference provider rows still owed
# "erased": [ ... ], # list of erased memory ids
# "deferred_held": [ ... ], # ids deferred under hold (restricted now)
# "receipt_hashes": [ {"id": ..., "receipt_hash": ...}, ... ],
# "signed_receipts": [ {"id": ..., "signed_receipt": { ... }}, ... ],
# "restriction_receipts": [ ... ],
# # closure block (present when a signer + commitment key are configured):
# "closure_receipt": { ... },
# "closure_receipt_hash": "sha256:...",
# "signed_closure_receipt": { ... },
# "note": "governed crypto-erasure (Art.17). ..."
# }
forgetrequires an admin-scoped bearer key. There is no top-levelsigned_receipt— per-memory proofs are undersigned_receipts(a list), and the subject-level proof issigned_closure_receipt. The matched-count and disposition live inside the closure receipt body, not at the top level.
For an external vector backend, nonzero external_delete_pending_count or
source_external_delete_pending_count means local DEK destruction committed but exact provider absence
has not been confirmed. The HTTP surface returns a structured retryable 503, not a successful erasure.
Engine-side subject_closure_state() and ClosureChecker keep fully_erased / fully_erased_now false
while attributable provider debt remains; a legacy pending row with no exact retry-owner link is
conservatively counted for every subject too.
Subject forget commits an opaque governed+inference target journal before erasing the first target. A crash
between targets is replayed on workspace reopen. This is a crash-resumable saga, not cross-provider ACID;
provider debt or an unresolved pre-crash upsert keeps global success and fully_erased false.
External deployments require more than an immutable operator binding_id and
causally_ordered_point_reads=True. The adapter must also inject a provider-enforced revision-CAS and
idempotency implementation with a provider-issued immutable resource id. Every upsert/delete carries a
pre-persisted random operation id and the workspace's exact provider revision; the provider verifies that
revision at mutation apply time, atomically advances it with the mutation, retains crash-reconcilable
operation status, and fences point reads and candidate queries at the same exact revision. After local
settlement, provider acknowledgement must synchronously destroy the operation-status mapping to the vector
id/result/receipt (a random operation-id/revision tombstone may remain). Bootstrap must prove the resource
empty and never bound. The fencing authority must hold the sole mutation credentials—an ordinary SDK call
behind a client-only lock or boolean is not conforming.
Shomei persists the binding and provider revision, refuses local/provider/resource drift, refuses to adopt a provider's newer revision from an old workspace copy, and reconciles an applied-but-unrecorded operation only through its pre-persisted operation id. The bundled Pinecone, Azure, Databricks, pgvector, and Snowflake SDK translators default to unsupported for the synchronous external-delete claim unless such a genuine provider-side implementation is injected. API-level absence still does not prove deletion from provider replicas, backups, transport/application logs, or physical media. The local no-readable-copy claim also assumes no writable/offline copy of the SQLite workspace exists; provider fencing prevents a stale copy from resurrecting a remote vector, but cannot erase readable payload bytes inside that copied database.
What happens:
- Every LIVE memory for the subject is found.
- Each record's per-row data-encryption key is destroyed (not just a tombstone). At-rest cells are sealed with per-row DEK envelope encryption — this is NOT SQLCipher page-encryption; destroying the row's DEK makes that ciphertext cryptographically undecryptable (under production KMS custody).
- Once the per-row DEK is destroyed under production KMS custody, that row's ciphertext is cryptographically undecryptable even if recovered from a data-volume backup. Honest scope: wrapped-KEK destruction is best-effort on SSD/FTL/copy-on-write media, and a KEK under a CMK only scheduled for deletion stays recoverable via KMS
CancelKeyDeletionuntil the AWS window elapses; the receipt does not assert that copies in other systems were erased. - Held memories are NOT erased now: they are RESTRICTED (Art. 18, out of recall) and carry a durable obligation that auto-fires the crypto-erase when the hold releases (reported under
deferred_held/deferred_count). - A signed subject-closure receipt is minted, attesting the closed set, counts, and disposition.
The subject-closure receipt body (shomei.subject_closure.v1, or shomei.subject_closure.v2 when the enriched combined-store fields are present) carries:
subject_pseudonym— a keyed commitment, not the rawuser_idclosed_set_commitment— a keyed commitment over the erased set (auditor-reproducible)closed_count,matched_count,erased_count,deferred_count,live_countdisposition—complete,partial,deferred, ornoop_no_match(re-derived from the counts; a body can never be signed with an overclaiming disposition)match_basis—exact_user_idorscoped_user_idapplied_at— the timestamp the operation took effectnonce— fresh perforget()call, so repeated forget attempts have distinct signed bodies
Honest non-claims (preserved in the framing):
- Does NOT assert removal from copies, backups, or third-party systems outside the governed store
- Does NOT erase facts inferable from correlated records elsewhere
- Subject identity is pseudonymized, not anonymized (the holder of the tenant key can re-derive it)
- Attests the at-rest state at the signed time; an auditor recomputes the commitment to verify current state
Processing Restriction (Art. 18): Reversible Hold
Restriction takes a memory out of recall without destroying it — distinct from a legal hold (which preserves and allows use). A restricted memory is excluded from recall/search/get but remains available for audit, subject-access, and legal claims.
The
restrict()/release_restriction()verbs exist on both SDK surfaces: the in-processmochi_memory.Memoryand the HTTPRemoteMemory(which callsPOST /v1/memories/{id}/restrictto restrict andDELETE /v1/memories/{id}/restrictto lift — both admin-scoped). The two surfaces return slightly different shapes: the in-process verb returns{"memory_id", "restricted", ...}with anote;RemoteMemoryreturns the service shape{"id", "restricted", "changed", ...}.
The flow (in-process SDK):
from mochi_memory import Memory
m = Memory.from_env()
# Restrict a single memory from use (e.g., pending legal review)
result = m.restrict(memory_id="lm_abc123", reason="art18_request")
# {
# "memory_id": "lm_abc123",
# "restricted": true,
# "receipt_hash": "sha256:...",
# "signed_receipt": { ... },
# "note": "Art.18 restriction: out of recall, preserved + reversible; ..."
# }
# Lift the restriction later (non-destructive)
result = m.release_restriction(memory_id="lm_abc123")
# { "memory_id": "lm_abc123", "restriction_lifted": true,
# "receipt_hash": "sha256:...", "signed_receipt": { ... } }
The reason must be one of the controlled-vocabulary reason codes (free text is rejected so content can never be smuggled into a content-free receipt): art18_request, art18_lifted, art17_erasure_deferred_under_hold, accuracy_contested, unlawful_processing, needed_for_legal_claims, objection_pending, consent_withdrawn, consent_restored. Lifting always records art18_lifted regardless of the caller-supplied reason.
What happens:
- The memory is taken OUT OF RECALL at the index level.
- It is preserved intact and remains reachable for audit and legal access.
- It does NOT contribute to semantic search or context recall.
- The restriction is reversible — lifting it returns the memory to normal use.
- A signed restriction receipt (
shomei.restriction.v1) is minted, attesting the action and timestamp.
The restriction-receipt body carries action (applied | released), reason_code, restricted_state, applied_at, and an optional key-derived subject_commitment. The top-level SDK response surfaces memory_id, restricted, receipt_hash, signed_receipt, and a note; the reason_code / restricted_state / applied_at fields live inside the receipt body, not at the top level.
Use cases:
- Data subject exercises Art. 18 to halt processing of their personal data
- Compliance hold pending litigation or investigation
- Temporary exclusion from AI-driven features while reviewing content
Subject Access Export (Art. 15): Portable Evidence
Export produces an evidence bundle for a subject: live governed memories, content-free erasure evidence for prior forgets, the subject's inference-derived and derived-artifact records, and a signed subject-export receipt — all verifiable offline.
The flow:
from mochi_memory import RemoteMemory
m = RemoteMemory.from_env()
# Export a subject's data + erasure evidence + signed proof
result = m.export(user_id="dana_smith", include_content=False)
# {
# "subject": "dana_smith",
# "count": 12, # live governed memories
# "content_included": false,
# "memories": [ ... ], # each row: id, semantic_type, held, restricted,
# # recallable (= not restricted), consent_basis, verify
# "erased_count": 41,
# "erased": [ ... ], # content-free proof of prior erasures
# "inference": { "sources": [...], "states": [...] },
# "inference_count": 7,
# "inference_restricted_retained": [ ... ],
# "inference_restricted_retained_count": 0,
# "derived": [ {"id": "artifact:9f3c...", "status": "active", "recallable": true}, ... ],
# "derived_count": 3,
# "live_count": 22, # = len(memories) + inference records + derived ids
# "note": "evidence export: LIVE governed memories + ...",
# # present when a commitment key is configured:
# "receipt": { ... },
# "receipt_hash": "sha256:...",
# # present when a signer is also configured:
# "signed_receipt": { ... }
# }
The subject key is
subject(notuser_id). The signed envelope issigned_receipt.RemoteMemory.exporttakesuser_id,include_content, andinclude_restricted; settinginclude_content=Trueorinclude_restricted=Truerequires an admin-scoped key.
include_restricted=True is the Art.15 subject-access mode; over HTTP, subject_access=true is accepted
as an alias. It only renders restricted-but-retained content when combined with include_content=True, and
it is gated by the same admin authorization as content export. A data-scoped key receives 403. Restricted
rows remain out of recall; crypto-erased rows stay unreturnable.
What's included:
- All LIVE governed memories for the subject (metadata; content only when
include_content=True). Each row carriesrecallable(true unless the memory is restricted). - Erasure evidence (
erased): for every prior erasure, a content-free record proving it happened. - Inference-derived records (
inference) and any restricted-but-retained inference sources (inference_restricted_retained, Art. 18 out-of-recall, not erased). - The subject's derived-artifact set (
derived): the Art. 15 mirror of the Art. 17 deletion cascade. Each entry is a content-free opaqueartifact:<hex>node id withstatus: "active"and an honest per-noderecallableflag —recallableistrueonly when at least one supporting source is still served, so a restricted-source derived node correctly reportsrecallable: false. - A signed subject-export receipt, verifiable by the subject or a third party.
The receipt:
The receipt body schema is shomei.subject_export.v1. The body carries tenant_id_hash, a keyed subject_commitment, exported_at, live_count (which includes the derived-artifact count), erased_count, content_included, a nonce, and exported_set_witness — a recomputable witness commitment over the sorted union of live governed ids, live inference logical ids, restricted-retained inference ids, content-free erased-evidence ids, and the derived-artifact node ids. The witness is committed under set-schema shomei.subject_export_set.v2, so access-completeness over the derived-artifact graph is attested the same way deletion-completeness is.
Honest scope. The witness proves the delivered set is untampered as of
exported_at. It does not prove the operator's enumeration was complete — completeness against a dishonest operator needs the measured-enclave roadmap (Tier 3).
Attested Recall: Context Trace Receipts
context() / POST /v1/context returns a signed trace_receipt when the tenant has a receipt signer.
The body schema is shomei.context_trace.v1. It is content-free and carries:
subject_pseudonym— a keyed commitment, ornullfor an anonymous query; never the rawuser_idquery_commitment— a keyed commitment; never the raw queryserved_set_witness— a commitment over the served opaque id setomissions_commitment— a commitment over the withheld/omitted setnonce— fresh per context renderproof_scope—integrity_of_served_and_withheld_set_as_of_query_not_retrieval_optimality
The keyed query_commitment above exists in the signed receipt returned to the caller; Shomei does not
retain it in the durable context-trace table. The durable row stores only unretained_<trace_id> (or an
erased_<rowid> tombstone after linked content is erased), and records only whether a scope was bound—not
the caller-derived scope commitment.
Before signing, the trace accounting requires every candidate artifact to be represented either in the served set or in the omission set. That is the served-set completeness guarantee: the signature binds the served/withheld accounting for this query. It does not prove retrieval optimality, answer correctness, or that a dishonest runtime selected the best candidates.
Verify offline with the standalone
shomei_memory_verify library:
from shomei_memory_verify import verify_context_trace_receipt
result = verify_context_trace_receipt(
trace_receipt,
served_ids=observed_served_ids,
omissions=observed_omissions, # optional; omit if you only retained the served ids
expected_public_key_hex=pin,
)
assert result.authenticated and result.served_set_matches
Tampering with a served id causes served_set_witness_mismatch; tampering with supplied omissions causes
omissions_commitment_mismatch.
Producer ON by default
The derived-artifact lineage producer is ON by default wherever a receipt signer is present. The hosted service and the SDK's Memory.from_profile / Memory.from_env constructors always wire a signer (derived from the tenant KEK), so the derived-artifact lineage — and its disclosure in export() — is active by default. There is nothing to opt into for the Art. 15 derived-artifact set.
Receipts: Content-Free, Signed, Verifiable Offline
Every receipt is a small JSON envelope, ed25519-signed, with exactly six fields:
{
"schema": "shomei.vector_delete.v1",
"receipt_hash": "sha256:<canonical-body-hash>",
"signer_id": "shomei-receipt:<tenant>",
"public_key_hex": "<ed25519 public key, hex>",
"signature_hex": "<ed25519 signature, hex>",
"body": {
"schema": "shomei.vector_delete.v1",
"tenant_id_hash": "<keyed hash>",
"memory_id": "lm_abc123",
"physical_purge_state": "...",
"key_custody": "kms",
"...": "content-free metadata only"
}
}
The envelope field names are exactly schema, receipt_hash, signer_id, public_key_hex, signature_hex, body — not signature / public_key / signed_at. There is no top-level signed_at; timestamps (applied_at, exported_at, etc.) live inside body. The signature_hex is an ed25519 signature over the RFC 8785 (JCS) canonical bytes of body, and receipt_hash is sha256: over those same canonical bytes.
Receipt body schemas you will encounter:
| Operation | Body schema |
|---|---|
Record deletion (delete) |
shomei.vector_delete.v1 |
Subject erasure closure (forget) |
shomei.subject_closure.v1 / .v2 |
Restriction (restrict/lift) |
shomei.restriction.v1 |
Subject export (export) |
shomei.subject_export.v1 |
Context trace (context) |
shomei.context_trace.v1 |
| Memory add | shomei.memory_add.v1 |
| Memory update | shomei.memory_update.v1 |
key_custody in a delete receipt is stamped from the configured KEK provider tier: kms, file, in_memory_dev, escrowed, tpm_simulated, or unknown for an unrecognized custom provider. kms / tpm_simulated / escrowed are production tiers; file / in_memory_dev are dev floors and self-qualify the receipt as such.
Properties:
- Content-free: the receipt body contains NO plaintext data, only opaque ids, keyed hashes, and operation metadata.
- Signed: ed25519 over RFC 8785 (JCS) canonical bytes — a reproducible serialization.
- Offline-verifiable: public-key crypto alone; no network, no private keys.
- Auditor-friendly: small, human-readable metadata.
Verifying a Receipt
In the SDK (Python)
After a deletion:
from mochi_memory import RemoteMemory
m = RemoteMemory.from_env()
deleted = m.delete(memory_id="lm_abc123")
receipt = deleted["signed_receipt"]
# Fetch or pin the signer's public key (production: pin out-of-band)
pin = m.signer_key()["public_key_hex"]
# Verify locally (no server round-trip)
result = m.verify_receipt(receipt, expected_public_key_hex=pin)
assert result.authenticated, f"Not authenticated: {result.reason}"
print(f"Verified {result.body['memory_id']} (schema {result.schema})")
RemoteMemory.verify_receipt is a static method that delegates to the standalone offline verifier; it raises RemoteMemoryError if the verifier package is not installed.
Offline CLI Verifier
Install the verifier (standalone, the one artifact Shomei publishes — cryptography is its only third-party dependency, Python 3.9+):
pip install shomei-memory-verify
Verify a receipt file:
# Verify against a pinned key; print a human-readable report
shomei-memory-verify receipt.json --pin <signer_public_key_hex> --render
# Quick verdict (exit code 0 = authenticated)
shomei-memory-verify receipt.json --pin <key>
# Verify without pinning (self-consistency only; exits non-zero)
shomei-memory-verify receipt.json --render
# Read the receipt from stdin
shomei-memory-verify - --pin <key> --json
# A mismatched pin fails authentication
shomei-memory-verify receipt.json --pin <wrong_key> --render
You can also invoke the module form, python -m shomei_memory_verify receipt.json --pin <hex>, which is identical. --pin has the alias --expected-public-key, and - as the receipt path reads stdin.
Output:
--render: a one-page, plain-language report for a GC, DPO, or regulator. Authenticated language is emitted only for an authenticated result; an unpinned-but-valid receipt is downgraded to "ASSERTED, pending verification," and an invalid receipt gets no authenticated report.--json: structured result with exactlyvalid,authenticated,reason,schema,public_key_hex.- Exit code:
0only whenauthenticated=true(pinned key + valid signature). Unpinned-but-valid, mismatched, invalid, and malformed all exit1.
Offline Library Verification
Python:
import json
from shomei_memory_verify import verify_signed_receipt, render
# verify_signed_receipt accepts a SignedReceipt OR a parsed mapping (dict), not a raw JSON string
receipt = json.load(open("receipt.json"))
# Pin the signer key (obtained out-of-band) to AUTHENTICATE
pin = "ed25519_public_key_hex"
result = verify_signed_receipt(receipt, expected_public_key_hex=pin)
assert result.authenticated, f"Not authenticated: {result.reason}"
# Human-readable statement
print(render(result))
verify_signed_receipt(receipt, *, expected_public_key_hex=None) returns a VerifyResult. bool(result) is authentication-gated — it is truthy only when the result is authenticated against a pinned key, so a plain if result: is a safe trust gate.
Result fields (VerifyResult, a frozen dataclass):
@dataclass(frozen=True)
class VerifyResult:
valid: bool # envelope well-formed AND ed25519 sig consistent over body AND hash matches
authenticated: bool # valid AND verified against a caller-PINNED key
reason: str # see reason codes below
public_key_hex: str = "" # the authenticated signer key (trust this only when authenticated)
schema: str = "" # the SIGNED body schema (the envelope schema is not surfaced)
signer_id: str = "" # ADVISORY ONLY — unsigned envelope metadata, attacker-relabelable
body: dict = {} # the content-free receipt claims (ids, counts, timestamps)
reason is one of exactly five codes:
'ok'— pinned key matches, signature and hash valid (valid=true,authenticated=true)'ok_unpinned'— valid but no key was pinned, self-consistent only (valid=true,authenticated=false)'public_key_mismatch'— the pinned key does not match the embedded key'signature_or_hash_invalid'— signature invalid orreceipt_hashmismatch'malformed: <detail>'— the envelope is not parseable (missing required key,bodynot a mapping, etc.)
validandauthenticatedare distinct.validmeans the receipt is internally consistent (well-formed envelope + signature consistent over the body + hash matches).authenticatedmeansvalidAND the signature was checked against a key you pinned out-of-band. The trust gate isauthenticated.signer_idis unsigned advisory metadata — never trust it; trustpublic_key_hex(and only whenauthenticated).
The Honest Framing
What a receipt proves:
- The operator signed the receipt with a specific ed25519 private key
- The receipt's content-free claims (timestamp, opaque ids, counts) are bound to that signature
- The key-holder can verify this offline, after pinning the signer key out-of-band
What a receipt does NOT prove:
- That the operator did not copy the data elsewhere before erasing
- That correlated data from other sources is also erased
- That the data was not logged or backed up outside the governed memory store
- That the subject identity is anonymized (it is pseudonymized with the tenant key)
- That the operator's enumeration of the subject's set was complete (the witness binds the delivered set as of the signed time; completeness against a dishonest operator is the Tier-3 / measured-enclave roadmap)
- The current state (only the at-rest state at the signed timestamp; an auditor recomputes the commitment to verify current state)
Verification levels:
- Self-consistent only (
valid=true, authenticated=false, reasonok_unpinned): the receipt is structurally sound and internally consistent, but you did NOT pin the signer's key, so anyone could have signed it. - Authenticated (
authenticated=true, reasonok): the signature matches the public key you pinned out-of-band. The operator (or someone with their key) signed it.
The verifier downgrades unpinned or invalid receipts to explicit unauthenticated language to prevent over-reliance.
Key Custody & Trust
Key custody is the proof boundary. A receipt is only as trustworthy as the key you pin.
For production:
- Pin the signer's public key out-of-band before verification. Store it in your security infrastructure (GC, CISO, DPO systems), not in the governed memory service you are auditing.
- Run the verifier locally on auditor machines, DPO systems, or in an air-gapped environment.
- Rotate keys via your KMS (AWS KMS, BYOK). The service derives its per-tenant signer key from the tenant KEK; under the default v2 custody there is no shared signing secret.
For demos/testing only:
The SDK can fetch the signer key from the service:
key = m.signer_key()["public_key_hex"]
But in production, fetch this once, verify it out-of-band with your CISO, and pin it locally. Fetching the key from the service you are auditing is a dev-only convenience — it is not a trust anchor.
Receipts in the Service API
A signed receipt is returned by:
DELETE /v1/memories/{id}→signed_receipt(record crypto-erasure; admin)POST /v1/forget→signed_closure_receipt+ per-memorysigned_receipts(subject erasure; admin)GET /v1/export?user_id=...→signed_receipt(subject access)POST /v1/context→trace_receipt(recall served/withheld-set integrity; present when a signer is configured)POST /v1/memories/{id}/restrict→signed_receipt(Art. 18 restriction; admin)
Signer public key:
GET /v1/account/signer-key→{"tenant": "...", "signer_public_key_hex": "..."}
Operator-reported verdict (NOT cryptographic proof):
GET /v1/memories/{id}/verify→{"valid", "retrieval_safe", "embedding_retired", "physically_purged", "failures"}, or{"valid": null, "reason": "not_found"}. This is self-reported governance status, not a signed proof.
Example: End-to-End Governance Workflow
Scenario: Dana requests her data subject rights (Art. 15 + 17).
from mochi_memory import RemoteMemory
import json
m = RemoteMemory.from_env() # admin-scoped key
# 1. Export her data (Art. 15)
export_result = m.export(user_id="dana_smith", include_content=True)
export_receipt = export_result["signed_receipt"]
print(f"Export: {export_result['count']} live governed, "
f"{export_result['derived_count']} derived artifacts, "
f"{export_result['erased_count']} prior erasures")
# 2. Verify the export receipt
pin = m.signer_key()["public_key_hex"] # in production, pin out-of-band instead
verify_result = m.verify_receipt(export_receipt, expected_public_key_hex=pin)
assert verify_result.authenticated, "Export receipt not authentic"
print(f"Export verified (schema {verify_result.schema})")
# 3. Ship her data + receipt
with open("dana_smith_export.json", "w") as f:
json.dump(export_result, f, indent=2)
# 4. Erase her remaining memories (Art. 17)
forget_result = m.forget(user_id="dana_smith")
forget_receipt = forget_result["signed_closure_receipt"]
# 5. Verify the forget closure receipt
verify_result = m.verify_receipt(forget_receipt, expected_public_key_hex=pin)
assert verify_result.authenticated, "Forget receipt not authentic"
print(f"Forget verified: {forget_result['erased_count']} erased, "
f"{forget_result['deferred_count']} deferred")
# 6. Auditor verifies offline (no service access needed)
# $ shomei-memory-verify dana_smith_export.json --pin <pinned_key> --render