You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add a persistent mcp_writes audit log table that records every successful MCP write tool invocation (memory.store, memory.note, tree.tag) so users can review what LLMs connected via MCP have written to their Memory Tree. Replaces the current tracing::info!-only audit trail with a queryable surface.
This closes out Q4 from the Phase 3 RFC (#2269) — the only one of the four RFC questions that doesn't have an implementation answer yet:
After #2306 / #2316 / #2332 landed, the MCP write surface is functionally complete but only emits ephemeral tracing::info! lines for write events. Quoting @graycyrus's #2306 review verbatim:
"The tracing::info! audit log exists but only shows in logs. A UI-side notification ... would give users visibility."
"The real residual risk isn't injection or corruption (that's well-covered), it's a rogue LLM silently filling memory with noise and the user not knowing."
Concrete user-impact gaps without a queryable audit log:
No accountability — a user can't answer "what did Claude Desktop write into my memory this week?" without grep-ing log files.
No compliance story — enterprise users with audit-trail requirements can't satisfy them with a tracing-only sink.
No abuse detection — a misbehaving client filling memory at the per-hour rate limit shows up only as log spam, not a queryable signal.
No undo path — even if a user spots a problem write in the log, there's no structured handle to address it.
Proposed solution
Add a new SQLite table next to the existing memory tree DB (same SQLite handle to avoid a second connection):
Per-tool slim JSON that captures the identifying args without duplicating the document content (which is already persisted via memory_doc_put):
Tool
args_summary shape
memory.store
{ "title": "<first 128 chars>", "namespace": "...", "tag_count": N }
memory.note
{ "chunk_id": "...", "note_text_length": N }
tree.tag
{ "chunk_id": "...", "tags": [...] }
args_summary deliberately avoids storing raw note/store content twice — the actual content lives in the memory tree itself; the audit table just records the metadata enough to identify and join.
Write flow
Inside dispatch_write_tool in src/openhuman/mcp_server/tools.rs:
Returns Vec<McpWriteRecord> ordered by timestamp_ms DESC.
Open design questions (need maintainer direction)
Q1 — Storage backend
A. Add the table to the existing memory tree SQLite (single handle, single migration, transactional with the write itself). Easiest deployment.
B. Separate mcp_audit.sqlite file (isolated; survives memory tree corruption / reset; can be wiped independently for privacy). Bit more plumbing.
C. Reuse one of the existing KV stores or event_bus persistence layers if there's a natural home I haven't found.
The implementation sketch assumes A, but mcp_writes is a write-heavy append-only log with very different access patterns from the chunk tree — splitting may be cleaner.
Q2 — Audit-write coupling on failure
A. Best-effort audit (write succeeds even if audit fails) — the sketch above. Audit failure is logged but not propagated. Pro: write availability never degrades; con: theoretical race where the chunk lands but the audit row doesn't, breaking the "every write is auditable" guarantee.
B. Transactional (audit-then-write inside one SQLite tx) — strict guarantee, but couples write latency to the audit table and complicates the memory_doc_put RPC boundary.
C. Write-then-audit, abort-on-audit-failure — between A and B; would require rolling back the just-completed write, which doc_put doesn't currently support cleanly.
Preference: A unless maintainers want the stronger guarantee for compliance reasons.
Q3 — Retention policy
A. No automatic prune (table grows forever). Simplest; trusts user disk space.
B. Rolling window — e.g. delete rows older than 90 days during a daily job (could piggyback on the existing memory tree scheduler).
C. Size-bounded — drop oldest rows when the table exceeds N MB.
D. Per-user opt-in retention setting (under config.mcp.audit_retention_days).
For v1, leaning A (no prune) with an explicit "retention is a follow-up" note — simpler scope, doesn't lock us into a policy before we see real usage volume.
Q4 — Query surface
A. Internal RPC only (openhuman.mcp_audit_list) — OpenHuman's own UI or CLI can read; MCP clients themselves cannot see their audit history. Tightest blast radius.
B. Also expose as an MCP tool (e.g. audit.list_writes) — lets a client like Claude Desktop reflect on its own writes, useful for "show me what you stored last session" UX. But also means an MCP client can see what other clients wrote to the same user's memory tree.
C. Hybrid — MCP tool exposes only the current client's own writes (filtered by client_info == session.source_type()), while the internal RPC sees all.
Strong preference for A in v1 (smallest surface, fewest privacy decisions). C is the right long-term shape; B is too permissive.
Acceptance criteria
mcp_writes table created via SQLite migration; existing users migrate cleanly on first launch after upgrade.
Every successful dispatch_write_tool call inserts a row with the correct client_info (from McpSession), tool_name, args_summary, and resulting_chunk_id.
Failed writes also recorded (success: 0, error_message populated) — abuse-detection signal.
args_summary does not duplicate the chunk content (only identifying metadata).
openhuman.mcp_audit_list RPC registered and returns records ordered by timestamp_ms DESC, with limit / offset / filter support.
Unit tests cover: insert success, insert failure, query with each filter, ordering, limit + offset.
Diff coverage ≥ 80%.
No new UI surface — that's the follow-up.
Out of scope (follow-ups)
UI surface for browsing the audit log — Channels tab? Settings panel? Dedicated "Memory Activity" view? Belongs to a separate issue once the data layer is in place. @graycyrus already flagged the UI notification angle on feat(mcp): add memory.store and memory.note write tools #2306; an audit list view is the natural companion.
Summary
Add a persistent
mcp_writesaudit log table that records every successful MCP write tool invocation (memory.store,memory.note,tree.tag) so users can review what LLMs connected via MCP have written to their Memory Tree. Replaces the currenttracing::info!-only audit trail with a queryable surface.This closes out Q4 from the Phase 3 RFC (#2269) — the only one of the four RFC questions that doesn't have an implementation answer yet:
doc_put(daemon-sole-writer)source_type = "mcp:<client>"(feat(mcp): add memory.store and memory.note write tools #2306 placeholder + YOMXXX's feat(mcp): capture client provenance in stdio sessions #2332clientInfo.namecapture)SecurityPolicy→ distinctenforce_write_policy()gating onToolOperation::Act(feat(mcp): add memory.store and memory.note write tools #2306)Problem
After #2306 / #2316 / #2332 landed, the MCP write surface is functionally complete but only emits ephemeral
tracing::info!lines for write events. Quoting @graycyrus's #2306 review verbatim:Concrete user-impact gaps without a queryable audit log:
grep-ing log files.Proposed solution
Add a new SQLite table next to the existing memory tree DB (same SQLite handle to avoid a second connection):
What goes into
args_summaryPer-tool slim JSON that captures the identifying args without duplicating the document content (which is already persisted via
memory_doc_put):args_summaryshapememory.store{ "title": "<first 128 chars>", "namespace": "...", "tag_count": N }memory.note{ "chunk_id": "...", "note_text_length": N }tree.tag{ "chunk_id": "...", "tags": [...] }args_summarydeliberately avoids storing raw note/store content twice — the actual content lives in the memory tree itself; the audit table just records the metadata enough to identify and join.Write flow
Inside
dispatch_write_toolinsrc/openhuman/mcp_server/tools.rs:The audit insert uses
let _ = ... .await(best-effort) — see Q2 below.Query surface
New RPC:
openhuman.mcp_audit_list(read-only):Returns
Vec<McpWriteRecord>ordered bytimestamp_ms DESC.Open design questions (need maintainer direction)
Q1 — Storage backend
A. Add the table to the existing memory tree SQLite (single handle, single migration, transactional with the write itself). Easiest deployment.
B. Separate
mcp_audit.sqlitefile (isolated; survives memory tree corruption / reset; can be wiped independently for privacy). Bit more plumbing.C. Reuse one of the existing KV stores or event_bus persistence layers if there's a natural home I haven't found.
The implementation sketch assumes A, but
mcp_writesis a write-heavy append-only log with very different access patterns from the chunk tree — splitting may be cleaner.Q2 — Audit-write coupling on failure
A. Best-effort audit (write succeeds even if audit fails) — the sketch above. Audit failure is logged but not propagated. Pro: write availability never degrades; con: theoretical race where the chunk lands but the audit row doesn't, breaking the "every write is auditable" guarantee.
B. Transactional (audit-then-write inside one SQLite tx) — strict guarantee, but couples write latency to the audit table and complicates the
memory_doc_putRPC boundary.C. Write-then-audit, abort-on-audit-failure — between A and B; would require rolling back the just-completed write, which
doc_putdoesn't currently support cleanly.Preference: A unless maintainers want the stronger guarantee for compliance reasons.
Q3 — Retention policy
A. No automatic prune (table grows forever). Simplest; trusts user disk space.
B. Rolling window — e.g. delete rows older than 90 days during a daily job (could piggyback on the existing memory tree scheduler).
C. Size-bounded — drop oldest rows when the table exceeds N MB.
D. Per-user opt-in retention setting (under
config.mcp.audit_retention_days).For v1, leaning A (no prune) with an explicit "retention is a follow-up" note — simpler scope, doesn't lock us into a policy before we see real usage volume.
Q4 — Query surface
A. Internal RPC only (
openhuman.mcp_audit_list) — OpenHuman's own UI or CLI can read; MCP clients themselves cannot see their audit history. Tightest blast radius.B. Also expose as an MCP tool (e.g.
audit.list_writes) — lets a client like Claude Desktop reflect on its own writes, useful for "show me what you stored last session" UX. But also means an MCP client can see what other clients wrote to the same user's memory tree.C. Hybrid — MCP tool exposes only the current client's own writes (filtered by
client_info == session.source_type()), while the internal RPC sees all.Strong preference for A in v1 (smallest surface, fewest privacy decisions). C is the right long-term shape; B is too permissive.
Acceptance criteria
mcp_writestable created via SQLite migration; existing users migrate cleanly on first launch after upgrade.dispatch_write_toolcall inserts a row with the correctclient_info(fromMcpSession),tool_name,args_summary, andresulting_chunk_id.success: 0,error_messagepopulated) — abuse-detection signal.args_summarydoes not duplicate the chunk content (only identifying metadata).openhuman.mcp_audit_listRPC registered and returns records ordered bytimestamp_ms DESC, withlimit/offset/ filter support.Out of scope (follow-ups)
memory.search,tree.read_chunk, etc.) — debatable whether reads need audit; defer until we see a request.Related
memory.store/memory.note) — answered Q1, Q2 placeholder, Q3.tree.tag) — third write tool; same audit hook applies uniformly.clientInfo.namecapture) — provides theclient_infofield this audit table records.