Skip to content

feat(mcp): add sandbox-safe OAuth and failure-aware routing / 添加沙箱安全的 MCP OAuth 与失败路由 - #7932

Merged
SivanCola merged 12 commits into
esengine:main-v2from
SivanCola:feature/mcp-oauth-sandbox-auth
Aug 8, 2026
Merged

feat(mcp): add sandbox-safe OAuth and failure-aware routing / 添加沙箱安全的 MCP OAuth 与失败路由#7932
SivanCola merged 12 commits into
esengine:main-v2from
SivanCola:feature/mcp-oauth-sandbox-auth

Conversation

@SivanCola

@SivanCola SivanCola commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

Add first-party, user-initiated OAuth authorization for remote Streamable HTTP MCP servers, with private Reasonix-owned state that works from sandboxed Desktop and CLI sessions. Also prevent failed or disabled cached MCP tools from being preferred by capability routing.

Problem

Users could configure remote MCP servers such as Figma, but Desktop and CLI sessions could not reuse credentials stored by another client or keychain path. The resulting authentication failure was presented as a sandbox limitation, while cached tools from the failed server could still be selected by capability routing.

Root cause

  • Reasonix had no native OAuth authorization-code flow for remote MCP.
  • OAuth client credentials and tokens had no Reasonix-private persistence boundary.
  • Cached MCP tool entries did not inherit failed or disabled server lifecycle state.

Changes

  • Add RFC 9728 Protected Resource Metadata and RFC 8414 Authorization Server Metadata discovery.
  • Add dynamic client registration, PKCE S256, loopback callback, resource indicators, authorization-code exchange, refresh-token rotation, and RFC 6749 client_secret_basic encoding.
  • Add reasonix mcp auth , TUI sign-in and reconnect, and Desktop AuthenticateMCPServer plus Sign in handling.
  • Carry the resolved Reasonix proxy transport into OAuth discovery, registration, and token requests.
  • Persist versioned OAuth state with mode 0600 outside the workspace, scoped to a workspace/server private directory and bound to the full resource URL.
  • Serialize authorization and refresh rotation with a bounded cross-process file lock.
  • Reload state under the refresh lock so concurrent processes reuse the newest rotated token.
  • Prevent a stale transport from restoring OAuth state after Clear authentication.
  • Propagate failed and disabled MCP server lifecycle state to cached tool entries and exclude failed entries from routing.
  • Document the English and Chinese user flows and security boundaries.

User flow

  • CLI: run reasonix mcp auth .
  • Desktop: open the MCP panel and click Sign in.
  • TUI: choose the authentication action from /mcp.
  • After authorization, Reasonix stores its own private state and reconnects the server.
  • Clear authentication removes only Reasonix local OAuth state; it does not sign out the third-party browser session.
  • Browser launch is always tied to an explicit user action, never a background tool-call 401.

Security

  • Static Authorization headers remain authoritative and disable Reasonix OAuth for that server.
  • Tokens and dynamic client secrets are stored outside the workspace with mode 0600.
  • Stored tokens are rejected when the configured resource URL changes.
  • OAuth redirects are restricted to the original HTTP origin.
  • OAuth error bodies and callback errors pass through credential redaction.
  • Refresh-token rotation is serialized across processes; lock waits are context-bounded.
  • No new dependency or custom cryptographic implementation is introduced.

Compatibility

  • Existing static-header MCP configurations keep their current behavior and take precedence.
  • The new oauth.json and lock sidecar are private, versioned Reasonix runtime state; older versions ignore them.
  • No OAuth or JWT fields are added to config.toml or .mcp.json contracts.
  • The Desktop bridge only adds AuthenticateMCPServer and does not change existing payload shapes.
  • Changing a remote MCP URL does not reuse credentials for the old resource.

Cache and documentation impact

Cache-impact: none - OAuth clients, token state, and failure lifecycle are runtime-only; MCP tool names, schemas, descriptions, registration order, and provider-visible prompt prefixes are unchanged.

Cache-guard: go test ./internal/plugin ./internal/capability verifies schema-cache isolation, runtime identity, cached failure propagation, and failed-route exclusion.

System-prompt-review: self-reviewed - internal/boot changes only inject a host-local proxy-aware OAuth client and do not change system-prompt assembly or provider-visible schemas.

Documentation-impact: updated - English and Chinese GUIDE and SPEC now document the OAuth sign-in flow, proxy behavior, private state, static-header precedence, and clear-authentication boundary.

Failed or disabled cached tools remain in the catalog for diagnostics but are no longer routing candidates.

Verification

  • go test -race ./internal/plugin ./internal/capability ./internal/boot ./internal/cli
  • go test ./...
  • cd desktop && go test ./...
  • cd desktop/frontend && pnpm exec tsx src/tests/capabilities-panel-actions.test.ts
  • cd desktop/frontend && pnpm typecheck
  • cd desktop/frontend && pnpm lint:hooks
  • go vet ./...
  • git diff --check
  • gofmt -l . returned no files
  • go run ./tools/repolint

