brain-mcp: one shared bearer for every client, no per-caller read scoping (OWASP MCP07/MCP10) #144
Closed
opened 2026-10-01 16:34:43 +00:00 by mathias
·
5 comments
No Branch/Tag Specified
main
fix/pipeline-writes-158
fix/edit-path-io-192
fix/create-regular-188
fix/guard-special-179
fix/linkcheck-keeps-write-178
fix/write-dir-redirect-173
fix/tunnel-detect-174
fix/pipeline-root-157
w3/154-mcp-auth-wiring
fix/tunnel-traversal
test/in-brain-targets-171
fix/brain-scan-root-157
fix/graph-wiki-root-157
fix/vectorstore-root-157
fix/notes-root-157
fix/search-root-157
fix/brainwide-root-157
fix/raw-symlinks-153
feat/symlink-tripwire-157
fix/rest-auth-149
fix/123-brain-edit-envelope-check
feat/106-page-aware-extraction
fix/105-source-drift-on-main
fix/104-surface-merge-conflicts-on-main
agent/b1-compile-on-ingest
agent/104-surface-merge-conflicts
agent/105-source-drift
agent/107-loud-unconfigured-alerting
feat/brain-read-edit
fix/capture-i1-refusal-names-target
renovate/configure
feat/consolidate-single-harness
icebox/cmd-routing
feat/brain-pending-promote
fix/close-session-classification-refresh
fix/capture-summarywriter
v0.24.0
v0.23.1
v0.23.0
v0.22.0
v0.21.1
v0.21.0
v0.20.1
v0.20.0
v0.19.1
v0.19.0
v0.18.4
v0.18.3
v0.18.2
v0.18.1
v0.18.0
v0.17.0
v0.16.1
v0.16.0
v0.15.0
v0.14.0
v0.13.2
v0.13.1
v0.13.0
v0.12.1
v0.12.0
v0.11.2
icebox/cmd-routing-2026-07-01
v0.11.1
v0.11.0
v0.10.0
v0.9.0
v0.8.0
v0.7.0
v0.6.0
v0.5.0
v0.4.1
v0.4.0
v0.3.0
v0.2.0
v0.1.0
Labels
Clear labels
agent/ready
blocked/client
blocked/supplier
blocked/upstream
risk/high
risk/low
risk/medium
size/l
size/m
size/s
size/xs
status:dispatch
status:dispatched
status:review
status:reviewing
type/chore
type/decision
type/docs
type/feat
type/fix
type/ops
type/research
type/test
Meets the agent-ready contract; authorizes unattended execution
Waiting on the client
Waiting on a supplier
Waiting on an upstream project or vendor
Irreversible, security-relevant, or control plane
Reversible, local, test-covered
Touches shared state, deploys or data
Too big - split it
Under two days
Under half a day
Under an hour
Maintenance, no behaviour change
Human decision required; never agent/ready
Documentation only
New capability
Corrects broken behaviour
Infrastructure, deploy, runtime
Investigate; output is a finding, not code
Test coverage only
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: mathias/hyperguild#144
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Found during an OWASP MCP Top 10 (2025) audit, 2026-10-01.
Gap
/mcpis guarded bychassisauth.BearerMiddleware(mcpToken, jwtValidator, ...). The opt-in OAuthclient_credentialsendpoint (cmd/server/main.go,POST /oauth/token) returnsAccessToken: mcpToken— the same static bearer. Claude Code on koala, the claude.ai connector, Crush etc. are therefore indistinguishable on the read path; revoking one revokes all.{client}+public". Nothing ininternal/mcp/handlers.goenforces that per caller:wingonbrain_queryis an optional argument the model chooses. Any holder of the token reads every wing. The I1 gate (capture,internal/classification) protects the write side only.captureand governance edits go throughinternal/audit.brain_query/brain_readleave no record of who read what.Why it matters
Item 2 is the one that matters: the stated client-confidentiality rule is a convention, not a control. Same failure mode as
verify-the-cited-control-not-the-comment.Decision needed before building
Is the brain meant to hold confidential client content at all? If no (current classification refuses it on capture), then item 2 is low and the fix is to correct CLAUDE.md's claim. If yes, it needs per-principal scopes (JWT claim → allowed wings) enforced in the handler.
Done when
sub) distinguish callers in logs.tools/callwith principal + tool + wing.OWASP refs: MCP07 Insufficient AuthN/AuthZ, MCP10 Context Over-Sharing, MCP08.
Created via git-mcp on behalf of @mathias
Decision (Mathias, 2026-10-01): the brain does NOT hold confidential client content
So read-path wing scoping is not needed. Consequences:
{client}+public") gets corrected inmathias/local-dev.context/AGENT.md— done separately, not in this repo.tools/call.Spec for the executor
In
internal/mcp/server.go, aroundhandleCall(dispatch for every tool), emit one structuredslogrecord per call:msg: "mcp tool call",tool,principal,origin(static/jwt/unknown, fromprincipalInfoin ctx),wing/hallwhen present in args,outcome(ok/ error string truncated to 200 chars),duration_ms.content(brain_write/brain_update/capture bodies) — logcontent_bytesinstead. The audit line must not become a second copy of brain content.Tests (red first)
TestHandleCallLogsOneAuditRecordPerCall— capture log output, callbrain_query, assert exactly one record with tool/principal/outcome.TestHandleCallAuditOmitsContent—brain_writewith a sentinel content string; assert the sentinel is absent from the log andcontent_bytesis present.TestHandleCallAuditRecordsErrorOutcome— unknown tool / failing handler →outcomeis the error.Done when
task checkgreen; after deploy,{app="ingestion"} |= "mcp tool call"in Loki shows records for a livebrain_query.Created via git-mcp on behalf of @mathias
Correction:
status:dispatchdoes nothing here. The dispatch watcher has been suspended since 2026-09-17 (dispatch#56), and the CI agent lane exists only on cobalt-dingo. Taking this interactively instead, via a PR and gatekeeper review. The CLAUDE.md claim is already corrected: local-dev12f694b(v0.18.1). The label is inert while the watcher is frozen (no unlabel tool, gitea-mcp#61).Created via git-mcp on behalf of @mathias
pr-gatekeeper: PR #147 BLOCKED at
32dedb3. On the audit-log half, one audit line per tools/call with tool, principal, origin, wing and hall works, verified by execution. But the outcome field copies caller content through error strings: a claim-envelope rejection echoes the frontmatter value, and brain_update echoes the path (E6). The spec says never log content. The 200-char truncation has no test. Details: #147 (comment)pr-gatekeeper: PR #147 merged as
a6da933. One structuredmcp tool callslog record per tools/call, carrying tool, principal, origin, wing, hall, content_bytes, duration_ms and outcome (ok/error only, so no argument values are echoed). Verified by execution with a sentinel across capture, brain_write, brain_edit, brain_update, brain_query and brain_read: no content in the record. Tests go red when content is logged or the error text comes back.Reopening: the narrowed done-when includes "after deploy,
{app=\"ingestion\"} |= \"mcp tool call\"in Loki shows records for a live brain_query". That needs a deploy, and the merge auto-closed this issue before it was checked. Close once the Loki query shows a live record.The last done-when clause is verified live. PR #147 deployed as
ingestion:a6da933(infra36916a3), and the rollout completed. I then ran a livebrain_querythrough the plugin MCP, and Loki (loki-gateway, query{namespace="supervisor"} |= "mcp tool call" |= "brain_query") returned:Every clause now holds:
Per-client tokens are split out to #146. Closing.
Created via git-mcp on behalf of @mathias