Skip to content

[codex] Add MCP write audit log#2566

Merged
senamakel merged 8 commits into
tinyhumansai:mainfrom
MackJack023:codex/issue-2536-mcp-write-audit
May 25, 2026
Merged

[codex] Add MCP write audit log#2566
senamakel merged 8 commits into
tinyhumansai:mainfrom
MackJack023:codex/issue-2536-mcp-write-audit

Conversation

@MackJack023
Copy link
Copy Markdown
Contributor

@MackJack023 MackJack023 commented May 24, 2026

Summary

  • add a persistent mcp_writes table to the memory-tree SQLite schema
  • record successful and failed MCP write tool calls from memory.store, memory.note, and tree.tag
  • add internal-only openhuman.mcp_audit_list RPC with limit, offset, since, client, tool, and success filters
  • thread McpSession::source_type() into MCP write dispatch so audit rows and write provenance use the connected client label

Closes #2536.

Validation

  • cargo test mcp_audit --lib
  • cargo test summarize_write_args --lib
  • cargo test extract_document_id --lib
  • cargo test schema_for_rpc_method_finds_internal_mcp_audit_list --lib
  • cargo test rpc_method_from_parts_does_not_expose_internal_mcp_audit_list --lib
  • pnpm format:check
  • pnpm rust:check
  • pnpm typecheck
  • pnpm lint (passes with existing warnings)
  • pnpm --dir app run lint:commands-tokens
  • git diff --check

Summary by CodeRabbit

  • New Features

    • Comprehensive MCP audit logging for write operations (timestamps, caller identity, redacted args summary, results/errors)
    • Queryable, filterable, paginated audit retrieval with newest-first ordering and safety limits
    • Audit inspection exposed as an internal-only RPC endpoint (registered on dispatch path but not publicly discoverable)
    • Tool calls now propagate session/client origin so audits capture caller provenance
  • Tests

    • Added coverage for storage, querying, redaction, ordering, pagination, and internal RPC wiring

Review Change Stack

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 24, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c0e0f58e-8043-4bc1-b39a-47ad5859ce36

📥 Commits

Reviewing files that changed from the base of the PR and between 52dfff4 and fda912a.

📒 Files selected for processing (1)
  • src/openhuman/mcp_audit/schemas.rs

📝 Walkthrough

Walkthrough

Adds persistent MCP write audit logging: new record types, SQLite storage and query API, an internal-only RPC mcp_audit.list, module wiring, and tool-dispatch changes to thread client provenance and record write outcomes.

Changes

MCP Audit Logging System

Layer / File(s) Summary
Audit Record Data Schema
src/openhuman/mcp_audit/types.rs
Defines NewMcpWriteRecord, McpWriteRecord, and McpWriteListQuery types with fields for timestamp, client/tool metadata, redacted arguments, outcome, and optional filters.
Audit Persistence Layer
src/openhuman/mcp_audit/store.rs
Implements record_write to insert audit rows into SQLite mcp_writes table and list_writes to query with optional filters for client, tool, timestamp, and success status; includes normalization helpers, UTF‑8-safe truncation, and unit tests.
RPC Handler & Query Endpoint
src/openhuman/mcp_audit/schemas.rs
Defines the internal-only mcp_audit.list controller schema with pagination/filter inputs, implements handle_list to deserialize params and call store::list_writes, and includes unit/integration tests for registration and response content.
Module Organization & Controller Registration
src/openhuman/mcp_audit/mod.rs, src/openhuman/mod.rs, src/core/all.rs, src/core/all_tests.rs
Adds the mcp_audit module with re-exports of store/types and schema lists, exposes the module in the crate index, registers internal controllers in the internal-only registry, and adds tests ensuring the controller is internal-only.
Tool Dispatch Integration & Audit Recording
src/openhuman/mcp_server/protocol.rs, src/openhuman/mcp_server/tools.rs
Threads session.source_type()/client_info into call_tool, updates call_tool/dispatch_write_tool to load config, enforce write policy, validate/inject params, extract document ids, and record success/failure NewMcpWriteRecord entries with redacted/summarized args; includes tests for redaction, denial auditing, and client provenance.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Protocol as MCP Protocol
  participant CallTool as call_tool()
  participant DispatchWrite as dispatch_write_tool()
  participant MemPut as openhuman.memory_doc_put
  participant AuditStore as Audit Store

  Client->>Protocol: tools/call (write tool)
  Protocol->>CallTool: call_tool(name, args, client_info)
  CallTool->>CallTool: load_write_config()
  CallTool->>CallTool: enforce_write_policy_for_config()
  CallTool->>CallTool: inject source_type = client_info
  CallTool->>DispatchWrite: dispatch_write_tool(audit_args, client_info, config)
  DispatchWrite->>MemPut: invoke mapped RPC (e.g., memory_doc_put)
  MemPut-->>DispatchWrite: response (success/failure)
  DispatchWrite->>DispatchWrite: extract_document_id()
  DispatchWrite->>AuditStore: record_write(NewMcpWriteRecord)
  AuditStore-->>DispatchWrite: row id
  DispatchWrite-->>CallTool: result
  CallTool-->>Protocol: JSON response
  Protocol-->>Client: tools/call result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • tinyhumansai/openhuman#2306: Modifies src/openhuman/mcp_server/tools.rs in the write-tool dispatch path—related to the audit dispatch wiring in this PR.
  • tinyhumansai/openhuman#2332: Threads session provenance into tools::call/call_tool, which this PR consumes for audit provenance.
  • tinyhumansai/openhuman#2316: Also modifies mcp_server/tools.rs around call_tool/dispatch write behavior and is code-adjacent to these changes.

