Skip to content

feat(mcp): isolate OAuth authorization by requesting user - #1

Draft
yu-iskw wants to merge 16 commits into
mainfrom
feat/mcp-per-user-oauth-scope
Draft

yu-iskw wants to merge 16 commits into
mainfrom
feat/mcp-per-user-oauth-scope

Conversation

@yu-iskw

@yu-iskw yu-iskw commented Aug 26, 2026

Copy link
Copy Markdown
Owner

Summary

Implements the security boundary from NousResearch#78174 against the current fork codebase rather than rebasing the outdated NousResearch#79449 implementation.

This draft intentionally focuses on NousResearch#78174 only. Headless consent delivery from NousResearch#78169 is left for a follow-up change after the credential/connection boundary is stable.

Security model

  • mcp.oauth.identity_mode supports backwards-compatible shared and opt-in per_user.
  • Per-user identity is derived only from task-local authenticated gateway context: platform + platform scope/workspace + user id.
  • Explicit invalid identity-mode configuration fails closed rather than silently downgrading to shared credentials.
  • Raw human identifiers are not used as storage/registry keys; a versioned SHA-256 principal digest is used instead.
  • Missing identity in per_user mode fails closed. There is no shared-token or other-user connection fallback.
  • MCP tool arguments are left intact; model-visible user_id or similar business arguments never select OAuth credentials.

Isolation covered

  • OAuth access/refresh token persistence
  • dynamic client registration state
  • OAuth metadata
  • CIMD refusal state
  • provider cache
  • refresh/disk-watch state
  • 401 dedup locks/futures
  • long-lived MCPServerTask lookup
  • reusable lazy connection config for additional users

Validation added

Adversarial tests cover:

  • Alice/Bob isolation for the same MCP server
  • same platform user id across different Slack workspaces
  • missing identity and stale process-global env fail closed
  • raw identity does not appear in storage paths
  • shared mode preserves historical paths/registry keys
  • legacy shared token is not silently adopted by a per-user principal
  • non-OAuth MCP connections retain existing shared semantics
  • provider lock / pending-401 state is distinct per principal

Follow-up before marking ready

  • Run/inspect existing MCP OAuth regression suite and repository CI.
  • Review current mcp_tool.py lifecycle/status/circuit-breaker behavior under multiple scoped connections and scope any remaining cross-user availability state where necessary.
  • Isolate private MCP schema-cache entries for shared gateway deployments.
  • Add operator-facing config/docs and explicit CLI administrative identity selection if required.

Refs NousResearch#78174.

@yu-iskw

yu-iskw commented Aug 27, 2026

Copy link
Copy Markdown
Owner Author

@codex[agent] review

@Codex

Codex AI commented Aug 27, 2026

Copy link
Copy Markdown

@codex[agent] review

Review completed on 41c5fa0. I found no blocking issues in the per-user OAuth isolation changes across identity resolution, storage, runtime registry scoping, manager state, and schema-cache scoping. I couldn’t execute the targeted test files in this environment because no pytest-enabled virtualenv is available.

@chatgpt-codex-connector

Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 41c5fa089e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +59 to +61
@staticmethod
def _resolve_scope(oauth_scope: McpOAuthScope | None) -> McpOAuthScope:
return oauth_scope if oauth_scope is not None else resolve_oauth_scope()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Pass the requesting scope into OAuth login workers

With mcp.oauth.identity_mode: per_user, resolving an omitted scope here makes every existing interactive authorization path fail before it can obtain tokens: hermes_cli.mcp_config._reauth_oauth_server() calls manager.remove() without a bound gateway identity, and tui_gateway.mcp_oauth_sessions.start_flow() starts _worker in a new thread that neither inherits session ContextVars nor supplies oauth_scope to remove() or the subsequent probe. The dashboard/TUI flow therefore reports a missing authenticated principal, while CLI login cannot select a principal at all; propagate a validated requesting scope through these flows and use scoped storage for their snapshot/verification steps.

AGENTS.md reference: AGENTS.md:L84-L87

Useful? React with 👍 / 👎.

Comment on lines +301 to +305
with _INSTALL_LOCK:
servers = mcp_tool._servers
if not isinstance(servers, ScopedMCPServerRegistry):
servers = ScopedMCPServerRegistry(dict(servers))
mcp_tool._servers = servers

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Scope live MCP tool registrations by user

When an OAuth server returns identity-dependent tool lists, these adapters isolate the connection and health maps but leave tools.registry.registry process-global. Each scoped connection still calls _register_server_tools(): a later user's discovery overwrites shared schemas for matching names, while tools available only to an earlier user remain registered and pass the server-level check_fn whenever the later user's connection is live. This exposes another user's capability metadata, sends unsupported calls to the current user's server, and mutates tool schemas underneath existing conversation snapshots; the live tool registry and stale-tool reconciliation need the same principal boundary as _servers.

AGENTS.md reference: AGENTS.md:L19-L23

Useful? React with 👍 / 👎.

Comment thread tools/mcp_schema_cache.py
Comment on lines +78 to +84
from hermes_cli.config import load_config

servers = (load_config() or {}).get("mcp_servers") or {}
config = servers.get(server_name) if isinstance(servers, dict) else None
return (
isinstance(config, dict)
and str(config.get("auth") or "").strip().lower() == "oauth"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Recognize portable OAuth servers when scoping schema caches

For an OAuth MCP supplied by a portable plugin, _load_mcp_config() merges PluginManager.get_portable_mcp_servers(), but this predicate examines only load_config()["mcp_servers"]. It consequently classifies that OAuth server as non-OAuth and reads/writes the legacy shared cache key even in per_user mode, allowing one gateway user's private tool manifest to be registered for another; determine OAuth status from the resolved server config passed through discovery or include the portable-server source in this lookup.

AGENTS.md reference: AGENTS.md:L84-L87

Useful? React with 👍 / 👎.

yu-iskw pushed a commit that referenced this pull request Aug 31, 2026
fal's post-trained H3 variant — #1-ranked quality/prompt adherence/
aesthetics, 5s 768p video in under 3 seconds, $0.04/s launch pricing.

- New minimax-h3-max family: minimax/h3-max/{text,image}-to-video
- Inherits base-H3 wire quirks (integer duration, i2v drops
  aspect_ratio) but caps at 768P (480P/768P enums, no 2K/4K) and
  declares seed on both endpoints
- New generic static_payload family flag: constant keys the endpoint
  requires on every request (H3 Max lists prompt_expansion_mode in its
  required array; sent as 'balanced')

Payload asserted against the endpoint OpenAPI schema; 73/73 targeted
tests green (surface matrix auto-covers the new family).
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.

2 participants