Skip to content

fix(security): close secret-scrubbing bypass paths in all MCP write routes#922

Open
Srinath279 wants to merge 2 commits into
rohitg00:mainfrom
Srinath279:fix/close-scrubbing-bypass-paths
Open

fix(security): close secret-scrubbing bypass paths in all MCP write routes#922
Srinath279 wants to merge 2 commits into
rohitg00:mainfrom
Srinath279:fix/close-scrubbing-bypass-paths

Conversation

@Srinath279

@Srinath279 Srinath279 commented Jun 13, 2026

Copy link
Copy Markdown

Problem

stripPrivateData was wired into the observe pipeline only (hooks/prompt-submit.ts).
Seven MCP write tools that accept content directly bypassed that pipeline entirely,
meaning secrets submitted through those routes were stored in plaintext:

Tool Bypass reason
memory_save (mem::remember) Explicit save, no hook
memory_lesson_save Crystallize output + manual saves
memory_slot_create / append / replace Slot writes never touch the hook
memory_sketch_create Sketch/action title+description
memory_team_share Re-shares already-stored rows
mem::compress (internal) LLM can echo secrets into its summary
mem::import Imported dumps skip capture entirely

Solution

Apply stripPrivateData (or the new scrubRecord for untyped payloads) at each
write site before data touches KV storage.

New patterns in privacy.ts

  • PEM private key blocks — -----BEGIN * PRIVATE KEY-----
  • DB connection URLs with credentials — postgres://, mysql://, mongodb+srv://, redis://, amqp://, mssql://

New utility: scrubRecord

A recursive T→T walker (objects, arrays, strings). Lets callers scrub arbitrarily-shaped
payloads (LLM output, import blobs, team-share content) without knowing their schema.

Tests

New test/scrubbing-bypass.test.ts covers every bypass path.

Bonus: pnpm-workspace.yaml

Adds allowBuilds for esbuild, onnxruntime-node, protobufjs, sharp — fixes
ERR_PNPM_IGNORED_BUILDS on fresh checkouts.

Test plan

  • pnpm test passes
  • Each tool stores [REDACTED_SECRET] instead of plaintext secrets
  • Import with plaintext secrets → stored rows are scrubbed
  • Clean content passes through unchanged (no false positives)

Summary by CodeRabbit

  • Documentation

    • Added comprehensive end-to-end architecture documentation covering system design, data flow, operational flows, security, observability, and deployment.
  • Bug Fixes

    • Strengthened privacy: introduced recursive scrubbing and applied automated secret/credential redaction across memory operations, imports, sharing, compression, slots, sketches, lessons, and team boundaries.
  • Tests

    • Added end-to-end tests validating scrubbing behavior across storage and memory workflows.
  • Chores

    • Updated workspace build configuration.

…outes

## Problem

`stripPrivateData` was only applied inside the observe pipeline
(`src/hooks/prompt-submit.ts`). Seven write paths that accept content
directly via MCP tools bypassed that pipeline entirely, so secrets
passed through those routes were stored in plaintext:

- `mem::remember`       — explicit memory saves
- `mem::lesson-save`    — lessons (content + context fields)
- `mem::slot-create`    — slot initial content
- `mem::slot-append`    — appended text
- `mem::slot-replace`   — replaced content
- `mem::sketch-create`  — sketch/action title and description
- `mem::team-share`     — shared item at the audience-widening boundary
- `mem::compress`       — LLM summary output (model can echo seen secrets)
- `mem::import`         — imported dumps bypass capture entirely

## Changes

### src/functions/privacy.ts
- Add regex for PEM private key blocks (`-----BEGIN * PRIVATE KEY-----`)
- Add regex for DB connection URLs with embedded credentials
  (`postgres://`, `mysql://`, `mongodb+srv://`, `redis://`, etc.)
- Export new `scrubRecord<T>` — a recursive `T→T` walker that applies
  `stripPrivateData` to every string in an arbitrary nested object/array.
  Use this where the shape is unknown (imports, LLM output, team shares);
  call `stripPrivateData` directly for known string fields.

### src/functions/remember.ts
- Call `stripPrivateData(data.content)` before the Jaccard similarity
  check so dedup compares and stores the already-scrubbed form.

### src/functions/lessons.ts
- Scrub `content` and `context` before fingerprinting, so dedup keys
  match the scrubbed form. Applies to both manual saves and output from
  `mem::crystallize`.

### src/functions/slots.ts
- Scrub content in `slot-create`, text in `slot-append`, and content in
  `slot-replace`.