Suggested reviewers

  • senamakel

Poem

🐰 I nibble bytes and store the tale,

timestamps, client, and tool set sail.
Quiet rows in SQLite keep,
Who wrote what while others sleep.
A tiny audit hop—then back to the dale.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 49.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title '[codex] Add MCP write audit log' clearly and concisely summarizes the main change: introduction of an MCP audit logging feature.
Linked Issues check ✅ Passed The pull request implements all primary coding objectives from #2536: persistent mcp_writes table schema [#2536], record insertion for write tools with args_summary [#2536], read-only RPC openhuman.mcp_audit_list with filters [#2536], client_info threading via source_type [#2536], and unit tests for insertion/filtering/ordering [#2536].
Out of Scope Changes check ✅ Passed All changes are directly related to MCP write audit logging. The modifications to protocol.rs and tools.rs thread session source_type into dispatch, store.rs/types.rs/schemas.rs implement the audit infrastructure, and core/all.rs wires the internal RPC—all in scope per #2536.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@MackJack023 MackJack023 marked this pull request as ready for review May 24, 2026 13:27
@MackJack023 MackJack023 requested a review from a team May 24, 2026 13:27
@qodo-code-review
Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@MackJack023 MackJack023 marked this pull request as draft May 24, 2026 13:27
@coderabbitai coderabbitai Bot added feature Net-new user-facing capability or product behavior. rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure. working A PR that is being worked on by the team. labels May 24, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/openhuman/mcp_server/tools.rs (2)

1161-1177: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Complete the write-path diagnostics and keep them at debug/trace.

The config-load failure is logged, but a policy denial in Lines 1173-1176 returns without any breadcrumb, and the per-request write diagnostics in Lines 1191-1218 are emitted at info. Please add a denial-branch log and downgrade these request/success logs to debug!/trace!.

As per coding guidelines: "Debug logging must follow these rules: default to verbose diagnostics on new/changed flows; log entry/exit, branches, external calls, retries/timeouts, state transitions, and errors" and "Use log / tracing at debug or trace level in Rust for development-oriented diagnostics."

