Skip to content

security: add write_user_memory MCP tool to fix PII cross-user memory leak (#888) - #891

Merged
vybe merged 2 commits into
devfrom
feature/888-write-user-memory-mcp-tool
May 18, 2026
Merged

vybe merged 2 commits into
devfrom
feature/888-write-user-memory-mcp-tool

Conversation

@vybe

@vybe vybe commented May 18, 2026

Copy link
Copy Markdown
Contributor

Summary

  • P0 privacy bug: agents serving public-link/channel users who write to ~/.claude/projects/memory/ leak that user's PII to every subsequent user of the same agent (the path is shared across all sessions)
  • Three-layer fix: platform guardrail (prompt) + write_user_memory MCP tool (isolated store) + execution_id surfaced in execution context (so the tool can resolve the user server-side)

Changes

Layer 1 — Platform guardrail (platform_prompt_service.py):
New "Remembering Things About Users" section in PLATFORM_INSTRUCTIONS explicitly prohibits writing user PII to the shared memory directory and explains the correct tool to use instead.

Layer 2 — write_user_memory MCP tool (new routers/public_memory.py, new tools/memory.ts):

  • POST /api/agents/{name}/user-memory — agent passes execution_id; backend resolves source_user_email server-side from schedule_executions, verifying the execution belongs to this agent and was triggered by public/slack/telegram/whatsapp. Agent never touches email addresses.
  • Writes to the existing public_user_memory table (per-(agent_name, user_email) blob, already isolated by MEM-001).
  • Returns error if called from a scheduled task or agent-to-agent execution.

Layer 3 — execution_id in execution context (platform_prompt_service.py, task_execution_service.py):
ExecutionContext now includes execution_id, rendered as - **Execution ID**: <id> in the injected system prompt block. Agents read this value and pass it to write_user_memory.

Security Properties

  • Agent cannot write memory for an arbitrary user (no user_email param accepted)
  • Backend validates: execution exists, belongs to calling agent, has user-facing trigger, has verified email
  • The public_user_memory table was already isolated per (agent_name, user_email) — no schema change needed
  • Platform guardrail gives LLM guidance before it reaches for the filesystem

Test Plan

  • Call POST /api/agents/{name}/user-memory with a valid execution_id from a public chat → returns success: true with resolved email
  • Same call with a scheduled execution_id → returns 422 "not a user-facing session"
  • Same call with execution_id belonging to a different agent → returns 403
  • MCP tool write_user_memory callable from an agent-scoped key during a public chat execution
  • Execution ID appears in the injected execution context system prompt block
  • Existing public_user_memory auto-summarizer (MEM-001) still works — it writes the same column, tool and summarizer coexist

Fixes #888

🤖 Generated with Claude Code

vybe and others added 2 commits May 18, 2026 17:31
… leak (#888)

Three-layer fix for P0 privacy bug where agents writing to ~/.claude/projects/memory/
during public/channel sessions exposed one user's PII to all subsequent users.

Layer 1 — Platform guardrail: PLATFORM_INSTRUCTIONS now explicitly warns agents not
to write user-identifying data to the shared memory directory and points them at
write_user_memory instead.

Layer 2 — write_user_memory MCP tool: Agents call this with their execution_id;
the backend resolves the user email server-side from schedule_executions.source_user_email,
validating the execution belongs to the calling agent and was triggered by a user-facing
channel (public/slack/telegram/whatsapp). The agent never touches email addresses directly.

Layer 3 — execution_id in context: ExecutionContext now includes execution_id, rendered
as "Execution ID" in the injected system prompt block so agents can pass it to the tool.

Files:
- services/platform_prompt_service.py: guardrail instruction + execution_id field/render
- services/task_execution_service.py: pass execution_id to ExecutionContext
- routers/public_memory.py (new): POST /api/agents/{name}/user-memory endpoint
- main.py: register new router
- mcp-server/src/client.ts: writeUserMemory() method
- mcp-server/src/tools/memory.ts (new): write_user_memory tool
- mcp-server/src/server.ts + tools/index.ts: register tool

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tool (#888)

Add public_memory.py router, POST /api/agents/{name}/user-memory endpoint,
and memory.ts MCP tool (17th module) to architecture.md. Update MEM-001 in
requirements.md to describe the explicit write path alongside the existing
background summarization.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@vybe
vybe merged commit 06b8649 into dev May 18, 2026
8 checks passed
vybe added a commit that referenced this pull request Oct 19, 2026
…, Storage, People, one restart banner (ent#887, #888, #890, #891, #353) (#3798)

* feat(ui): one Settings home for the agent — rail, General, Guardrails, Storage, People, one restart banner

The agent page splits into a working strip and ONE Settings tab, pinned at
the end of the strip, with a rail of panes each answering one question
(decision on #966). The pane is in the URL
(?tab=settings&section=…); old ?tab=info|guardrails|credentials|folders|access
links land on their pane.

- General (Abilityai/trinity-enterprise#887): identity (label, id rename,
  avatar, description, tags), runtime/model/funding, execution timeout
  (Abilityai/trinity-enterprise#353 — named range error, #929 schedule
  conflict with an Open Schedules action), parallel capacity, read-only,
  memory/CPU, template details, Danger zone (reset to main only with a bound
  repo; delete). The header keeps identity, state, the autonomy chip and the
  doors; live usage and spend move to Overview; Pull/Push move to the Git tab.
- One restart banner: rows report to stores/agentRestart.js; memory/CPU is
  saved and applied on the next restart instead of restarting at once.
- Guardrails (Abilityai/trinity-enterprise#890): rules, self-change
  permissions (ent#756), gated-skill count, autonomy read-only until
  PR #2984 merges, cross-model validation, reliability.
- Storage (Abilityai/trinity-enterprise#888): repository, GitHub token,
  git sync, credentials, shared folders, file sharing. Credentials and
  Folders tabs removed; Sharing loses Distribution.
- People (Abilityai/trinity-enterprise#891): the Access tab, unchanged.
- Channels/Integrations hold the Voice / MCP cards from the old flat list and
  point at the tabs that still hold the rest.

Red before green: agentSettingsHome.spec.js (7 strip/alias cases) and the
file-sharing restart report fail against dev's agentTabs.js and
FileSharingPanel.vue.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(agent-settings): restore the controls the Settings home dropped, settle the restart banner on the status edge (ent#887, ent#888)

A completeness audit against dev found five controls with no new home and
three defects in the one restart banner:

- Memory/CPU: the "Inherit default" choice is back, so a per-agent override
  can be cleared again; the editor seeds from the override, not the
  effective value, and saves an empty choice as null.
- Capacity: a stored value above the fleet ceiling says so, and names the
  value that applies.
- Git tab: the "Refresh status" button the header had; the Settings › Storage
  link is shown only to owners, who have that tab.
- General › Avatar: "Next emotion" for a prompt-generated avatar.
- Overview: a stopped agent's "Created" line.

- The banner's list is cleared on the agent's stopped → running edge, not by
  the button: a start that fails keeps every waiting change listed (the
  lifecycle composable reports and returns normally, so the old code read a
  failed start as success and lost the guardrails / GitHub-token entries),
  and a manual stop/start from the header settles the list too.
- FileSharingPanel resets on an in-place agent switch, so agent A's
  "restart required" is never announced for agent B.

Also: the two @Interactive e2e specs that deep-linked to ?tab=info now open
Settings › General → Technical details; user docs name Settings › Storage /
People instead of the retired Credentials / Access / Folders tabs.

Red first: agentSettingsRestart.mount.spec.js (failed start kept nothing,
manual start settled nothing), fileSharingPanelAudience.spec.js (switch
carried the wait over), agentSettingsHome.mount.spec.js (no inherit option,
no ceiling note) — 5 assertions red on the previous commit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(screenshots): drop deleted SettingsPanel.vue from settings-general sources

The agent Settings home (ent#887) deletes components/settings/SettingsPanel.vue.
That component was the agent page's old Settings tab, never part of the platform
Settings view the screenshot shows, and screenshot-guard fails on a missing source.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(1712): repoint the ReliabilityPanel wiring guard at the Guardrails pane

The agent Settings home (ent#887) deletes components/settings/SettingsPanel.vue
and mounts ReliabilityPanel in agent-settings/GuardrailsPane.vue. The guard now
reads the pane and asserts the rendered tag, not just the name. Negative control:
removing the <ReliabilityPanel> tag turns it red.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: trinity-ability <309458136+trinity-ability@users.noreply.github.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