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 透传:

newConnectorMcpPlugin({declarativeStdio: ['my-mcp-server']})// 命令允许清单(严格相等)newConnectorMcpPlugin({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

vercelBot commented Jul 16, 2026

Copy link
Copy Markdown

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

ProjectDeploymentActionsUpdated (UTC)
specReadyReadyPreview, CommentJul 16, 2026 2:28pm

Request Review

@github-actionsgithub-actionsBot 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 mainJuly 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
@os-zhuang
os-zhuang marked this pull request as ready for review July 16, 2026 13:31
@os-zhuang
os-zhuang merged commit e97885b into mainJul 16, 2026
15 of 16 checks passed
@os-zhuang
os-zhuang deleted the claude/adr-0097-stdio-gate branch July 16, 2026 13:31
baozhoutao pushed a commit that referenced this pull request Aug 7, 2026
Closes the two coverage holes the seed import left: nothing covered the AI
metadata kinds (agent/tool/skill, MCP surfaces) or the integration/system
services (declarative connectors, webhooks, jobs, email templates).
- areas/ai.json — agent/tool/skill metadata round-trip (variants matrix),
MCP HTTP transport both-sides (enabled 501/off + /mcp/skill public),
stdio fail-closed + RLS/FLS parity (from #3358 §9), run_action
ai.exposed gate + audit (15.1 §A9), validate_expression. Showcase ships
no AI seeds (ADR-0063) — fixture requirements declared explicitly.
- areas/integration-system.json — declarative connector lifecycle from the
15.1 §B rows (#2994/#3062 boot materialization, #3049 degraded husk +
atomic recovery, #3059 stdio default-deny allowlist, #3024 spec-path
escape rejection, #2985 descriptor-only boot audit, objectui#2563
designer picker), webhook live-fire + retired-trigger build gate, job
scheduled run, email-template variable rendering.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YD9f6FYyMraUWYeJf53V43
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

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

@os-zhuang@claude