Also applies to: 1191-1218

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/mcp_server/tools.rs` around lines 1161 - 1177, Add missing
denial breadcrumb and lower noisy info-level diagnostics to debug/trace: in
enforce_write_policy, after calling
SecurityPolicy::from_config(...).enforce_tool_operation(ToolOperation::Act,
tool_name), log a denial branch (use log::debug! or log::trace!) that includes
the tool_name and policy decision before returning ToolCallError::InvalidParams;
also change the per-request write diagnostics around the later write-path (the
info-level logs in the block referenced at lines ~1191-1218) to use log::debug!
for entry/exit and success messages and log::trace! for more verbose state/step
details so request and success breadcrumbs are recorded at development levels
rather than info.

548-588: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Audit the rejected write-tool paths too.

Line 559, Line 581, and Line 586 can all fail before dispatch_write_tool(...) runs, so malformed or policy-denied memory.store / memory.note / tree.tag calls never reach mcp_writes. That leaves the new audit trail incomplete for failed write invocations.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/mcp_server/tools.rs` around lines 548 - 588, The memory write
branch ("memory.store" | "memory.note" | "tree.tag") currently can return early
from reject_unexpected_arguments, enforce_write_policy, or
validate_controller_params and skip the mcp_writes audit; update call_tool so
that any error from reject_unexpected_arguments, enforce_write_policy, or
validate_controller_params triggers a call to the existing mcp_writes audit path
before returning the Err. Concretely, for the branch handling spec.name ==
"memory.store"|"memory.note"|"tree.tag", propagate errors from
reject_unexpected_arguments, enforce_write_policy, and
validate_controller_params via map_err or match handlers that first call
mcp_writes (including spec.name, audit_arguments, client_info, params snapshot
and the failure reason) and then return the original ToolCallError, ensuring
dispatch_write_tool remains the path for successful calls.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/openhuman/mcp_audit/mod.rs`:
- Around line 10-12: Add the two missing domain-level re-exports: export
schemas::all_controller_schemas under the alias all_mcp_audit_controller_schemas
and export controllers::all_registered_controllers under the alias
all_mcp_audit_registered_controllers so the module satisfies the domain-module
contract; i.e., add pub use schemas::all_controller_schemas as
all_mcp_audit_controller_schemas and pub use
controllers::all_registered_controllers as all_mcp_audit_registered_controllers
alongside the existing pub use lines (refer to the existing pub use
schemas::all_internal_controllers, store::list_writes/record_write, and
types::{McpWriteListQuery, McpWriteRecord, NewMcpWriteRecord} to place them
consistently).

In `@src/openhuman/mcp_audit/schemas.rs`:
- Around line 69-76: Add diagnostic logging to the handle_list RPC handler: log
an entry message at the start of handle_list, trace the result of
config_rpc::load_config_with_timeout (success or error), log a branch message
for serde_json::from_value parsing (include the raw params on entry and error
details on failure), emit a trace before calling store::list_writes and another
on its success with record counts or returned info, and ensure all error paths
convert/return errors after logging contextual error messages (include function
name, query, and config identifiers where available). Use the existing logging
facility consistent with other handlers so logs record entry/exit, branches,
external call boundaries, and error context for handle_list,
config_rpc::load_config_with_timeout,
serde_json::from_value::<McpWriteListQuery>, and store::list_writes.
- Around line 10-67: The module only exports all_internal_controllers but must
also expose the domain schema symbols expected by the contract: add and export
three items named schemas, all_controller_schemas, and
all_registered_controllers; implement schemas as a Vec<ControllerSchema> by
returning vec![schema()], implement all_controller_schemas as an alias or
wrapper returning the same vec (vec![schema()]) and implement
all_registered_controllers to return the existing all_internal_controllers() (or
forward to it), ensuring the names exactly match schemas,
all_controller_schemas, and all_registered_controllers so callers can find them.

In `@src/openhuman/mcp_audit/store.rs`:
- Around line 13-93: The record_write and list_writes functions lack the
required diagnostic logging; add debug/trace logs with a stable prefix like
"[mcp_audit]" at function entry and exit, log key correlation fields
(client_info, tool_name, timestamp_ms, and pagination/filter params such as
since_ms, limit, offset, client_filter, tool_filter, success_only) and include
contextual error details on DB failures (e.g., when serialize, prepare, execute,
query_map, or collect fail) while scrubbing PII (do not log full sensitive
args_summary or secret values); place logs inside record_write (around
serde_json::to_string and before/after the DB insert and on error) and inside
list_writes (log built SQL/filters applied, bound param summaries, before
executing stmt, after successful row count, and on any .context() error) using
the existing logging facility so the messages include the stable prefix and
correlation fields.

In `@src/openhuman/mcp_server/tools.rs`:
- Around line 1265-1268: The audit_write function currently performs
mcp_audit::record_write(...) inline and blocks the response path; change it to
fire-and-forget the audit call so it runs in the background. Update audit_write
to spawn a background task (use tokio::task::spawn_blocking if record_write is
synchronous, or tokio::spawn if you can call an async variant) and move or clone
the Config and NewMcpWriteRecord into that task; inside the task call
mcp_audit::record_write and log any Err via log::warn! so failures remain
best-effort and do not block the write response. Ensure captured variables meet
'static requirements for the spawned task.

---

Outside diff comments:
In `@src/openhuman/mcp_server/tools.rs`:
- Around line 1161-1177: Add missing denial breadcrumb and lower noisy
info-level diagnostics to debug/trace: in enforce_write_policy, after calling
SecurityPolicy::from_config(...).enforce_tool_operation(ToolOperation::Act,
tool_name), log a denial branch (use log::debug! or log::trace!) that includes
the tool_name and policy decision before returning ToolCallError::InvalidParams;
also change the per-request write diagnostics around the later write-path (the
info-level logs in the block referenced at lines ~1191-1218) to use log::debug!
for entry/exit and success messages and log::trace! for more verbose state/step
details so request and success breadcrumbs are recorded at development levels
rather than info.
- Around line 548-588: The memory write branch ("memory.store" | "memory.note" |
"tree.tag") currently can return early from reject_unexpected_arguments,
enforce_write_policy, or validate_controller_params and skip the mcp_writes
audit; update call_tool so that any error from reject_unexpected_arguments,
enforce_write_policy, or validate_controller_params triggers a call to the
existing mcp_writes audit path before returning the Err. Concretely, for the
branch handling spec.name == "memory.store"|"memory.note"|"tree.tag", propagate
errors from reject_unexpected_arguments, enforce_write_policy, and
validate_controller_params via map_err or match handlers that first call
mcp_writes (including spec.name, audit_arguments, client_info, params snapshot
and the failure reason) and then return the original ToolCallError, ensuring
dispatch_write_tool remains the path for successful calls.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e476a831-11e0-4951-9e8e-ca479605003e

📥 Commits

Reviewing files that changed from the base of the PR and between e9ca97c and 8ba1f4f.

📒 Files selected for processing (10)
  • src/core/all.rs
  • src/core/all_tests.rs
  • src/openhuman/mcp_audit/mod.rs
  • src/openhuman/mcp_audit/schemas.rs
  • src/openhuman/mcp_audit/store.rs
  • src/openhuman/mcp_audit/types.rs
  • src/openhuman/mcp_server/protocol.rs
  • src/openhuman/mcp_server/tools.rs
  • src/openhuman/memory_tree/store.rs
  • src/openhuman/mod.rs

Comment thread src/openhuman/mcp_audit/mod.rs Outdated
Comment thread src/openhuman/mcp_audit/schemas.rs
Comment thread src/openhuman/mcp_audit/schemas.rs
Comment thread src/openhuman/mcp_audit/store.rs
Comment thread src/openhuman/mcp_server/tools.rs
@MackJack023 MackJack023 marked this pull request as ready for review May 24, 2026 14:28
@qodo-code-review
Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/openhuman/mcp_server/tools.rs (1)

1192-1204: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Fix return type mismatch in load_write_config
The Ok(config) match arm currently returns a bare Config while the function signature is Result<Config, ToolCallError>.

Suggested fix
 async fn load_write_config(tool_name: &str) -> Result<Config, ToolCallError> {
     match config_rpc::load_config_with_timeout().await {
-        Ok(config) => config,
+        Ok(config) => Ok(config),
         Err(err) => {
             log::warn!(
                 "[mcp_server] enforce_write_policy config load failed tool={tool_name} error={err}"
             );
             return Err(ToolCallError::Internal(format!(
                 "failed to load config: {err}"
             )));
         }
     }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/mcp_server/tools.rs` around lines 1192 - 1204, The match in
load_write_config returns a bare Config on Ok instead of the expected Result;
change the Ok arm to return Ok(config) so the function returns Result<Config,
ToolCallError>, keeping the Err arm as-is (refer to load_write_config,
config_rpc::load_config_with_timeout, and ToolCallError).
🧹 Nitpick comments (1)
src/openhuman/mcp_server/tools.rs (1)

1221-1498: 🏗️ Heavy lift

Extract the new write-audit path into a smaller module.

This PR adds another large chunk of dispatch/audit logic to mcp_server/tools.rs, which is already far beyond the repo’s size target. Moving the write-tool dispatch and audit helpers into a sibling module would make this flow easier to maintain and keep future MCP changes localized.

As per coding guidelines: “File size should not exceed approximately 500 lines. When a module grows beyond this threshold, split it into smaller, more focused modules with clear responsibilities.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/mcp_server/tools.rs` around lines 1221 - 1498, Move the write
dispatch and audit helpers into a new focused module to shrink tools.rs: extract
the functions dispatch_write_tool, audit_write, audit_write_rejection,
audit_write_rejection_without_config, is_write_tool,
summarize_rejected_write_args, extract_document_id, summarize_write_args,
first_chars, and now_ms into a new sibling module (e.g.,
mcp_server::write_dispatch), implement the same signatures, re-export or pub use
the needed symbols, and update callers/imports in the codebase to reference the
new module; ensure the new module imports Config, NewMcpWriteRecord,
ToolCallError, Map, Value and other types used by these functions and that
tokio/runtime and mcp_audit dependencies remain available, then run cargo
build/tests to fix any missing imports or visibility issues.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/openhuman/mcp_server/tools.rs`:
- Around line 1192-1204: The match in load_write_config returns a bare Config on
Ok instead of the expected Result; change the Ok arm to return Ok(config) so the
function returns Result<Config, ToolCallError>, keeping the Err arm as-is (refer
to load_write_config, config_rpc::load_config_with_timeout, and ToolCallError).