### src/functions/sketches.ts
- Scrub `title` and `description` in both `sketch-create` and the inline
  action-create path.

### src/functions/team.ts
- Apply `scrubRecord` at the share boundary before writing the
  `TeamSharedItem`. Defense-in-depth: catches rows written before a newer
  pattern was added and shares widen audience from one user to the team.

### src/functions/compress.ts
- Apply `scrubRecord` to the LLM-parsed compression output before it is
  spread into `CompressedObservation`. An LLM can echo a secret from its
  context window into a generated summary.

### src/functions/export-import.ts
- Apply `scrubRecord` to all content-bearing collections
  (sessions, memories, summaries, lessons, insights, semanticMemories,
  crystals, sketches, observations) before any row is written during
  import. Imports arrive from outside the capture pipeline and may have
  been created by an older install with fewer patterns.

### test/scrubbing-bypass.test.ts (new)
- Unit tests for all seven bypass paths: remember, lesson-save,
  slot-create, slot-append, slot-replace, sketch-create, team-share,
  compress (LLM output), and import.
- Fixtures cover every secret family: Anthropic key, GitHub PAT,
  DB URL with embedded password, and PEM private key block.

### pnpm-workspace.yaml (new)
- `allowBuilds` for esbuild, onnxruntime-node, protobufjs, sharp so
  `pnpm build` and `pnpm start` are not blocked by the
  ERR_PNPM_IGNORED_BUILDS check on developer machines.
@vercel

vercel Bot commented Jun 13, 2026

Copy link
Copy Markdown

Someone is attempting to deploy a commit to the rohitg00's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 53a52420-e0e6-4f51-b304-a549ebcf933c

📥 Commits

Reviewing files that changed from the base of the PR and between d89342c and 915c526.

📒 Files selected for processing (2)
  • docs/E2E_ARCHITECTURE.md
  • src/functions/slots.ts
✅ Files skipped from review due to trivial changes (1)
  • docs/E2E_ARCHITECTURE.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/functions/slots.ts

📝 Walkthrough

Walkthrough

Adds recursive record scrubbing and new secret-detection patterns, applies scrubbing across memory operations and import/export boundaries, updates architecture docs and workspace build config, and adds comprehensive tests validating redaction across workflows.

Changes

Privacy Scrubbing Enhancement

Layer / File(s) Summary
Privacy module core extension
src/functions/privacy.ts
SECRET_PATTERN_SOURCES expanded with PEM private key and DB-URI credential patterns; new exported scrubRecord<T> recursively scrubs nested structures while preserving non-string values.
Privacy scrubbing across memory operations
src/functions/remember.ts, src/functions/lessons.ts, src/functions/sketches.ts, src/functions/slots.ts, src/functions/team.ts, src/functions/export-import.ts, src/functions/compress.ts
Scrubbing integrated at persistence and boundary points: remember (dedup/input), lesson-save (content/context fingerprinting), sketch create/add (title/description), slot create/append/replace (content), team-share (shared payload), mem::import (all imported collections/observations), and compress (LLM output fields).
Test suite for privacy scrubbing
test/scrubbing-bypass.test.ts
Vitest suite with unit tests for stripPrivateData/scrubRecord (PEM/DB patterns, recursion, idempotence) and integration tests verifying scrubbing across remember, lessons, slots, sketches, team-share, import, and compress.
Architecture documentation and build configuration
docs/E2E_ARCHITECTURE.md, pnpm-workspace.yaml
Adds full E2E architecture blueprint (versioned) including security model and scrubbing details; enables native build permissions for specified packages in pnpm workspace.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • rohitg00

Poem

🐰 In burrows deep the secrets hide,

I hop with scrubbing brush in stride—
PEM and URIs I gently sweep,
Through nested fields I softly creep,
Now memories sleep safe and tidy.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.33% 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 accurately describes the main objective: closing secret-scrubbing bypass paths in all MCP write routes, which is the core change across multiple function files.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/functions/slots.ts (1)

322-322: 💤 Low value

Consider assigning to a local variable instead of mutating the input parameter.

While the current approach works correctly, mutating data.content directly is stylistically inconsistent with the patterns used elsewhere in this file (slot-create and slot-append assign to local constants). Using a local variable would make the code more consistent and easier to reason about.

♻️ Optional refactor for consistency
     const label = validateLabel(data?.label);
     if (!label) return { success: false, error: "label required" };
     if (typeof data?.content !== "string") return { success: false, error: "content required (string)" };
