guardrail(SSOT): pin required_tool + loaded_mcp_tools_field in the MCP delivery contract #3258
@@ -1 +1 @@
|
||||
{"settings_path":"/configs/.claude/settings.json","key":"mcpServers","entry_shape":"name->{command,args?,env?}","mcp_server_name":"molecule-platform","legacy_binary_path":"/opt/molecule-mcp-server","runtime_present_field":"mcp_server_present","producer":"MCPServerAdaptor","consumer":"claude_sdk_executor._load_settings_mcp","consumers":["claude_sdk_executor._load_settings_mcp","platform_agent_identity.mcp_server_present","workspace-server/internal/handlers/registry.go#RCA2970-online-gate"],"descriptor":"runtime-agnostic name->{command,args?,env?}; the plugin is the SSOT for this descriptor (settings-fragment.json is the Claude adapter's rendering of it). The MCP-wiring PORT (InstallContext.register_mcp_server -> BaseAdapter.register_mcp_server_hook) renders the descriptor into the native config the ACTIVE runtime reads.","port":{"hook":"InstallContext.register_mcp_server","impl":"BaseAdapter.register_mcp_server_hook","present_probe":"BaseAdapter.management_mcp_present","dispatch":"BaseAdapter default hook dispatches on self.name() via mcp_render.render_for_runtime/mcp_settings_path_for/management_mcp_present_for; per-template adapter override NOT required.","resolver_default":"plugins_registry.resolve defaults an mcpServers-shaped plugin to MCPServerAdaptor (AdaptorSource.MCP_SERVER) for ANY runtime, so molecule-platform-mcp needs no per-runtime adapters/<runtime>.py."},"runtimes":{"claude_code":{"settings_path":"/configs/.claude/settings.json","format":"json","key":"mcpServers","renderer":"mcp_render.render_claude_settings","status":"implemented"},"codex":{"settings_path":"~/.codex/config.toml","format":"toml","table":"mcp_servers","renderer":"mcp_render.render_codex_config","status":"implemented"},"gemini_cli":{"settings_path":"~/.gemini/settings.json","format":"json","key":"mcpServers","renderer":"mcp_render.render_gemini_settings","status":"todo-unverified"},"hermes":{"settings_path":"unverified","format":"unverified","renderer":"mcp_render.render_hermes_config","status":"todo-unverified"}}}
|
||||
{"settings_path":"/configs/.claude/settings.json","key":"mcpServers","entry_shape":"name->{command,args?,env?}","mcp_server_name":"molecule-platform","required_tool":"create_workspace","loaded_mcp_tools_field":"loaded_mcp_tools","legacy_binary_path":"/opt/molecule-mcp-server","runtime_present_field":"mcp_server_present","producer":"MCPServerAdaptor","consumer":"claude_sdk_executor._load_settings_mcp","consumers":["claude_sdk_executor._load_settings_mcp","platform_agent_identity.mcp_server_present","workspace-server/internal/handlers/registry.go#RCA2970-online-gate"],"descriptor":"runtime-agnostic name->{command,args?,env?}; the plugin is the SSOT for this descriptor (settings-fragment.json is the Claude adapter's rendering of it). The MCP-wiring PORT (InstallContext.register_mcp_server -> BaseAdapter.register_mcp_server_hook) renders the descriptor into the native config the ACTIVE runtime reads.","port":{"hook":"InstallContext.register_mcp_server","impl":"BaseAdapter.register_mcp_server_hook","present_probe":"BaseAdapter.management_mcp_present","dispatch":"BaseAdapter default hook dispatches on self.name() via mcp_render.render_for_runtime/mcp_settings_path_for/management_mcp_present_for; per-template adapter override NOT required.","resolver_default":"plugins_registry.resolve defaults an mcpServers-shaped plugin to MCPServerAdaptor (AdaptorSource.MCP_SERVER) for ANY runtime, so molecule-platform-mcp needs no per-runtime adapters/<runtime>.py."},"runtimes":{"claude_code":{"settings_path":"/configs/.claude/settings.json","format":"json","key":"mcpServers","renderer":"mcp_render.render_claude_settings","status":"implemented"},"codex":{"settings_path":"~/.codex/config.toml","format":"toml","table":"mcp_servers","renderer":"mcp_render.render_codex_config","status":"implemented"},"gemini_cli":{"settings_path":"~/.gemini/settings.json","format":"json","key":"mcpServers","renderer":"mcp_render.render_gemini_settings","status":"todo-unverified"},"hermes":{"settings_path":"unverified","format":"unverified","renderer":"mcp_render.render_hermes_config","status":"todo-unverified"}}}
|
||||
|
||||
@@ -32,6 +32,21 @@ type MCPPluginDeliveryContract struct {
|
||||
// TestSSOT_DegradeGateToolDerivesFromContract.
|
||||
MCPServerName string `json:"mcp_server_name"`
|
||||
|
||||
// RequiredTool is the SSOT for the management MCP's REQUIRED tool VERB (e.g.
|
||||
// "create_workspace"). Combined with MCPServerName it yields the full
|
||||
// dispatcher id the online/degraded gate looks for:
|
||||
// `mcp__<MCPServerName>__<RequiredTool>`. Pinning the verb here (not as a
|
||||
// hardcoded literal in the gate const or the test) closes the SSOT gap the
|
||||
// 2026-06-25 audit flagged: previously only the `mcp__<server>__` prefix was
|
||||
// contract-derived while `create_workspace` was re-spelled in both the Go
|
||||
// const and the test. See TestSSOT_DegradeGateToolDerivesFromContract.
|
||||
RequiredTool string `json:"required_tool"`
|
||||
|
||||
// LoadedMCPToolsField is the SSOT for the register/heartbeat status-field NAME
|
||||
// the runtime emits (the loaded MCP tool inventory) and core's #3082 gate
|
||||
// consumes. Pinned so a rename on either side is caught by the drift gate.
|
||||
LoadedMCPToolsField string `json:"loaded_mcp_tools_field"`
|
||||
|
||||
// Descriptor is the prose SSOT describing the runtime-agnostic MCP descriptor
|
||||
// shape and the wiring-PORT indirection (register_mcp_server →
|
||||
// register_mcp_server_hook). It is pinned so a refactor that collapses the
|
||||
@@ -105,6 +120,8 @@ func (c *MCPPluginDeliveryContract) MatchesSSOT() []string {
|
||||
eq("producer", c.Producer, "MCPServerAdaptor")
|
||||
eq("consumer", c.Consumer, "claude_sdk_executor._load_settings_mcp")
|
||||
eq("mcp_server_name", c.MCPServerName, "molecule-platform")
|
||||
eq("required_tool", c.RequiredTool, "create_workspace")
|
||||
eq("loaded_mcp_tools_field", c.LoadedMCPToolsField, "loaded_mcp_tools")
|
||||
|
||||
// PORT symbols (#3159). These pin the wiring seam: if the adaptor regresses
|
||||
// to a hard-coded Claude write, the hook/impl indirection disappears and this
|
||||
|
||||
@@ -48,11 +48,26 @@ func TestSSOT_DegradeGateToolDerivesFromContract(t *testing.T) {
|
||||
if c.MCPServerName == "" {
|
||||
t.Fatal("contract mcp_server_name is empty — SSOT for the platform MCP tool prefix")
|
||||
}
|
||||
want := "mcp__" + c.MCPServerName + "__create_workspace"
|
||||
if c.RequiredTool == "" {
|
||||
t.Fatal("contract required_tool is empty — SSOT for the management MCP's required tool verb")
|
||||
}
|
||||
// The PRODUCTION gate const is COMPOSED from two building-block consts; pin
|
||||
// EACH to the contract so the gate (not just this test) can never use a
|
||||
// server name or verb that diverges from the SSOT.
|
||||
if conciergePlatformMCPServerName != c.MCPServerName {
|
||||
t.Errorf("SSOT drift: conciergePlatformMCPServerName = %q, but contract mcp_server_name = %q",
|
||||
conciergePlatformMCPServerName, c.MCPServerName)
|
||||
}
|
||||
if conciergePlatformMCPRequiredTool != c.RequiredTool {
|
||||
t.Errorf("SSOT drift: conciergePlatformMCPRequiredTool = %q, but contract required_tool = %q",
|
||||
conciergePlatformMCPRequiredTool, c.RequiredTool)
|
||||
}
|
||||
// And the composed full id must equal the contract-derived id (mcp__<server>__<verb>).
|
||||
want := "mcp__" + c.MCPServerName + "__" + c.RequiredTool
|
||||
if conciergePlatformMCPCreateWorkspaceTool != want {
|
||||
t.Errorf("SSOT drift: conciergePlatformMCPCreateWorkspaceTool = %q, but contract mcp_server_name = %q implies %q.\n"+
|
||||
"The degraded gate must look for the tool id the runtime actually emits (mcp__<server>__create_workspace).",
|
||||
conciergePlatformMCPCreateWorkspaceTool, c.MCPServerName, want)
|
||||
t.Errorf("SSOT drift: conciergePlatformMCPCreateWorkspaceTool = %q, but contract implies %q (mcp__%s__%s).\n"+
|
||||
"The degraded gate must look for the tool id the runtime actually emits (mcp__<server>__<required_tool>).",
|
||||
conciergePlatformMCPCreateWorkspaceTool, want, c.MCPServerName, c.RequiredTool)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -555,27 +555,35 @@ const conciergePlatformMCPSource = "gitea://molecule-ai/molecule-ai-plugin-molec
|
||||
// derivation, not the human label "molecule-platform-mcp".
|
||||
const conciergePlatformMCPName = "molecule-ai-plugin-molecule-platform-mcp"
|
||||
|
||||
// conciergePlatformMCPCreateWorkspaceTool is the literal MCP tool identifier
|
||||
// the platform concierge must surface for the post-online fail-loud gate
|
||||
// (core#3082) to consider the management MCP actually loaded. The Claude
|
||||
// Code dispatcher formats every MCP tool as `mcp__<server>__<tool>`; the
|
||||
// platform MCP server's install name derives to "molecule-platform" via
|
||||
// PluginNameFromSource(conciergePlatformMCPSource), so the create_workspace
|
||||
// tool's namespaced identifier is `mcp__molecule-platform__create_workspace`.
|
||||
// This is the SAME literal the staging concierge E2E (tests/e2e/test_staging_
|
||||
// concierge_creates_workspace_e2e.sh:4.5/6) probes for; pinning it as a
|
||||
// constant keeps the runtime gate and the E2E in lock-step so a drift in
|
||||
// one breaks both with the same signal.
|
||||
// The platform concierge must surface mcp__molecule-platform__create_workspace
|
||||
// for the post-online fail-loud gate (core#3082) to consider the management MCP
|
||||
// actually loaded. The Claude Code dispatcher formats every MCP tool as
|
||||
// `mcp__<server>__<tool>`.
|
||||
//
|
||||
// Why we don't match the server/plugin NAME here: the heartbeat's
|
||||
// loaded_mcp_tools list carries TOOL identifiers (mcp__<server>__<tool>),
|
||||
// not plugin names. The previous check (loadedSet[conciergePlatformMCPName])
|
||||
// was a no-op false-green — it matched the plugin NAME against a list of
|
||||
// TOOL strings, which would always be empty for the management MCP (the
|
||||
// plugin is named "molecule-ai-plugin-molecule-platform-mcp" while its
|
||||
// tools are namespaced "mcp__molecule-platform__*"). The literal-tool
|
||||
// match below is the actual contract the runtime must satisfy.
|
||||
const conciergePlatformMCPCreateWorkspaceTool = "mcp__molecule-platform__create_workspace"
|
||||
// SSOT (audit 2026-06-25): the gate tool id is COMPOSED from two building blocks,
|
||||
// each pinned to the shared contract (contracts/mcp-plugin-delivery.contract.json)
|
||||
// by TestSSOT_DegradeGateToolDerivesFromContract — there is NO standalone
|
||||
// hardcoded full tool-id and NO independently-spelled verb. The gate and the test
|
||||
// share the single `mcp__<server>__<verb>` formula and the same contract source.
|
||||
// (The contract file lives at repo root, OUTSIDE this Go module, so the embed
|
||||
// directive cannot reach it for a true runtime load; compile-time composition +
|
||||
// the dual contract-assertion test is the deployment-safe SSOT enforcement — a
|
||||
// drift on either building block vs the contract fails CI before the gate.)
|
||||
//
|
||||
// Note we match the TOOL id, not the plugin/server NAME: the heartbeat's
|
||||
// loaded_mcp_tools list carries TOOL identifiers (mcp__<server>__<tool>); a name
|
||||
// match would be a no-op false-green (the plugin is named
|
||||
// "molecule-ai-plugin-molecule-platform-mcp" while its tools are
|
||||
// "mcp__molecule-platform__*").
|
||||
|
||||
// conciergePlatformMCPServerName == contract.mcp_server_name (the mcp__<server>__
|
||||
// prefix). conciergePlatformMCPRequiredTool == contract.required_tool (the verb).
|
||||
const conciergePlatformMCPServerName = "molecule-platform"
|
||||
const conciergePlatformMCPRequiredTool = "create_workspace"
|
||||
|
||||
// conciergePlatformMCPCreateWorkspaceTool is composed from the two contract-pinned
|
||||
// constants above via the canonical mcp__<server>__<tool> formula.
|
||||
const conciergePlatformMCPCreateWorkspaceTool = "mcp__" + conciergePlatformMCPServerName + "__" + conciergePlatformMCPRequiredTool
|
||||
|
||||
// ensureConciergeProvider pins the concierge's LLM provider to `platform` (core
|
||||
// companion to ensureConciergeModel). It guarantees the env-level provider pin
|
||||
|
||||
Reference in New Issue
Block a user