---

Nitpick comments:
In `@src/openhuman/mcp_server/tools.rs`:
- Around line 1221-1498: Move the write dispatch and audit helpers into a new
focused module to shrink tools.rs: extract the functions dispatch_write_tool,
audit_write, audit_write_rejection, audit_write_rejection_without_config,
is_write_tool, summarize_rejected_write_args, extract_document_id,
summarize_write_args, first_chars, and now_ms into a new sibling module (e.g.,
mcp_server::write_dispatch), implement the same signatures, re-export or pub use
the needed symbols, and update callers/imports in the codebase to reference the
new module; ensure the new module imports Config, NewMcpWriteRecord,
ToolCallError, Map, Value and other types used by these functions and that
tokio/runtime and mcp_audit dependencies remain available, then run cargo
build/tests to fix any missing imports or visibility issues.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 30fa063e-7625-4955-86c3-010d1d6616e7

📥 Commits

Reviewing files that changed from the base of the PR and between 8ba1f4f and 32ae773.

📒 Files selected for processing (4)
  • src/openhuman/mcp_audit/mod.rs
  • src/openhuman/mcp_audit/schemas.rs
  • src/openhuman/mcp_audit/store.rs
  • src/openhuman/mcp_server/tools.rs

coderabbitai[bot]
coderabbitai Bot previously approved these changes May 24, 2026
@YOMXXX
Copy link
Copy Markdown
Contributor

YOMXXX commented May 24, 2026

Review/CI triage note: this red run is a real compile failure, not the earlier checkout/GHCR infra class. The current first blocker is in src/openhuman/mcp_server/tools.rs: load_write_config(...) -> Result<Config, ToolCallError> matches Ok(config) => config, but that arm needs to return Ok(config). That single E0308 failure is enough to fan out into Rust Quality, core tests, Tauri build, and E2E failures.\n\nAlso flagging reviewer context: this overlaps with draft #2553 for #2536. If this PR stays canonical, I would first fix the compile error and then ask the #2553 author/reviewer to either close the draft or explicitly keep it as an alternative design thread so reviewers do not review two divergent audit-log implementations in parallel.

@justinhsu1477
Copy link
Copy Markdown
Contributor

Author of #2553 (the parallel draft for #2536) here — flagging coordination context, not pushing for territory.