Consolidation of #6759

This PR is an integration and superseding implementation based on current main-v2.

Kept or adapted from #6759 by @andrei-kiparuk:

  • proxy-aware OAuth HTTP transport
  • serialized refresh-token rotation
  • RFC 7636 known-answer coverage
  • RFC 6749 client_secret_basic credential encoding
  • dedicated user-facing OAuth documentation

Reviewed but not adopted:

  • automatic browser launch from a tool-call 401
  • origin-wide shared credentials
  • JWT and private-key assertion configuration plus a custom JOSE implementation
  • fallback to an unregistered default public client
  • OAuth overriding an explicit static Authorization header
  • silently treating a damaged token store as empty

The narrower design keeps user interaction explicit, binds credentials to the full MCP resource and workspace/server private state, preserves existing static headers, fails closed for damaged state, and avoids expanding the cryptographic and configuration surface.

Refs #6759

Contributors

@SivanCola
SivanCola requested a review from esengine as a code owner August 8, 2026 04:52
@github-actions github-actions Bot added v2 Go rewrite (1.x) — main-v2 branch, active development desktop Wails desktop app (desktop/**) tui Terminal UI / CLI (internal/cli, internal/control) mcp MCP servers / plugins (internal/plugin, codegraph) config Configuration & setup (internal/config) labels Aug 8, 2026
@SivanCola
SivanCola force-pushed the feature/mcp-oauth-sandbox-auth branch from 1be2309 to b1783f0 Compare August 8, 2026 05:16
Problem:
- Remote MCP OAuth depended on externally injected credentials that sandboxed desktop and CLI sessions could not access.
- Failed cached MCP tools could still be preferred by capability routing.

Root cause:
- Reasonix had no first-party user-initiated OAuth flow or private credential state.
- Cached tool entries did not inherit failed or disabled server status.

Fix:
- Add standards-based OAuth discovery, DCR, PKCE, refresh rotation, proxy-aware HTTP, and resource-bound private state.
- Serialize token rotation across processes and prevent stale transports from restoring cleared state.
- Add CLI, TUI, and Desktop sign-in and reconnect flows.
- Propagate failed and disabled lifecycle state to cached MCP tools.

Verification:
- go test -race ./internal/plugin ./internal/capability ./internal/boot ./internal/cli
- go test ./...
- (cd desktop && go test ./...)
- (cd desktop/frontend && pnpm exec tsx src/__tests__/capabilities-panel-actions.test.ts)
- (cd desktop/frontend && pnpm typecheck)
- (cd desktop/frontend && pnpm lint:hooks)
- go vet ./...
- git diff --check

Co-authored-by: andrei-kiparuk <88461327+andrei-kiparuk@users.noreply.github.com>
@SivanCola
SivanCola force-pushed the feature/mcp-oauth-sandbox-auth branch from b1783f0 to 1c30748 Compare August 8, 2026 05:21
Problem:
- Failed or disabled cached MCP tools could remain ready after catalog deduplication.
- stdio, legacy SSE, and statically authenticated servers could enter the HTTP OAuth flow.
- Removing an MCP declaration retained Reasonix OAuth client and token state.

Root cause:
- Registry-backed MCP tool entries did not inherit host failure state.
- Authentication diagnosis and UI actions trusted auth failures before checking transport and configured credentials.
- Remove paths did not reconcile private OAuth state with the effective fallback declaration.

Fix:
- Propagate failed and disabled status through the real catalog assembly path.
- Gate native OAuth consistently in diagnostics, CLI, TUI, Desktop, frontend, and the plugin owner.
- Reconcile OAuth state under the cross-process lock, preserving only same-resource fallbacks and covering open Desktop workspaces.

Verification:
- go test ./...
- go test -race ./internal/plugin ./internal/capability ./internal/mcpdiag ./internal/cli ./internal/control
- cd desktop && go test ./...
- cd desktop && go test -race ./...
- frontend focused test, typecheck, ESLint, and CSS checks
- go vet ./...
- go run ./tools/repolint
- git diff --check
@github-actions github-actions Bot added the agent Core agent loop (internal/agent, internal/control) label Aug 8, 2026
SivanCola and others added 6 commits August 8, 2026 15:58
Problem:
- The Desktop Linux and macOS CI jobs rejected two test fixtures with the fmtappendf modernize rule.

Root cause:
- The fixtures converted fmt.Sprintf output with []byte instead of appending formatted bytes directly.

Fix:
- Build the OAuth JSON fixtures with fmt.Appendf.

Verification:
- cd desktop && go test ./... -run TestRemoveMCPServerReconcilesOAuthState -count=1
- cd desktop && golangci-lint run --timeout=5m ./...
- go run ./tools/repolint
- git diff --check
Problem:
- OAuth authorization held the MCP state lock across browser interaction and could race with removal.
- Existing OAuth state could be loaded alongside static MCP credentials.

Root cause:
- The authorization flow used one lock scope for the entire interactive exchange and had no invalidation generation.
- HTTP transport only excluded OAuth when an Authorization header was present.

Fix:
- Capture and compare a private OAuth generation around interactive authorization, and advance it during removal reconciliation.
- Treat all explicit MCP authentication configuration as authoritative when constructing HTTP transports.
- Add deterministic regression coverage for browser-lock release, removal invalidation, fallback reconciliation, and static API keys.

Verification:
- go test -race ./internal/plugin ./internal/mcpdiag
- go test ./internal/cli ./internal/control ./internal/capability
- go test ./...
- cd desktop && go test ./...
- git diff --check
Problem:
- Explicit URL and header credentials could be combined with stale OAuth state.
- Remote HTTP endpoints and failed semantic IDs could be offered as usable capabilities.
- Token refresh held the cross-process state lock during network I/O.

Root cause:
- OAuth eligibility used incomplete string heuristics and refresh used one lock scope for read, request, and write.

Fix:
- Parse URL userinfo/query credentials and sensitive headers, restrict OAuth to HTTPS or loopback HTTP, and filter failed/disabled semantic candidates.
- Add a process-local refresh gate plus two-phase locked compare-and-apply with generation checks.
- Add regression coverage for credentials, transport eligibility, routing status, and clear-during-refresh.

Verification:
- go test -race ./internal/plugin ./internal/capability ./internal/mcpdiag ./internal/control ./internal/cli
- go vet ./...
- go run ./tools/repolint
- pnpm exec tsx src/__tests__/mcp-oauth-eligibility.test.ts
- pnpm typecheck

Co-authored-by: Andrei Kiparuk <88461327+andrei-kiparuk@users.noreply.github.com>
Problem:
Full-suite macOS sandbox probes could time out while desktop filesystem tests could fail during temporary-directory teardown with EBADF.

Root cause:
The sandbox tests depended on process-global PATH and a two-second shell probe, while atomic file paths used repeated close paths and affected desktop tests used fragile t.TempDir cleanup.

Fix:
Probe sandbox-exec by explicit path with an absolute probe command, avoid caching transient timeouts, close temporary files exactly once, and use robustTempDir for the affected desktop filesystem tests.

Verification:
Focused tests and race tests pass; root go test ./..., desktop go test ./..., go vet ./... in both modules, and repolint pass.
@github-actions github-actions Bot added the updater Auto-update / installer / release packaging label Aug 8, 2026
Problem:
The PR merge commit exceeded the main-v2 controller file-size budget by one line, blocking the repository standards job.

Root cause:
The latest main-v2 reduced the carried-forward budget while this branch retained a two-line dispatcher comment.

Fix:
Keep the same dispatcher documentation in one line without changing behavior.

Verification:
`go run ./tools/repolint` and `go test ./internal/control` pass.
Resolve the latest main-v2 controller/runtime changes in favor of the current base implementation while retaining the MCP OAuth and sandbox fixes.
Problem: Controller.RemoveMCPServer left the OAuth removal reconciler unused after the main-v2 merge, so the controller path could retain stale Reasonix OAuth state and failed lint.

Root cause: The merge kept the pre-OAuth fallback lookup and nil cleanup return instead of consuming the reconciled post-removal configuration.

Fix: Reconcile OAuth state immediately after persistence removal, restore the resolved fallback from that state, and return cleanup errors to the caller while preserving the lint budget.

Verification: go test ./internal/control ./internal/cli; golangci-lint run --timeout=5m; go run ./tools/repolint; git diff --check.
Problem: The first lint fix removed a function separator to stay within the controller file budget, which made the file fail the CI gofmt check.

Root cause: The file-size budget was reduced by a comment change without restoring gofmt-required spacing.

Fix: Restore gofmt output and shorten the adjacent MCP removal documentation by one line, preserving the exact removal and OAuth reconciliation behavior.

Verification: gofmt -l internal/control/controller.go; go test ./internal/control ./internal/cli; golangci-lint run --timeout=5m; go run ./tools/repolint; git diff --check.
@SivanCola
SivanCola merged commit 6c68927 into esengine:main-v2 Aug 8, 2026
22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent Core agent loop (internal/agent, internal/control) config Configuration & setup (internal/config) desktop Wails desktop app (desktop/**) mcp MCP servers / plugins (internal/plugin, codegraph) tui Terminal UI / CLI (internal/cli, internal/control) updater Auto-update / installer / release packaging v2 Go rewrite (1.x) — main-v2 branch, active development

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant