Skip to content

fix(security/channels): enforce cross-account channel_send guard through the /mcp bridge (#6443)#6455

Merged
houko merged 1 commit into
mainfrom
fix/6443-mcp-bridge-account-parity
Jul 15, 2026
Merged

fix(security/channels): enforce cross-account channel_send guard through the /mcp bridge (#6443)#6455
houko merged 1 commit into
mainfrom
fix/6443-mcp-bridge-account-parity

Conversation

@houko

@houko houko commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Closes #6443

Summary

#6449 fixed the cross-account (cross-tenant) channel_send guard on the in-process agent-loop path and explicitly flagged the /mcp bridge (subprocess claude-code driver) as an unchanged parallel surface pending a maintainer call.
Without this parity, an agent running through the subprocess driver could still pass another tenant's account_id and dispatch content into that tenant's chat — the exact vulnerability #6443 reported, on the one remaining path.
This PR lands the parity, mirroring how #6125 threaded the #6117 peer scope through the same chain.

Changes

  • librefang-llm-driver: CompletionRequest gains sender_account_id — the bot account / tenant the inbound turn arrived on. Out-of-band callers (cron, triggers, compaction, routing probes) leave it None via the struct's existing Default, so the ~35 ..Default::default() construction sites are untouched.
  • librefang-runtime: both agent loops (run_agent_loop and the streaming variant) populate the new field from the manifest metadata stamp the kernel already writes (SENDER_ACCOUNT_ID_METADATA_KEY, fix: resolve four reported bugs (#6423, #6442, #6443, #6444) #6449).
  • librefang-llm-drivers: claude_code.rs::write_mcp_config forwards it on the bridge connection as X-LibreFang-Current-Account-Id, next to the existing X-LibreFang-Current-Peer-Jid / -Channel / -Chat-Id headers. Empty or absent values emit no header, so single-tenant and out-of-band spawns produce byte-identical configs to before.
  • librefang-api: mcp_http reads the header back and routes tool execution through execute_tool_with_sender_account, so tool_channel_send's existing bug(security/channels): channel_send account_id accepted unvalidated — cross-tenant dispatch across bot accounts #6443 guard fires on the bridge path. Clients that omit the header (external MCP clients, pre-parity CLIs) run unguarded exactly as before — the guard no-ops on None.
  • CHANGELOG.md: Security entry under [Unreleased].

No guard logic changed — the bridge now feeds the same guard #6449 landed, with the same explicit-value-only, same-channel scoping.

Verification

…ugh the /mcp bridge (#6443)

#6449 fixed the cross-account (cross-tenant) channel_send guard on the in-process agent-loop path and flagged the /mcp bridge (subprocess claude-code driver) as an unchanged parallel surface pending a maintainer call.
This lands that parity, mirroring how #6125 threaded the #6117 peer scope through the same chain:

- CompletionRequest gains sender_account_id — the bot account / tenant the inbound turn arrived on. Out-of-band callers (cron, triggers, compaction, routing probes) leave it None via the struct's existing Default.
- Both agent loops (run_agent_loop and the streaming variant) populate it from the manifest metadata stamp the kernel already writes (SENDER_ACCOUNT_ID_METADATA_KEY, #6449).
- The claude-code driver's write_mcp_config forwards it on the bridge connection as X-LibreFang-Current-Account-Id, next to the existing X-LibreFang-Current-Peer-Jid / -Channel / -Chat-Id headers; empty or absent values emit no header.
- mcp_http reads the header back and routes tool execution through execute_tool_with_sender_account, so tool_channel_send's existing #6443 guard rejects an explicit account_id that differs from the turn's account on the same channel. Clients that omit the header run unguarded exactly as before.

Tests:
- claude_code::tests — the peer-scope header tests now cover the account header (emitted when present, omitted when absent).
- api_integration_test::test_mcp_http_channel_send_cross_account_guard_uses_account_header — end-to-end /mcp tools/call: mismatched account blocked, matching account passes, missing header no-ops.

Verified: cargo check --workspace --lib, cargo clippy --workspace --all-targets -D warnings (clean), cargo fmt, scoped cargo test (llm-drivers mcp_config: 5 passed; api channel_send guards: 2 passed; runtime channel_send: 23 passed).
@github-actions github-actions Bot added size/M 50-249 lines changed area/docs Documentation and guides area/runtime Agent loop, LLM drivers, WASM sandbox labels Jul 14, 2026
@houko
houko merged commit 00d1bd6 into main Jul 15, 2026
32 checks passed
@houko
houko deleted the fix/6443-mcp-bridge-account-parity branch July 15, 2026 00:48
houko added a commit that referenced this pull request Jul 15, 2026
…issue (#6466)

* fix(ci): treat retired pnpm audit endpoint as skip, not a dependency issue

npm retired the legacy pnpm audit endpoints (/-/npm/v1/security/audits and .../audits/quick); they now return HTTP 410 with a body directing callers to the bulk advisory endpoint. pnpm v10 still calls the legacy path, so `pnpm audit` exits non-zero even when no advisory exists.

`xtask deps --web` only inspected the exit code, so it counted each of the three frontend dirs (web / dashboard / docs) as a dependency issue and failed the Security job with "3 dependency issue(s) found". Because the job runs on any Rust/CI change, main went red on every push regardless of the diff under test (#6455, #6456, #6457 all inherited it).

run_pnpm_audit now captures the combined stdout+stderr and classifies the outcome: a genuine advisory still fails the build, but the retired-endpoint signature (ERR_PNPM_AUDIT_BAD_RESPONSE / "endpoint is being retired" / an audit-context 410) is warned and skipped rather than counted. An audit-infrastructure outage no longer reads as a vulnerability.

Restoring real frontend advisory coverage requires a pnpm version that speaks the bulk advisory endpoint and is out of scope here.

Closes #6462

* docs(changelog): reference the PR number (#6466) for the pnpm audit fix

CHANGELOG entries follow the PR-number convention; the initial entry used the tracking issue number (#6462) written before the PR existed. The `Closes #6462` link lives in the PR body / commit.

---------

Co-authored-by: Evan <[email protected]>
@houko houko mentioned this pull request Jul 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/docs Documentation and guides area/runtime Agent loop, LLM drivers, WASM sandbox size/M 50-249 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug(security/channels): channel_send account_id accepted unvalidated — cross-tenant dispatch across bot accounts

1 participant