-
Notifications
You must be signed in to change notification settings - Fork 29
Protect personal MCP grants and proxy HTTP OAuth server-side #99
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
7e69d73
9720bbb
7a9fb8c
d0ba933
0c28b99
2efb367
e811efe
a5875d9
05c578a
a3ef7f2
bc1a55c
fa06a79
087f11f
4d67752
5136a67
e03bb07
e86fdec
41ea734
9c7aed2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -44,6 +44,8 @@ import { findSession, touchNativeSession } from "./session-cache"; | |
| import { attachRepo, linkPr, resolveSessionRepoContext, sessionRepoIds, switchPrimaryRepo } from "./session-repos"; | ||
| import { makeAskHandler } from "./asks"; | ||
| import { activeSandboxFor } from "./session-sandbox"; | ||
| import { mcpOauthProxyServers } from "./mcp-oauth-proxy"; | ||
| import type { McpScope } from "./runner-shared"; | ||
|
|
||
| type PreviewAction = "start" | "status" | "stop"; | ||
| type PreviewModule = typeof import("./preview"); | ||
|
|
@@ -126,11 +128,42 @@ function papercutsServerFor( | |
| }; | ||
| } | ||
|
|
||
| /** The personal-proxy scope a session's own run should get, for launchers that | ||
| * rebuild a run's servers from the session file rather than from live run | ||
| * options (the run-rpc fallback builder, the resume path in opensession.ts). | ||
| * Old feed sessions can predate the persisted allowlist, and this cannot | ||
| * resolve their connectors, so those fail closed rather than widening. */ | ||
| export function personalMcpScopeForSession( | ||
| session: { mcpServers?: string[]; externalRefs?: unknown[] } | undefined, | ||
| ): McpScope { | ||
| if (session?.mcpServers?.length) return session.mcpServers; | ||
| if (session?.externalRefs?.length) return []; | ||
| return "all"; | ||
| } | ||
|
|
||
| export function interactiveMcpServers( | ||
| user?: string, | ||
| sessionId?: string, | ||
| user: string | undefined, | ||
| sessionId: string | undefined, | ||
| /** Which external servers this run may see, so a personal proxy is never | ||
| * mounted wider than the run's own allowlist. REQUIRED, and deliberately | ||
| * not optional: every launcher has to make the choice, because forgetting | ||
| * it is silent — `buildOpencodeMcpConfig` drops a granted server on the | ||
| * assumption a proxy replaces it, so a launcher that omits the scope gets | ||
| * neither. Pass `undefined` only to mean "no personal tools at all" | ||
| * (Desk voice). */ | ||
| personalMcpScope: McpScope | undefined, | ||
| ): Record<string, unknown> { | ||
| const createdBy = user || productName(); | ||
| const session = sessionId ? findSession(sessionId) : undefined; | ||
| // Personal provider tools are opt-in at the run launch sites. Other callers | ||
| // (notably Desk voice) deliberately consume a narrower interactive facade. | ||
| const personalMcp = session && personalMcpScope | ||
| ? mcpOauthProxyServers( | ||
| personalMcpScope, | ||
| user, | ||
| [user], | ||
| ) | ||
| : {}; | ||
| return { | ||
| "opensession-sessions": createSessionsMcpServer({ | ||
| createdBy, | ||
|
|
@@ -145,6 +178,7 @@ export function interactiveMcpServers( | |
| createdBy, | ||
| isAdmin: true, | ||
| }), | ||
| ...personalMcp, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔴 P1 — Make the personal proxy shadow the external MCP entry This proxy does not handle ordinary local Pi turns after the latest main merge.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 9c7aed2: the Pi MCP runtime now removes external entries shadowed by coordinator-owned in-process servers, and its external connector no longer selects or relays creator-first personal grants. Same-named personal proxies therefore own the runtime entry and use only the prompter identity supplied by interactive-mcp. Added coverage for in-process shadowing. |
||
| // Runners are deliberately trusted persistent machines for platform-locked | ||
| // work. Interactive-only: untrusted automation text must never reach one. | ||
| "opensession-runners": createRunnersMcpServer({ user, sessionId }), | ||
|
|
@@ -316,7 +350,12 @@ export function interactiveMcpServers( | |
| // recursion is lazy and terminates: this closure runs on a | ||
| // script's first mcp.* call, and the workflows server the rebuild | ||
| // produces is excluded from the allowlist anyway. | ||
| inProcessMcp: () => interactiveMcpServers(user, sessionId), | ||
| inProcessMcp: () => | ||
| interactiveMcpServers( | ||
| user, | ||
| sessionId, | ||
| personalMcpScopeForSession(findSession(sessionId)), | ||
| ), | ||
| }), | ||
| // Per-session scratch assets (previewed in the Assets tab). | ||
| // Works in Ask mode — writes land outside the checkout. | ||
|
|
@@ -376,7 +415,14 @@ registerInteractiveMcpBuilder((sessionId, user) => { | |
| if (sessionId && session?.automation) { | ||
| return automationSessionMcp(session, sessionId); | ||
| } | ||
| const servers = interactiveMcpServers(user, sessionId); | ||
| // Old feed sessions can predate the persisted allowlist. The asynchronous | ||
| // launch path still resolves their external connectors, but this fallback | ||
| // builder cannot; fail closed for personal proxies rather than widening. | ||
| const servers = interactiveMcpServers( | ||
| user, | ||
| sessionId, | ||
| personalMcpScopeForSession(session), | ||
| ); | ||
| const goalId = session?.goalId; | ||
| if (goalId) | ||
| (servers as Record<string, unknown>)["opensession-goal-self"] = | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔴 P1 — Pass the personal MCP scope from every run launcher
Personal proxies are now opt-in, but
session-create.ts:644still callsinteractiveMcpServers(spec.user, bksId)andrunner-session.ts:78does the same for every Runner turn. MeanwhilebuildOpencodeMcpConfigremoves any server for which that user has a grant. Therefore a new local session's opening prompt, and every Runner prompt, gets neither the external server nor its proxy. For example, an opening request to post through a connected Slack account has no Slack tools, although a later ordinary local prompt does. Passspec.runMcpServers ?? "all"andopts.mcpServers ?? "all"at those call sites and add coverage for both launch paths.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 9720bbb, and this one was worse than reported. Rather than patch the two call sites, I made
personalMcpScopea required parameter, because the failure is silent in both directions:buildOpencodeMcpConfigdrops a granted server assuming a proxy replaces it, so a launcher that forgets the scope gets neither. Making it required had the compiler find two more launchers you had not flagged, inopensession.ts's resume path. Those and the run-rpc fallback builder now share one derivation,personalMcpScopeForSession. Desk voice passesundefinedexplicitly to keep its narrow facade. On coverage: I did not add per-launcher tests, since the type signature now enforces this at every present and future call site, which a test enumerating today's launchers would not.