RFC design context (from the lazy-consensus comment on #2536):

  • Q1 = A — colocated in the existing memory-tree SQLite, additive CREATE TABLE IF NOT EXISTS (no separate migration step). ✅ both PRs picked this.
  • Q2 = A — best-effort audit; insert failures are logged but never propagate to the underlying write. ✅ both PRs picked this.
  • Q3 = A — no auto-prune in v1; revisit once we see usage volume.
  • Q4 = A (internal RPC only, deferred in feat(mcp): add mcp_writes audit log table (draft, RFC #2536) #2553) — the openhuman.mcp_audit_list surface you've shipped in this PR.

So we're aligned on Q1–Q3. The split is Q4: my draft deliberately deferred the query RPC pending ratification (kept a recent() helper as #[cfg(test)] only); yours commits to A (internal-only RPC, no MCP-client-side exposure) in v1. That's a reasonable read of the lazy consensus.

One safety detail worth checking that bit me in #2553: error_message truncation needs to land on a UTF-8 char boundary, not a raw byte offset. A 4-byte char like 🦀 sliced mid-codepoint produces invalid UTF-8 and SQLite happily stores garbage. The pattern I used:

let mut end = ERROR_MESSAGE_MAX_BYTES;
while end > 0 && !msg.is_char_boundary(end) {
    end -= 1;
}
record.error_message = Some(msg[..end].to_string());

Plus a regression test (record_handles_multibyte_truncation_safely) that repeats 🦀 past the cap and asserts is_char_boundary(stored.len()). Worth checking your store doesn't have the same naive-slice risk.

On the "writes vs write attempts" question @YOMXXX raised: I'd argue both should be recorded. Failure rows are the abuse-detection signal — a misbehaving MCP client repeatedly bouncing off the policy gate or doc_put validation will show up as a burst of success = false rows that pure "successful write history" framing would miss. Both PRs already do this; just worth ratifying explicitly so the RPC docs match.

Happy to close #2553 once @senamakel picks the canonical path. Either outcome works — flagging this so the design context from the RFC thread doesn't get lost in the handoff.

coderabbitai[bot]
coderabbitai Bot previously approved these changes May 25, 2026
@MackJack023
Copy link
Copy Markdown
Contributor Author

@justinhsu1477 Agreed on both points, thanks for carrying the #2553/RFC context over here.

I tightened this up in the latest commits:

  • error_message is now capped at 1 KiB in mcp_audit::store::record_write, and truncation walks back to a UTF-8 char boundary before slicing. I added record_handles_multibyte_error_truncation_safely with a repeated \u{1F980} payload to lock that down.
  • The mcp_audit.list controller schema now explicitly describes the rows as write attempts, including successful writes plus rejected/failed attempts, and clarifies that success_only filters for succeeded attempts.

I also fixed the load_write_config Ok(config) compile regression and updated the audit store to use the current memory_store::chunks::store connection path after the main-branch memory refactor.

Validation on this branch:

  • cargo fmt --manifest-path Cargo.toml --check
  • git diff --check
  • cargo check --manifest-path Cargo.toml -p openhuman --lib
  • cargo test --manifest-path Cargo.toml -p openhuman --lib record_handles_multibyte_error_truncation_safely -- --nocapture

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/openhuman/mcp_audit/schemas.rs (2)

193-197: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Test assertion assumes incorrect response structure.

This test expects value.as_array() directly, but if the response is corrected to match the schema ({"records": [...]}), this will fail. Update to access the "records" field.

Proposed fix (after handler is corrected)
         let value = handle_list(Map::new()).await.expect("handle list");
-        let records = value.as_array().expect("records array");
+        let records = value["records"].as_array().expect("records array");
         assert_eq!(records.len(), 1);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/mcp_audit/schemas.rs` around lines 193 - 197, The test
incorrectly treats the handler response as a bare array; update the assertions
to access the "records" field from the returned JSON object (i.e., call
value["records"] or value.get("records") and then as_array()) before checking
length and fields; adjust references to the existing handle_list await call and
the local variables value and records so the test asserts on
records[0]["tool_name"] and records[0]["client_info"] after extracting the
records array.

131-137: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Response structure does not match schema output definition.

The schema defines outputs with a field named "records", implying the response should be {"records": [...]}. However, handle_list returns serde_json::to_value(records) directly, which produces just [...].

This mismatch will cause clients relying on the schema contract to fail when accessing response["records"].

Proposed fix
         let count = records.len();
-        let value = serde_json::to_value(records).map_err(|err| {
+        let value = serde_json::to_value(serde_json::json!({ "records": records })).map_err(|err| {
             log::warn!("[mcp_audit] handle_list serialize response failed error={err}");
             err.to_string()
         })?;

Or alternatively:

+        let mut response = Map::new();
+        response.insert(
+            "records".to_string(),
+            serde_json::to_value(records).map_err(|err| {
+                log::warn!("[mcp_audit] handle_list serialize response failed error={err}");
+                err.to_string()
+            })?,
+        );
         let count = records.len();
-        let value = serde_json::to_value(records).map_err(|err| {
-            log::warn!("[mcp_audit] handle_list serialize response failed error={err}");
-            err.to_string()
-        })?;
         log::debug!("[mcp_audit] handle_list exit records={count}");
-        Ok(value)
+        Ok(Value::Object(response))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openhuman/mcp_audit/schemas.rs` around lines 131 - 137, handle_list is
serializing and returning records directly (serde_json::to_value(records)) but
the schema defines outputs with a top-level "records" field; change handle_list
to wrap the vector into an object with the key "records" before serializing
(e.g., create a serde-friendly map or struct containing records and use
serde_json::to_value on that) so the returned value matches the schema, preserve
the existing count/logging logic (records and count) and keep the same error
handling around serde_json::to_value.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/openhuman/mcp_audit/schemas.rs`:
- Around line 193-197: The test incorrectly treats the handler response as a
bare array; update the assertions to access the "records" field from the
returned JSON object (i.e., call value["records"] or value.get("records") and
then as_array()) before checking length and fields; adjust references to the
existing handle_list await call and the local variables value and records so the
test asserts on records[0]["tool_name"] and records[0]["client_info"] after
extracting the records array.
- Around line 131-137: handle_list is serializing and returning records directly
(serde_json::to_value(records)) but the schema defines outputs with a top-level
"records" field; change handle_list to wrap the vector into an object with the
key "records" before serializing (e.g., create a serde-friendly map or struct
containing records and use serde_json::to_value on that) so the returned value
matches the schema, preserve the existing count/logging logic (records and
count) and keep the same error handling around serde_json::to_value.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2420efc9-4e5a-45af-ac8b-8d7feb285030

📥 Commits

Reviewing files that changed from the base of the PR and between e7242fc and 52dfff4.

📒 Files selected for processing (3)
  • src/openhuman/mcp_audit/schemas.rs
  • src/openhuman/mcp_audit/store.rs
  • src/openhuman/mcp_server/tools.rs

@senamakel senamakel self-assigned this May 25, 2026
…tch schema

The ControllerSchema outputs define a "records" field, but handle_list
was returning a bare JSON array. Clients relying on the schema contract
would fail when accessing response["records"]. Updated the test
assertion to match.

Addresses @coderabbitai on schemas.rs:131-137 and schemas.rs:193-197.
}
}

fn audit_write(config: &Config, record: NewMcpWriteRecord) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Acknowledged nitpick: tools.rs is ~2750 lines — well beyond the ~500 line guideline. Extracting the write-dispatch and audit helpers (dispatch_write_tool, audit_write, audit_write_rejection, summarize_write_args, etc.) into a sibling module like mcp_server/write_dispatch.rs would be a solid follow-up. Deferring for a separate PR since it's a heavy refactor orthogonal to the audit feature.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tracked as #2604. Keeping this out of #2566 because the extraction is a mechanical-but-large module split; the audit behavior can land independently, then the refactor can preserve the write/audit contract with focused regression coverage.

@senamakel senamakel merged commit ace44fb into tinyhumansai:main May 25, 2026
26 of 31 checks passed
@MackJack023 MackJack023 deleted the codex/issue-2536-mcp-write-audit branch May 25, 2026 13:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Net-new user-facing capability or product behavior. rust-core Core Rust runtime in src/: CLI, core_server, shared infrastructure. working A PR that is being worked on by the team.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add mcp_writes audit log table for queryable MCP write history (Phase 3 #2269 Q4)

4 participants