-    data.content = stripPrivateData(data.content);
+    const content = stripPrivateData(data.content);
     return withKeyedLock(`slot:${label}`, async () => {
       const { slot, scope } = await readSlot(kv, label);
       if (!slot) return { success: false, error: "slot not found (use mem::slot-create first)" };
       if (slot.readOnly) return { success: false, error: "slot is read-only" };
-      if (data.content.length > slot.sizeLimit) {
+      if (content.length > slot.sizeLimit) {
         return {
           success: false,
-          error: `content exceeds sizeLimit (${data.content.length} > ${slot.sizeLimit})`,
+          error: `content exceeds sizeLimit (${content.length} > ${slot.sizeLimit})`,
           sizeLimit: slot.sizeLimit,
         };
       }
-      const updated: MemorySlot = { ...slot, content: data.content, updatedAt: nowIso() };
+      const updated: MemorySlot = { ...slot, content, updatedAt: nowIso() };
       await kv.set(scopeKv(scope), label, updated);
       await recordAudit(kv, "slot_replace", "mem::slot-replace", [label], {
         scope,
         before: slot.content.length,
-        after: data.content.length,
+        after: content.length,
       });
-      return { success: true, slot: updated, size: data.content.length };
+      return { success: true, slot: updated, size: content.length };
     });
🤖 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/functions/slots.ts` at line 322, Replace the in-place mutation of the
input parameter by assigning the sanitized value to a local variable: instead of
mutating data.content with stripPrivateData, create a local const (e.g., const
content = stripPrivateData(data.content)) and use that local variable for
subsequent logic; update any references in the surrounding function (slots.ts,
functions that follow this line) to read from the new local variable rather than
data.content to keep the style consistent with slot-create/slot-append.
🤖 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 `@docs/E2E_ARCHITECTURE.md`:
- Around line 686-699: The Session model lacks the parentSessionId used by the
subagent flow; add an optional parentSessionId?: string (UUID v4) to the Session
interface to reflect that subagents link back to a parent session, and update
any other occurrences of the Session declaration (the later duplicate) and any
documentation/examples that reference Session to include this optional field so
the contract in Section 7 and Section 5 stays consistent.
- Around line 455-459: The table row for Scope Key `mem:obs:{sessionId}`
contains the cell text RawObservation | CompressedObservation[] which breaks the
markdown table; update that cell to escape the pipe (e.g., replace the vertical
bar with \| or the HTML entity &`#124`;) so it stays a single column (locate the
row with `mem:obs:{sessionId}` and adjust the cell content).
- Around line 100-106: The diagram contains a truncated reference
"index-persist.."—replace it with the full filename used in Section 4.4, e.g.
"index-persistence.ts", so the entry reads "agent-sdk   │   │  IndexPersistence
(disk)      ←─── index-persistence.ts" and ensure the spelling matches the
actual source file name referenced elsewhere.

---

Nitpick comments:
In `@src/functions/slots.ts`:
- Line 322: Replace the in-place mutation of the input parameter by assigning
the sanitized value to a local variable: instead of mutating data.content with
stripPrivateData, create a local const (e.g., const content =
stripPrivateData(data.content)) and use that local variable for subsequent
logic; update any references in the surrounding function (slots.ts, functions
that follow this line) to read from the new local variable rather than
data.content to keep the style consistent with slot-create/slot-append.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2ff725c5-7bd4-49de-821c-4549c61dc0cf

📥 Commits

Reviewing files that changed from the base of the PR and between f6f9e3c and d89342c.

📒 Files selected for processing (11)
  • docs/E2E_ARCHITECTURE.md
  • pnpm-workspace.yaml
  • src/functions/compress.ts
  • src/functions/export-import.ts
  • src/functions/lessons.ts
  • src/functions/privacy.ts
  • src/functions/remember.ts
  • src/functions/sketches.ts
  • src/functions/slots.ts
  • src/functions/team.ts
  • test/scrubbing-bypass.test.ts

Comment thread docs/E2E_ARCHITECTURE.md
Comment thread docs/E2E_ARCHITECTURE.md
Comment thread docs/E2E_ARCHITECTURE.md
- Fix truncated reference 'index-persist..' → 'index-persistence.ts' in architecture diagram
- Escape pipe in table cell for RawObservation | CompressedObservation[]
- Add parentSessionId field to Session interface to align with subagent contract
- Refactor slot-replace to use local const instead of mutating input parameter

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant