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 AWS recorded destruction 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