Python SDK Reference
The mochi_memory package provides two main entry points: Memory (in-process) and RemoteMemory (HTTP client). Both expose a Mem0-shaped API with governance verbs for provable deletion and access control.
The package is imported as
mochi_memory. The two classes do not share an identical surface: the in-processMemoryis the full engine facade (every verb below, with anow=clock override on most), whileRemoteMemoryis a thinner HTTP client that wraps only the routes the service exposes. Signatures and return shapes are documented per class where they differ.
Constructors
Memory.from_profile
Memory.from_profile(
profile: Union[str, Profile],
*,
tenant_id: str,
db_path: str = ":memory:",
regulation: Optional[str] = None,
kek_provider: Optional[Any] = None,
commitment_key: Optional[CommitmentKey] = None,
allow_dev_key_custody: Optional[bool] = None,
now: Optional[float] = None,
clock: Optional[Any] = None
) -> Memory
Construct a governed in-process memory instance from a named profile.
Parameters:
profile: One of"support_copilot","enterprise_assistant","regulated_agent". If aProfileobject, it is used directly.tenant_id: Logical tenant identifier (required).db_path: SQLite database path;":memory:"for ephemeral. Defaults to":memory:".regulation: Required ifprofile="regulated_agent"; one of"hipaa","pci","finance". The other two profiles rejectregulation=(passNone).kek_provider: A KMS/TPM/HSM-backed key provider. IfNoneand dev custody is not opted in, raisesValueError.commitment_key: ACommitmentKeyfrom your secret store. IfNoneand dev custody is not opted in, raisesValueError.allow_dev_key_custody: ExplicitTrue/Falseoverride;Nonedefers toSHOMEI_ALLOW_DEV_CUSTODYenv. For production, never allow dev custody.now: Optional clock override (float, seconds since epoch).clock: Optional clock object (for testing).
Returns: A Memory instance, initialized and embedder-configured (hash floor by default; bedrock/ollama per env). A receipt signer is always derived from the tenant KEK (HKDF), so signed receipts and the derived-artifact lineage producer are on.
Raises:
ValueErrorifregulationis missing forregulated_agent, or if a non-existent regulation is named.ValueErrorif nokek_provider/commitment_keyand dev custody is not opted in.
Example:
from mochi_memory import Memory
mem = Memory.from_profile(
"support_copilot",
tenant_id="acme",
db_path="./memory.db",
allow_dev_key_custody=True # dev only
)
Memory.from_env
Memory.from_env(
*,
profile: Optional[str] = None,
kek_provider: Optional[Any] = None,
commitment_key: Optional[CommitmentKey] = None,
now: Optional[float] = None,
clock: Optional[Any] = None
) -> Memory
Construct from environment variables (fail-closed, production-safe defaults). Each var is read as SHOMEI_<name> with a legacy MOCHI_<name> fallback.
Environment variables:
SHOMEI_TENANT_ID: Required.SHOMEI_MEMORY_DB: Default"./shomei-memory.db".SHOMEI_PROFILE: Default"support_copilot".SHOMEI_REGULATION: Required iff profile isregulated_agent.SHOMEI_KMS_PROVIDER: Default"local-dev"; any other name raisesValueErrorunless a real provider is injected.SHOMEI_ALLOW_DEV_CUSTODY: Set to1,true,yes, oronto allow local-dev key custody (dev only).SHOMEI_EMBEDDER:"hash"(default, non-semantic),"bedrock", or"ollama".SHOMEI_EMBED_MODEL: Model name (for bedrock/ollama).SHOMEI_EMBED_DIMS: Embedding dimensions; default"1024".SHOMEI_EMBEDDER_STRICT: Default"1"(strict mode); set to"0"for degraded-but-available fallback.SHOMEI_AWS_REGION: AWS region for bedrock; default"us-east-1".
Returns: A Memory instance.
Raises: ValueError if SHOMEI_TENANT_ID is not set, or if SHOMEI_KMS_PROVIDER is not "local-dev" and a real provider is not injected.
Example:
import os
os.environ["SHOMEI_TENANT_ID"] = "acme"
os.environ["SHOMEI_ALLOW_DEV_CUSTODY"] = "1"
mem = Memory.from_env()
RemoteMemory.from_env
RemoteMemory.from_env() -> RemoteMemory
Construct a remote client from environment variables.
Environment variables:
SHOMEI_BASE_URL: The Shomei service root URL (required).SHOMEI_API_KEY: Bearer token (required).SHOMEI_CA_CERT: Path to a CA certificate to pin (optional; for self-signed certs in dev).SHOMEI_TLS_INSECURE: Set to1to skip TLS verification (encryption only, demo only).
Each var is read as SHOMEI_<name> with a legacy MOCHI_<name> fallback.
Returns: A RemoteMemory instance.
Raises: RemoteMemoryError if SHOMEI_BASE_URL or SHOMEI_API_KEY are missing.
Example:
import os
os.environ["SHOMEI_BASE_URL"] = "https://shomei.example.com"
os.environ["SHOMEI_API_KEY"] = "sk-..."
remote = RemoteMemory.from_env()
RemoteMemory.init
RemoteMemory(
base_url: str,
api_key: str,
*,
verify_tls: bool = True,
ca_cert: Optional[str] = None,
timeout: float = 30.0,
capability_actor: Optional[str] = None,
capability_audience: Optional[str] = None
) -> None
Direct constructor for the remote client.
Parameters:
base_url: Service root URL (e.g.,"https://shomei.example.com").api_key: Bearer token.verify_tls: Whether to verify the TLS certificate; defaultTrue.ca_cert: Path to a CA certificate file to pin (for self-signed certs).timeout: Request timeout in seconds; default30.0.capability_actor,capability_audience: Accepted but inert (capability-token auth is retired).
Raises: RemoteMemoryError if base_url or api_key are empty.
Write Operations
add
# Memory (in-process)
def add(
messages: Any,
*,
user_id: Optional[str] = None,
semantic_type: str = "GENERIC",
infer: bool = False,
now: Optional[float] = None,
governance: Optional[GovernanceMetadata] = None,
infer_mode: str = "async",
consistency_mode: str = "session",
idempotency_key: Optional[str] = None,
inference_profile: Optional[str] = None,
source_kind: Optional[str] = None,
actor_kind: Optional[str] = None,
actor_id: Optional[str] = None,
caller_key_id: Optional[str] = None,
scope: Optional[Any] = None,
return_context_pack: bool = False
) -> dict
RemoteMemory.add has the same parameters except now, governance, and caller_key_id (which are in-process only).
Store a memory record. Content is indexed and retrievable; subject-linked to user_id (for DSR); optionally triggers inference.
Parameters:
messages: The memory content (string or list of message objects).user_id: Subject identifier (required for DSR/forget/export); must be a string if provided. Non-string types raiseTypeError.semantic_type: Policy label (e.g.,"GENERIC","PII","SENSITIVE"); default"GENERIC".infer: IfTrue, run inference; defaultFalse.governance: AGovernanceMetadataobject (in-process only) with optional fields such asconsent_basis.infer_mode:"async"(default) or"sync".consistency_mode:"session"(default, read-after-write) or"eventual".idempotency_key: A unique key to deduplicate retries.inference_profile: Profile for the inference step (ifinfer=True).source_kind,actor_kind,actor_id: Metadata for audit.caller_key_id: Metadata for key-rotation audit (in-process only).scope: Additional scope metadata.return_context_pack: IfTrue, include context-retrieval metadata in the response (honored only wheninfer=True).now: Optional clock override (in-process only).
Returns (infer=False): a results list with one row per stored memory:
{
"results": [
{
"id": "lm_...", # public logical ID
"memory": "Dana prefers SMS reminders", # the stored text, echoed back
"event": "ADD",
"receipt": {...}, # content-free creation receipt body
"receipt_hash": "sha256:...",
"signed_receipt": {...} # present only when a receipt signer is configured
}
]
}
Returns (infer=True): a different, inference-pipeline shape (handle/source ids, claim/event/state ids, pending-job ids, degraded, and optionally context_pack) — not the {"results": [{...ADD...}]} shape above.
Raises:
TypeErrorifuser_idis notNoneor a string.ValueErrorifuser_idis an empty string.
Example:
result = mem.add("Dana prefers SMS reminders", user_id="u1")
memory_id = result["results"][0]["id"]
bulk
# Memory (in-process)
def bulk(
items: list, # list of item dicts (see below)
*,
import_id: Optional[str] = None, # caller label, echoed + bound into the batch receipt
idempotency_key: Optional[str] = None # request-level convergence key
) -> dict
# RemoteMemory
def bulk(
items: list,
*,
import_id: Optional[str] = None,
idempotency_key: Optional[str] = None,
page_size: Optional[int] = None # defaults to the server's 2,000-item request cap
) -> dict
Bulk import through the governed single-add path (POST /v1/memories/bulk server-side, which ships dark behind SHOMEI_BULK_IMPORT_ENABLED). RemoteMemory.bulk pages sets larger than 2,000 items transparently and returns one aggregated result whose items list is index-aligned with the input list (global indices), with one batch_receipt per page under batch_receipts.
Each item dict accepts the single-add provenance fields: messages (required), user_id, occurred_at, idempotency_key, source_kind, actor_kind, actor_id, scope, consent_basis, inference_profile, semantic_type. Bulk is always async — every accepted item returns recall_readiness: "pending_semantic_index" until the shared index worker drains it; an item with consistency_mode: "strong" fails the request (strong_unsupported_in_bulk).
Partial failure is normal, never all-or-nothing: a validation-rejected item is reported at its index (status: "rejected", error.code) and every other item proceeds. With idempotency_key set, a retry of the whole call converges per page and per item (derived keys {key}:{index}) instead of double-ingesting.
Returns: batch_id (or batch_ids from the remote pager), accepted_count, rejected_count, recall_readiness, index-aligned items (accepted entries carry id, job_id, receipt_hash, recall_readiness, and signed_receipt when a signer is configured), the signed content-free batch_receipt binding the ordered per-item receipt hashes, and quota.metered_items (metering counts only committed items).
Raises: ValueError on an empty/oversized request; BulkImportInFlightError (in-process) or an HTTP 429 bulk_concurrency_exceeded (remote) when another bulk import is already in flight for the tenant.
Example:
result = remote.bulk(
[{"messages": t, "user_id": "u1"} for t in transcripts], # any size; pages transparently
import_id="backfill-2026-07",
idempotency_key="backfill-2026-07-run1",
)
assert result["rejected_count"] == 0, [e for e in result["items"] if e["status"] == "rejected"]
update
# Memory (in-process)
def update(
memory_id: str,
data: Any,
*,
semantic_type: Optional[str] = None,
now: Optional[float] = None
) -> dict
RemoteMemory.update(memory_id, data, *, semantic_type=None) is the same without now.
Governed update: this is not an in-place edit. It revokes the prior memory and recreates a successor with a new id, emitting an UPDATE event and a fresh receipt.
Parameters:
memory_id: The public logical ID (e.g.,"lm_...").data: The new content.semantic_type: Optional new policy label.now: Optional clock override (in-process only).
Returns:
{
"results": [
{
"id": "lm_...", # the NEW successor id
"previous_id": "lm_...", # the revoked prior id
"event": "UPDATE",
"prior_erased": True,
"note": "...",
"receipt": {...},
"receipt_hash": "sha256:...",
"signed_receipt": {...}
}
]
}
A held, restricted, withdrawn-consent, or unknown/already-erased memory raises ValueError (over HTTP these map to 423/409/404 — see the HTTP API reference).
Example:
out = mem.update("lm_abc123", "Dana now prefers email", semantic_type="GENERIC")
new_id = out["results"][0]["id"]
Read Operations
These read verbs take a now= clock override on the in-process Memory; the RemoteMemory equivalents do not.
search
# Memory (in-process)
def search(
query: str,
*,
user_id: Optional[str] = None,
agent_id: Optional[str] = None,
session_id: Optional[str] = None,
run_id: Optional[str] = None,
limit: int = 10,
min_score: Optional[float] = None,
now: Optional[float] = None,
include_restricted: bool = False
) -> dict
RemoteMemory.search(query, *, user_id=None, limit=10, min_score=None) exposes the common subset over HTTP.
Search for memories by semantic similarity.
Parameters:
query: The search text.user_id: Scope results to this subject (optional).limit: Max results; default10.min_score: Similarity threshold; omit low-scoring noise. DefaultNone(no threshold).
Returns:
{
"results": [
{
"id": "lm_...",
"memory": "...", # the stored text — the field is `memory`, not `content`
"user_id": "...",
"score": 0.87,
# "rank_score" and "rank_explanation" appear only if a reranker is applied
}
],
"recall_label": "...",
"degraded": False,
"reranked": False,
"rerank_label": "not_configured",
"rerank_degraded": False
}
Example:
hits = mem.search("SMS reminders", user_id="u1", limit=5, min_score=0.7)
for hit in hits["results"]:
print(hit["id"], hit["memory"], hit["score"])
get
# Memory (in-process)
def get(memory_id: str, *, now: Optional[float] = None, include_restricted: bool = False) -> Optional[dict]
RemoteMemory.get(memory_id) is the HTTP equivalent.
Retrieve a single memory by ID.
Returns: None if not found (erased, restricted, or missing all return None), otherwise:
{
"id": "lm_...",
"memory": "...", # the stored text — the field is `memory`, not `content`
"user_id": "...",
"metadata": {"semantic_type": "GENERIC"} # semantic_type is nested under metadata
}
Example:
mem_data = mem.get("lm_abc123")
if mem_data:
print(mem_data["memory"])
get_all
# Memory (in-process)
def get_all(
*,
user_id: Optional[str] = None,
agent_id: Optional[str] = None,
session_id: Optional[str] = None,
run_id: Optional[str] = None,
limit: int = 100,
now: Optional[float] = None,
include_restricted: bool = False
) -> dict
RemoteMemory.get_all(*, limit=100, user_id=None) exposes the common subset over HTTP.
List memories (optionally scoped to a subject).
Returns: {"results": [...]}, each item the same _public shape as get() (id, memory, user_id, metadata.semantic_type).
Example:
all_memories = mem.get_all(user_id="u1", limit=50)
context
# Memory (in-process)
def context(
query: str,
*,
user_id: Optional[str] = None,
scope: Optional[Any] = None,
as_of: Optional[float] = None,
consistency_mode: str = "eventual",
context_mode: str = "auto",
token_budget: int = 800,
candidate_k: int = 12,
rerank_window: Optional[int] = None,
include_raw_items: bool = False,
include_sources: bool = False,
now: Optional[float] = None
) -> dict
RemoteMemory.context(...) takes the same parameters except now.
Retrieve a context pack: a ranked, token-budgeted slice of memories for a query (RAG-like).
Parameters:
query: The context prompt.user_id: Scope to this subject (optional).scope: Scope metadata (optional).as_of: Retrieve as of a timestamp (optional).consistency_mode:"eventual"(default) or"session".context_mode:"auto"(default) or another strategy.token_budget: Target token count; default800.candidate_k: Initial candidate pool size; default12.rerank_window: Optional rerank window size.include_raw_items: Include raw retrieved items; defaultFalse.include_sources: Include source metadata; defaultFalse.now: Optional clock override (in-process only).
Returns: A context pack dict (the same shape the HTTP /v1/context route returns).
When a receipt signer is configured, the returned context pack also includes a signed trace_receipt
envelope. The signed body schema is shomei.context_trace.v1 and the body is content-free: it carries
tenant_id_hash, keyed subject_pseudonym (or null for an anonymous query), keyed
query_commitment, served_set_witness, omissions_commitment, created_at, nonce, and
proof_scope. The receipt attests integrity of the served plus withheld set as of the query; it is not
a claim that retrieval was optimal or that an answer was correct. Before signing, every candidate artifact
is accounted for as either served or omitted.
The keyed query_commitment is carried only in the signed receipt returned to the caller. Shomei's durable
context-trace row retains neither that value nor a caller-derived scope commitment; it stores a structural
unretained_<trace_id> marker (or an erased_<rowid> tombstone) and a scope-bound boolean instead.
Verify a trace receipt offline with the open
shomei_memory_verify package:
from shomei_memory_verify import verify_context_trace_receipt
result = verify_context_trace_receipt(
ctx["trace_receipt"],
served_ids=observed_served_ids,
expected_public_key_hex=pin,
)
assert result.authenticated and result.served_set_matches
observed_served_ids is the opaque id set you received in the context pack, including served items and
any citation, recent-change, conflict, or pending ids you rely on. If you also retained the debugger trace's
omission list, pass it as omissions=. Tampering with a served id or omission causes verification to fail.
Example:
ctx = mem.context("What are Dana's preferences?", user_id="u1")
history
# Memory (in-process)
def history(memory_id: str, *, now: Optional[float] = None) -> list
RemoteMemory.history(memory_id) returns the same list over HTTP.
Retrieve the lifecycle history of a memory.
Returns: A list of history entries (each a dict with lifecycle metadata).
Example:
events = mem.history("lm_abc123")
for event in events:
print(event)
debug_trace
# Memory (in-process)
def debug_trace(trace_id: str) -> Optional[dict]
RemoteMemory.debug_trace(trace_id) is the HTTP equivalent (this route requires an admin-scoped key).
Retrieve a context pack by trace ID (for debugging a retrieval).
Returns: The context pack, or None if not found.
graph_answers (RemoteMemory)
# RemoteMemory (HTTP)
def graph_answers(
*,
subject: Optional[str] = None,
predicate: Optional[str] = None,
user_id: Optional[str] = None,
as_of: Optional[float] = None,
include_history: bool = False,
limit: int = 50,
cursor: Optional[str] = None
) -> dict
Deterministic Temporal Answers over admitted temporal graph edges (POST /v1/graph/answers). Read-only and idempotent: no model, embedder, reranker, or LLM runs on the serve path, and every served value resolves to a literal source quote/span/source id.
Flag-gated and dark by default. The route exists only when the deployment sets
SHOMEI_GRAPH_ANSWERS_API=1. Against a deployment without the flag it returns the same404 {"error": "no such route"}as an unknown path, which this client surfaces asRemoteMemoryError: HTTP 404— so a 404 usually means "not enabled here", not "bad request". A403means the route is enabled but the tenant's plan lacks thegraph_answersfeature.
Returns: the shomei.graph_answers_response.v1 shape (groups, per-group answer_status / current_answer / candidates / trace, next_cursor, function_versions) — see the HTTP API reference for the full field list.
Governance Operations
verify
# Memory (in-process)
def verify(memory_id: str, *, horizon: Optional[float] = None, now: Optional[float] = None) -> dict
RemoteMemory.verify(memory_id) is the HTTP equivalent.
Return the operator-reported lifecycle/governance status (NOT an independent proof).
Returns:
{
"valid": True,
"retrieval_safe": True,
"embedding_retired": False,
"physically_purged": False,
"failures": []
}
For a hidden or absent memory, returns {"valid": None, "reason": "not_found"}.
Important: This is the server's self-reported verdict, not a cryptographic proof. For independent pinned-key verification, use verify_receipt() on a signed receipt from delete(), forget(), or export().
Example:
status = mem.verify("lm_abc123")
print(status["valid"], status["retrieval_safe"], status["physically_purged"])
delete
# Memory (in-process)
def delete(memory_id: str, *, now: Optional[float] = None) -> dict
RemoteMemory.delete(memory_id) is the HTTP equivalent.
Crypto-erase a memory (governed deletion). Emits a signed deletion receipt.
Returns (success):
{
"message": "...deleted (governed crypto-erasure)",
"deleted": True,
"receipt": {...},
"receipt_hash": "sha256:...",
"key_custody": "...",
"retirement_hashes": [...],
"signed_receipt": {...} # present when a signer is configured
}
Returns (under legal hold): {"message": "...under legal hold; delete blocked", "deleted": False, "held": True} — not an error; governance working as intended.
Returns (not found / already erased): {"message": "...not found", "deleted": False}.
Over HTTP,
RemoteMemory.delete()maps these to 200 (erased), 423 (held), and 404 (not found). The in-processMemory.delete()returns the dict directly with no status code.
Returns (external provider cleanup pending): HTTP 503 with a structured body containing
deleted: false, local_crypto_erased: true, external_delete_pending: true, retryable: true,
and the original transition_id. RemoteMemory.delete() returns this structured body so the caller
can retry the same id; it raises RemoteMemoryError for an unrelated/unstructured 503. This is local
crypto-erasure, not successful global deletion.
Example:
out = mem.delete("lm_abc123")
if out.get("deleted"):
receipt = out["signed_receipt"]
# Pin the public key out-of-band; verify offline:
result = RemoteMemory.verify_receipt(receipt, expected_public_key_hex=pin)
assert result.authenticated
hold
# Memory (in-process)
def hold(memory_id: str, *, now: Optional[float] = None) -> dict
RemoteMemory.hold(memory_id) is the HTTP equivalent (POST /v1/memories/{id}/hold).
Place a legal hold on a memory: preserve but allow use (e.g., litigation). Legal hold is distinct from an Art.18 restriction — that distinction is load-bearing.
Returns:
{
"memory_id": "lm_...",
"held": True, # or False if not found / not holdable
"note": "legal hold: preserved + readable; erase/update/delete blocked until release"
}
Semantics: A held memory remains readable (search, get, context) but deletes and updates are blocked. If a forget() is attempted while held, it records a deferred erasure obligation that auto-fires on release().
Example:
mem.hold("lm_abc123")
release
# Memory (in-process)
def release(memory_id: str, *, now: Optional[float] = None) -> dict
RemoteMemory.release(memory_id) is the HTTP equivalent (DELETE /v1/memories/{id}/hold).
Release a legal hold. Any deferred erasure obligations fire immediately.
Returns:
{
"memory_id": "lm_...",
"released": True, # or False if nothing changed
"obligations_fired": [...] # present only if deferred erasures ran
}
Example:
mem.release("lm_abc123")
restrict
# Memory (in-process)
def restrict(
memory_id: str,
*,
reason: str = "art18_request",
now: Optional[float] = None
) -> dict
RemoteMemory.restrict(memory_id, *, reason="art18_request") is the HTTP equivalent (POST /v1/memories/{id}/restrict; requires an admin-scoped key — a data-plane key gets 403).
Place an Art.18 processing restriction: exclude from recall (but preserve for audit/SAR).
Parameters:
memory_id: The public logical ID.reason: The restriction reason (default"art18_request"); must be one of the restriction reason codes or it raisesValueError(over HTTP:400).now: Optional clock override (in-process only).
Returns (in-process Memory):
{
"memory_id": "lm_...",
"restricted": True,
"receipt_hash": "sha256:...",
"signed_receipt": {...},
"note": "Art.18 restriction: out of recall, preserved + reversible; still available for audit / subject-access / legal claims"
}
Returns (RemoteMemory): the service shape — the two surfaces differ here:
{
"id": "lm_...",
"restricted": True,
"changed": True,
"receipt": {...}, # present when the state actually changed
"receipt_hash": "sha256:...",
"signed_receipt": {...} # present when changed and a signer is configured
}
The reason_code, applied_at, and restricted_state live inside the signed receipt body, not at the top level of either response.
Semantics: Restricted memories are excluded from search/get/context but still available for subject-access exports and legal claims (non-destructive).
Example:
mem.restrict("lm_abc123", reason="objection_pending")
release_restriction
# Memory (in-process)
def release_restriction(memory_id: str, *, now: Optional[float] = None) -> dict
RemoteMemory.release_restriction(memory_id) is the HTTP equivalent (DELETE /v1/memories/{id}/restrict; admin-scoped).
Lift an Art.18 restriction; the memory returns to normal recall. The engine hardcodes the lift reason (art18_lifted).
Returns (in-process Memory):
{
"memory_id": "lm_...",
"restriction_lifted": True,
"receipt_hash": "sha256:...",
"signed_receipt": {...}
}
Returns (RemoteMemory): the service shape ({"id", "restricted": False, "changed", ...}). When the restriction cannot be lifted (e.g. withdrawn consent or a pending erasure obligation) the response carries changed: false plus a reason and a note — governance working, not an error.
Example:
mem.release_restriction("lm_abc123")
Inference-Evidence Operations
These methods append evidence to inference handles (e.g., src_..., clm_..., evt_..., stv_...). They exist on both Memory (with a now= override) and RemoteMemory (without). The RemoteMemory signatures shown below omit now.
confirm
def confirm(handle: str, *, actor_id: Optional[str] = None) -> dict
Append confirmation evidence to an inferred item.
correct
def correct(
handle: str,
correction_text: str,
*,
user_id: Optional[str] = None,
actor_id: Optional[str] = None
) -> dict
Append a correction to an inference.
dispute
def dispute(handle: str, *, reason: str = "user_dispute", actor_id: Optional[str] = None) -> dict
Dispute an inferred item.
forget_handle
def forget_handle(handle: str, *, actor_id: Optional[str] = None) -> dict
Erase an inference record (e.g., a claimed preference that is wrong).
For an externally stored vector, local inference erasure may complete before provider absence is
confirmed. In that case HTTP returns structured 503 with status: "external_delete_pending",
local_crypto_erased: true, retryable: true, and the retry transition_id/pending vector ids.
RemoteMemory.forget_handle() returns only that structured pending body; unrelated 503 responses
raise RemoteMemoryError. Retry the same handle until status: "accepted" and
external_vector_delete_confirmed: true.
mark_irrelevant
def mark_irrelevant(handle: str, *, reason: str = "irrelevant", actor_id: Optional[str] = None) -> dict
Mark an inference as out-of-date or irrelevant (non-destructive).
change_scope
def change_scope(handle: str, scope: Any, *, actor_id: Optional[str] = None) -> dict
Change inference scope metadata.
pin
def pin(handle: str, *, actor_id: Optional[str] = None) -> dict
Pin an inference (mark it persistent/high-confidence).
Each of these returns the publicized inference-response dict (public logical ids only).
make_temporary (retired)
def make_temporary(handle: str, ttl_or_until: Any, *, actor_id: Optional[str] = None) -> dict
Graded/temporary TTL has been retired. Present on both Memory and RemoteMemory so old callers fail honestly; it always returns:
{"status": "rejected", "retired": True, "reason": "temporary_ttl_retired"}
Export & DSR
forget
# Memory (in-process)
def forget(*, user_id: str, now: Optional[float] = None) -> dict
RemoteMemory.forget(*, user_id) is the HTTP equivalent (no now).
Data Subject Request (Art. 17) erasure: governed crypto-erase of every LIVE memory for a subject. The target set is durably journaled before the first erase and replayed on reopen after an interrupted multi-target run. This is crash-resumable local orchestration, not literal ACID with an external provider.
Parameters:
user_id: Subject identifier (required, must be a non-empty string).now: Optional clock override (in-process only).
Returns: a large object. The subject key is subject (not user_id), and per-memory proofs are in signed_receipts (a list) — there is no top-level singular signed_receipt. The subject-level proof is signed_closure_receipt, present only when both a signer and a commitment key are configured. Key fields:
{
"subject": "u1",
"erased_count": 2,
"deferred_count": 0,
"external_delete_pending_count": 0,
"external_delete_pending": [], # direct rows awaiting provider absence
"erased": ["lm_...", ...], # erased memory ids
"deferred_held": [...], # held memories (erasure deferred until release)
"source_deferred_count": 0,
"source_erased_count": 0,
"source_local_crypto_erased_count": 0,
"source_locally_erased": [],
"source_external_delete_pending_count": 0,
"source_external_delete_pending": [],
"source_external_vector_delete_confirmed": True,
"claim_erased_count": 0,
"event_erased_count": 0,
"state_erased_count": 0,
"trace_affected_count": 0,
"governance_dependency_count": 0,
"governance_dependency_ids": [...],
"governance_dependencies": [...],
"receipt_hashes": [{"id": "lm_...", "receipt_hash": "sha256:..."}, ...],
"signed_receipts": [{"id": "lm_...", "signed_receipt": {...}}, ...],
"restriction_receipts": [{"id": "lm_...", "signed_receipt": {...}}, ...],
"note": "...",
# closure block, present only when a signer AND commitment key are configured:
"closure_receipt": {...},
"closure_receipt_hash": "sha256:...",
"signed_closure_receipt": {...} # the subject-level erasure proof
}
Semantics: If a memory is under legal hold, it is restricted (Art.18, out of recall) immediately and a durable obligation auto-fires the crypto-erase when the hold releases. Held memories are reported under deferred_held/deferred_count — never left recallable.
For an external vector backend, a structured retryable result can be returned with
external_delete_pending_count and/or source_external_delete_pending_count nonzero. The affected local
rows are already crypto-erased, but provider absence is not confirmed, so those rows are not included in
the successful erased counts. RemoteMemory.forget() returns this structured HTTP 503 body as a dict;
unstructured 503 failures still raise RemoteMemoryError. Preserve each returned id / transition_id
and retry the indicated deletion.
An unresolved provider upsert operation also keeps the result pending even after a current exact-absence probe. This prevents a request accepted before a process death/timeout from landing after a false success. Shomei requires an exact point-presence read after upsert; an asynchronous enqueue acknowledgement alone does not clear uncertainty.
closure_receipt is a SubjectClosureReceipt body. It includes a per-forget() nonce, so repeated
forget attempts produce distinct signed bodies even when the closed set is unchanged.
Raises: TypeError or ValueError if user_id is not a non-empty string.
Example:
result = mem.forget(user_id="u1")
for mid in result["erased"]:
print(f"Erased: {mid}")
# The subject-level proof (when configured):
closure = result.get("signed_closure_receipt")
if closure:
verified = RemoteMemory.verify_receipt(closure, expected_public_key_hex=pin)
export
# Memory (in-process)
def export(
*,
user_id: str,
include_content: bool = False,
include_erased: bool = True,
include_restricted: bool = False,
now: Optional[float] = None
) -> dict
RemoteMemory.export(*, user_id, include_content=False, include_restricted=False) is the HTTP equivalent — it does not expose include_erased or now (those are in-process only).
Data Subject Request (Art. 15) access export: a portable, signed evidence bundle for a subject.
Parameters:
user_id: Subject identifier (required, must be a non-empty string).include_content: IfTrue, include the governed content rendering; defaultFalse(content-free).include_erased: IfTrue, include erasure evidence; defaultTrue(in-process only).include_restricted: Art.15 subject-access mode. It only renders restricted-but-retained content when combined withinclude_content=True; crypto-erased content remains unreturnable.now: Optional clock override (in-process only).
Returns: The subject key is subject (not user_id). The bundle now also discloses the subject's derived-artifact set (the Art.15 mirror of the Art.17 deletion cascade). Key fields:
{
"subject": "u1",
"count": 3, # live governed memories
"content_included": False,
"restricted_content_included": False,
"memories": [ # each row carries a `recallable` flag (= not restricted)
{"id": "lm_...", "semantic_type": "GENERIC", "held": False, "restricted": False,
"recallable": True, "consent_basis": "...", "verify": {...}}
],
"erased_count": 1,
"erased": [...], # content-free erasure evidence rows
"inference": [...],
"inference_count": 0,
"inference_restricted_retained": [...],
"inference_restricted_retained_count": 0,
"derived": [ # content-free opaque derived-artifact ids
{"id": "artifact:<hex>", "status": "active", "recallable": True}
],
"derived_count": 1,
"live_count": 4, # = len(memories) + inference records + derived ids (INCLUDES derived)
"note": "...",
# present when a commitment key is configured:
"receipt": {...},
"receipt_hash": "sha256:...",
"signed_receipt": {...} # present when a signer is also configured
}
Derived-artifact disclosure: derived is a list of content-free opaque artifact:<hex> ids. The recallable flag is honest per node — True only if at least one supporting source is still served, so it does not overclaim for nodes whose source was restricted. The derived ids are bound into the signed witness under set-schema shomei.subject_export_set.v2 (the signed receipt body schema is still shomei.subject_export.v1), so access-completeness is attested the same way deletion-completeness is.
Art.15 access mode: default export is an evidence bundle and stays content-free. include_content=True
adds content for admin-authorized exports. include_restricted=True is the subject-access override for
restricted-but-retained rows: with include_content=True, restricted rows can include their content while
their recallable flag remains False; without that access mode, the rows remain disclosed but content is
omitted. Erased content stays gone.
Honest scope: the signed witness proves the delivered set is untampered as of exported_at. It does not prove the operator's enumeration was complete against a dishonest operator — completeness-against-a-dishonest-operator is the measured-enclave (Tier-3) roadmap, not shipped today.
Raises: TypeError or ValueError if user_id is not a non-empty string.
Example:
bundle = mem.export(user_id="u1", include_content=True)
# Verify the bundle (when a signer is configured):
receipt = bundle.get("signed_receipt")
if receipt:
verified = RemoteMemory.verify_receipt(receipt, expected_public_key_hex=pin)
Admin & Ops sub-client (RemoteMemory.admin)
RemoteMemory(...).admin is a lazily-constructed RemoteAdmin bound to the same transport as the parent client — same bearer, same pinned TLS context, same fail-closed no-redirect handler (the admin bearer never follows a 3xx off the configured origin). It closes the gap where programmatic admin meant hand-rolled requests calls.
Every route it wraps requires an admin-scoped key; a data-plane key gets 403 {"error": "this action requires an admin-scoped key"} (raised as RemoteMemoryError). Managed-tier routes additionally require the deployment to run the managed control plane; without it the service answers 404 {"error": "managed control plane is not enabled"}.
m = RemoteMemory.from_env() # admin-scoped key
m.admin.health() # operational detail
m.admin.quota() # usage vs plan limits
new = m.admin.keys_issue(scope="data", label="mobile-app") # api_key shown ONCE
| Method | Route | Notes |
|---|---|---|
keys_list() |
GET /v1/admin/keys |
Key metadata only — never secret material |
keys_issue(*, scope="data", label=None, ttl_seconds=None) |
POST /v1/admin/keys/issue |
api_key in the response is shown once |
keys_rotate(lookup_id, *, grace_seconds=0, ttl_seconds=None) |
POST /v1/admin/keys/rotate |
New key shown once; 409 if not rotatable |
keys_revoke(lookup_id) |
POST /v1/admin/keys/revoke |
Terminal; effective on the next request |
projects_list() |
GET /v1/admin/projects |
Managed control plane only |
projects_create(name, *, environment="live") |
POST /v1/admin/projects |
Creates an isolated project tenant + reveal-once admin key |
projects_admin_key(project_id) |
POST /v1/admin/projects/admin-key |
Recover/rotate a project's admin key (old ones revoked) |
invites_create(email, *, role="compliance", invited_by=None) |
POST /v1/admin/invites |
Managed control plane only |
retention_policy_get() |
GET /v1/admin/retention-policy |
The explicit-delete retention contract |
retention_policy_set(profile) |
POST /v1/admin/retention-policy |
Controlled profile names only (400 otherwise) |
plan_get() |
GET /v1/admin/quota |
Honest note: no dedicated plan-read route exists — the resolved plan rides on the quota route (plan field) |
plan_bind(plan_id, *, overrides=None) |
POST /v1/admin/plan |
Managed-tier quota enforcement only; enterprise plans are platform-assigned (403) |
quota() |
GET /v1/admin/quota |
{"plan", "enforced", "quotas": [{"name", "used", "limit", "percent"}]} |
health() |
GET /v1/admin/health |
Authenticated operational detail (custody tier, readiness) |
metrics_summary() |
GET /v1/admin/metrics/summary |
Content-free request/erasure counts |
erase_tenant(*, confirm) |
POST /v1/admin/erase-tenant |
Destructive — see below |
erase_tenant (destructive, fail-closed)
def erase_tenant(*, confirm: str) -> dict
Crypto-erase the entire tenant (all data + KEK) and return a durable signed closure receipt (schema: shomei.tenant_closure.v1). Not reversible.
Fail-closed ergonomics: confirm must be the exact tenant id this API key is bound to. The client first resolves the live tenant via GET /v1/account/signer-key and refuses without sending the erase request when confirm is missing, empty, or does not match — so a mistyped call, or a client pointed at the wrong deployment, cannot erase anything. If the tenant is already erased (the signer-key probe answers 410), the erase is an idempotent re-fetch of the persisted closure receipt and proceeds — nothing is left to destroy.
out = m.admin.erase_tenant(confirm="acme") # must match the key's tenant
assert out["erased"] is True
receipt = out["signed_receipt"] # pin-verifiable offline, like every receipt
Verification & Utility
signer_public_key (Memory, in-process)
def signer_public_key() -> Optional[dict]
The in-process signer identity an auditor pins to verify your signed receipts.
Returns: None if unsigned, otherwise:
{"signer_id": "shomei-receipt:...", "public_key_hex": "..."}
signer_key (RemoteMemory, HTTP)
def signer_key() -> dict
Fetch this tenant's receipt signer public key from the service (GET /v1/account/signer-key).
Returns:
{
"signer_public_key_hex": "...",
"public_key_hex": "..." # client-side alias for easier quickstart
}
Note: Production callers should pin this key out-of-band (e.g., via your certificate infrastructure), not fetch it on every request.
Example:
key_info = m.signer_key()
pin = key_info["public_key_hex"]
verify_receipt (static, RemoteMemory)
@staticmethod
RemoteMemory.verify_receipt(
signed_receipt: Dict[str, Any],
*,
expected_public_key_hex: str
) -> VerifyResult
Independent, client-side verification of a signed receipt (the pinned-key verification path). Delegates to shomei_memory_verify.verify_signed_receipt.
Parameters:
signed_receipt: The receipt dict fromdelete(),forget()(itssigned_receipts/signed_closure_receipt), orexport().expected_public_key_hex: The ed25519 public key (hex-encoded) you pinned out-of-band.
Returns: A VerifyResult (see below).
Raises: RemoteMemoryError if the shomei_memory_verify package is not installed.
Example:
# Pin the key out-of-band in production (e.g., from your certificate store).
pin = m.signer_key()["public_key_hex"]
# After deletion:
out = m.delete("lm_abc123")
receipt = out["signed_receipt"]
# Verify locally, with no service call:
result = RemoteMemory.verify_receipt(receipt, expected_public_key_hex=pin)
if result.authenticated:
print("Deletion verified by authorship.")
VerifyResult
A frozen dataclass returned by verify_receipt. Full field list:
VerifyResult(
valid: bool, # well-formed + ed25519 signature consistent over the body + hash matches
authenticated: bool, # AND verified against the caller-PINNED key — this is the authentication gate
reason: str, # 'ok' | 'ok_unpinned' | 'public_key_mismatch'
# | 'signature_or_hash_invalid' | 'malformed: <detail>'
public_key_hex: str = "", # the authenticated signer identity (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 = {}
)
valid and authenticated are distinct: valid means self-consistent (signature + hash check out over the body); authenticated means valid and the signature verified against a pinned key. bool(result) is authentication-gated — it is truthy only when authenticated, so if verify_receipt(r, expected_public_key_hex=pin): cannot pass on a receipt signed by an unpinned (attacker-chosen) key. Base receipt decisions on authenticated, not valid alone.
Profiles
Three shipped profiles control governance behavior:
support_copilot
General privacy posture for customer-support agents. NOT a named regulation.
- Mode:
privacy(sensitive ceilings enforced). - Retention: Explicit sharp deletion/forget, no automatic graded decay.
- Compliance pack: None.
Honest note: This is general privacy posture, not "HIPAA-ready" or "PCI-ready". For a specific regulation, use regulated_agent.
enterprise_assistant
Relevance-first retention for work context (e.g., employee-facing assistants).
- Mode:
relevance(sensitive types still have ceilings). - Retention: Explicit sharp deletion, no automatic graded decay.
- Compliance pack: None.
Honest note: Attach a compliance pack via regulated_agent if a regulation governs the data.
regulated_agent
Healthcare/finance/insurance: strict ceilings + a named compliance pack.
- Mode:
privacy. - Retention: Per-type schedules from the pack.
- Compliance pack: Required; one of
"hipaa","pci","finance".
Honest note: A pack is a starting configuration, never "verified compliance." The operator owns the legal determination.
Environment Variable Reference
Memory (in-process)
| Variable | Default | Notes |
|---|---|---|
SHOMEI_TENANT_ID |
— | Required |
SHOMEI_MEMORY_DB |
./shomei-memory.db |
SQLite path; :memory: for ephemeral |
SHOMEI_PROFILE |
support_copilot |
support_copilot, enterprise_assistant, regulated_agent |
SHOMEI_REGULATION |
— | Required iff profile is regulated_agent; one of hipaa, pci, finance |
SHOMEI_KMS_PROVIDER |
local-dev |
Only local-dev is wired; other names raise ValueError unless a real provider is injected |
SHOMEI_ALLOW_DEV_CUSTODY |
— | Set to 1, true, yes, or on to allow local-dev custody (dev only) |
SHOMEI_EMBEDDER |
hash |
hash (non-semantic, default), bedrock, ollama |
SHOMEI_EMBED_MODEL |
— | Model name for bedrock/ollama |
SHOMEI_EMBED_DIMS |
1024 |
Embedding dimensions |
SHOMEI_EMBEDDER_STRICT |
1 |
1 for strict mode (fail on missing model); 0 for degraded-but-available fallback |
SHOMEI_AWS_REGION |
us-east-1 |
AWS region for bedrock |
Each variable above also accepts the legacy MOCHI_<name> form as a fallback; SHOMEI_<name> wins when both are set.
RemoteMemory (HTTP client)
| Variable | Default | Notes |
|---|---|---|
SHOMEI_BASE_URL |
— | Service root URL (required) |
SHOMEI_API_KEY |
— | Bearer token (required) |
SHOMEI_CA_CERT |
— | Path to CA cert to pin (for self-signed certs) |
SHOMEI_TLS_INSECURE |
— | Set to 1 to skip TLS verification (demo only) |
Legacy MOCHI_<name> fallbacks apply here too.
Type Notes
- Memory IDs: Public logical IDs (e.g.,
"lm_..."), opaque strings. - Inference handles: Special IDs for inferred records (e.g.,
"src_...","clm_...","evt_...","stv_..."). - Subject IDs: String user identifiers; non-string types are rejected at write time to prevent silent coercion.
- Governance receipts: Content-free, ed25519-signed envelopes (
schema,receipt_hash,signer_id,public_key_hex,signature_hex,body); verify offline viaRemoteMemory.verify_receipt(). A receipt proves authorship of the governance act — it does not prove that copies or backups held elsewhere were erased.
Error Handling
RemoteMemoryError
All RemoteMemory network errors raise RemoteMemoryError (a RuntimeError subclass):
try:
m.delete("lm_abc123")
except RemoteMemoryError as e:
print(f"HTTP error: {e}")
ValueError, TypeError
The in-process Memory class validates user_id:
- Non-string types raise
TypeError. - Empty strings, or
Nonewhere required (forget/export), raiseValueError.
Key Custody & Security
DEV Key Custody (Local-Dev)
By default, Memory.from_profile() and Memory.from_env() refuse to start without a real KMS/TPM/HSM-backed provider unless dev custody is explicitly opted in via:
allow_dev_key_custody=True(constructor), orSHOMEI_ALLOW_DEV_CUSTODY=1(env; legacyMOCHI_ALLOW_DEV_CUSTODYhonored).
Dev custody is NOT KMS, NOT TPM, NOT HSM, and NOT hardware-backed erasure. A local key file (0600) is used; crypto-erasure strength is bounded by how well deleting that file destroys it (best-effort overwrite-then-unlink; on SSD/FTL bytes may survive). Dev custody is dev-only — never run it in a production config.
For production: Always inject a real provider via kek_provider= and commitment_key= from your secret store. At rest, payload and embedding cells are sealed via per-row DEK envelope encryption — not SQLCipher page-encryption — and each tenant gets its own workspace KEK. Key custody is the proof boundary: an operator who never holds your keys cannot silently un-erase.
Independent Verification
For pinned-key receipt verification:
- Pin the signer's ed25519 public key out-of-band (e.g., from your CA / certificate infrastructure).
- After
delete(),forget(), orexport(), take thesigned_receipt(orsigned_closure_receipt/ a row fromsigned_receipts). - Call
RemoteMemory.verify_receipt(receipt, expected_public_key_hex=pin)locally, without contacting the service, and check.authenticated.
This is operator-verifiable (Tier-2) verification: the receipt proves authorship of the governance act under a key you pinned. A TEE-attested Tier-3 (verifiable against a dishonest operator, including completeness of enumeration) is on the roadmap, not shipped today.