Skip to content

feat(connector-mcp): policy-gate declarative stdio transports — default-deny + host allowlist (#3055)#3059

Merged
os-zhuang merged 3 commits into
mainfrom
claude/adr-0097-stdio-gate
Jul 16, 2026
Merged

feat(connector-mcp): policy-gate declarative stdio transports — default-deny + host allowlist (#3055)#3059
os-zhuang merged 3 commits into
mainfrom
claude/adr-0097-stdio-gate

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

实现 #3055(ADR-0097 安全后续):声明式 provider: 'mcp' 实例的 stdio 传输默认拒绝,宿主按命令允许清单显式 opt-in——这是把 connector-mcp 收编进默认预设(#3056)的前置安全闸。

⚠️ Stacked PR:base 是 #3049 的分支 claude/adr-0097-mcp-nonfatal-boot,diff 只含本闸的增量。#3049 squash 合并后我会把 base retarget 到 main 并 rebase。请先审/合 #3049

威胁模型

声明式条目经由元数据到达——包括运行期的 Studio publish(metadata:reloaded reconcile 会 materialize 新实例;soft 模式跳过的是失败,不是成功)。而 stdio 传输的 materialization 会 spawn 本地子进程(StdioClientTransport)。两者相乘:能发布元数据 ≈ 能在服务器上执行任意命令。今天的"屏障"只是宿主恰好没装 connector-mcp——#3056 一旦落地这道屏障就消失,必须换成显式策略。

变更

createMcpProviderFactory 新增 declarativeStdio 策略(默认拒绝),ConnectorMcpPlugin 透传:

new ConnectorMcpPlugin({ declarativeStdio: ['my-mcp-server'] })  // 命令允许清单(严格相等)
new ConnectorMcpPlugin({ declarativeStdio: true })               // 显式完全信任
// 缺省 / false → 拒绝
  • 违规 = 配置错误:普通抛出(boot 致命 / reload 记录并跳过),错误信息给出确切的 opt-in 写法;绝不归类为 CONNECTOR_UPSTREAM_UNAVAILABLE(feat(connectors): degrade + retry declarative instances on unreachable upstream (#3017) #3049 的分类接缝)——安全拒绝不能被重试"重试出来"。拒绝发生在任何连接尝试之前(测试有断言)。
  • http 传输不受影响(与 http 节点同一信任等级)。
  • 手写路径不受影响(插件实例 options / createMcpConnector):命令写在宿主代码里,信任锚不同——既有 e2e(手写 stdio、无策略)原样通过,并加注释作为回归绊线。
  • 新导出 McpDeclarativeStdioPolicy;ADR-0097 新增 §"Declarative stdio policy"(交叉引用 ADR-0024 §4;如实说明允许清单是粗粒度边界——放行 npx ≈ 放行它能跑的任意包,应列出具体信任的 server 二进制;沙箱化执行仍属企业层)。

测试(23/23 通过,+6 新增)

  • 默认拒绝:普通错误(非 unavailable)、信息含可操作 opt-in 提示、未发起任何连接;
  • 允许清单命中放行 / 未命中拒绝(错误列出清单);true 放行任意命令;
  • http 在默认拒绝策略下照常 materialize;
  • 插件选项透传(stub 捕获注册的 factory,验证清单行为);
  • 手写 stdio e2e 无策略照常注册、派发、关闭。

既有 stdio 用例改为显式 declarativeStdio opt-in(行为变更的如实体现);changeset(minor,标注行为变更)。

Closes #3055。Refs #3049 / #3056 / ADR-0097 / ADR-0024 §4。

🤖 Generated with Claude Code

https://claude.ai/code/session_01Pbmw3pMqNJfPbkvwh9Fkjs


Generated by Claude Code

claude added 2 commits July 16, 2026 11:31
…e upstream (#3017)

ADR-0097 made every declarative-connector materialization failure fatal at
boot — right for configuration faults, wrong for operational ones: a
provider: 'mcp' instance must contact its MCP server (tools/list) to
materialize, so a transient network blip aborted the whole app boot.

- spec: ConnectorUpstreamUnavailableError (code CONNECTOR_UPSTREAM_UNAVAILABLE,
  structural guard isConnectorUpstreamUnavailable) lets a provider factory mark
  a failure as 'upstream temporarily unreachable — degrade and retry' instead
  of fatal. New integration/connector-provider-errors.ts; api-surface +3.
- service-automation: the reconcile degrades such instances in BOTH modes.
  With no live connector under the name it registers an action-less husk —
  state: 'degraded' + degradedReason on the GET /connectors descriptor,
  status: 'error' on the def — so the instance stays visible instead of
  silently missing; connector_action dispatch fails with the reason and a
  'retries automatically' pointer. On a changed-config re-materialization the
  old connector keeps serving. Degraded instances retry on an exponential
  backoff (5s doubling to 5min, reset by config edits) and on every
  metadata:reloaded reconcile; recovery swaps the husk atomically. Reconcile
  runs (boot / reload / retry timer) are serialized; destroy() cancels the
  retry loop.
- connector-mcp: connect / tools/list failures are classified unavailable;
  transport-shape validation stays a plain (fatal) throw.

Configuration faults (unknown provider, invalid providerConfig, unresolvable
credentialRef, name conflicts) keep the ADR-0097 fail-loud contract, verified
by the existing tests. ADR-0097 gains an 'Upstream availability' section; the
deferred live-mcp showcase demo and the openapi remote-URL classification are
recorded as scope boundaries.

Tests: 10 new reconcile cases (degrade visibility, pointed dispatch error,
fake-timer recovery, exponential backoff, reload retry, removal while
degraded, changed-config keeps old serving, revert cancels retry, shutdown
cancels retry, plain-throw still fatal); 3 new mcp-provider classification
cases; 3 new spec error-contract cases.

Refs #3017 (mechanism; showcase demo deferred — see ADR scope boundaries)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pbmw3pMqNJfPbkvwh9Fkjs
…lt-deny + host allowlist (#3055)

A declarative provider: 'mcp' entry with a stdio transport spawns a local
child process from METADATA — which a runtime Studio publish can introduce
(the reload reconcile materializes new instances; soft mode skips failures,
not successes). Anyone who can publish metadata could otherwise execute
arbitrary commands on the server. Declarative stdio is now denied by default
and opt-in per host:

  new ConnectorMcpPlugin({ declarativeStdio: ['my-mcp-server'] })  // allowlist
  new ConnectorMcpPlugin({ declarativeStdio: true })               // full trust

- createMcpProviderFactory gains a declarativeStdio policy (undefined/false =
  deny, string[] = strict-equality command allowlist, true = allow any);
  ConnectorMcpPlugin plumbs it through. New export McpDeclarativeStdioPolicy.
- A violation is a CONFIGURATION fault: plain throw with an actionable opt-in
  message (fatal at boot, skipped+logged on reload) — never classified
  CONNECTOR_UPSTREAM_UNAVAILABLE, so a security rejection cannot be retried
  into existence (#3049 seam).
- http transports are unaffected (same trust class as the http node), and so
  are hand-wired connectors (plugin instance options / createMcpConnector) —
  their command lives in host code, a different trust anchor than metadata.
- ADR-0097 gains a 'Declarative stdio policy' section (cross-ref ADR-0024 §4,
  honest about the allowlist being a coarse boundary); this gate is the
  precondition for shipping connector-mcp in default presets (#3056).

Tests: default-deny (plain, non-retryable, rejected before any connection
attempt), allowlist hit/miss, true passthrough, http unaffected, plugin
option plumb-through, hand-wired stdio path explicitly un-gated (e2e).

Refs #3055. Stacked on #3049 (claude/adr-0097-mcp-nonfatal-boot).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pbmw3pMqNJfPbkvwh9Fkjs
@vercel

vercel Bot commented Jul 16, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
spec Ready Ready Preview, Comment Jul 16, 2026 2:28pm

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling size/m labels Jul 16, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

No hand-written docs reference the 1 changed package(s). ✅

Base automatically changed from claude/adr-0097-mcp-nonfatal-boot to main July 16, 2026 13:15
#3049's squash (4f8c2d1) is content-identical to this branch's base commit,
so this merge only rebases the PR surface — the three-dot diff vs main now
shows the #3055 gate alone.

# Conflicts:
#	docs/adr/0097-declarative-connector-instances.md
#	packages/connectors/connector-mcp/src/mcp-provider.test.ts
#	packages/connectors/connector-mcp/src/mcp-provider.ts
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(connector-mcp): policy-gate declarative stdio transports — default-deny + host allowlist (ADR-0097 security follow-up)

2 participants