From 5411675cc3e879d0786d6dd0dc5400642b90e6ce Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Fri, 19 Jun 2026 21:15:42 +0000 Subject: [PATCH 1/8] feat(mcp): pin MCP-plugin delivery contract between core and claude-code template (core#3080) - Add contracts/mcp-plugin-delivery.contract.json as the SSOT for the /configs/.claude/settings.json mcpServers delivery surface. - Add LoadMCPPluginDeliveryContract helper + producer-side guard test that pins settings_path, key, producer, and consumer. - Add cross-repo drift gate that byte-compares core's contract against molecule-ai-workspace-template-claude-code's canonical copy. Fixes #3080 Co-Authored-By: Claude --- .../mcp-plugin-delivery-contract-drift.yml | 91 +++++++++++++++++++ contracts/mcp-plugin-delivery.contract.json | 1 + .../handlers/mcp_plugin_delivery_contract.go | 40 ++++++++ .../mcp_plugin_delivery_contract_test.go | 41 +++++++++ 4 files changed, 173 insertions(+) create mode 100644 .gitea/workflows/mcp-plugin-delivery-contract-drift.yml create mode 100644 contracts/mcp-plugin-delivery.contract.json create mode 100644 workspace-server/internal/handlers/mcp_plugin_delivery_contract.go create mode 100644 workspace-server/internal/handlers/mcp_plugin_delivery_contract_test.go diff --git a/.gitea/workflows/mcp-plugin-delivery-contract-drift.yml b/.gitea/workflows/mcp-plugin-delivery-contract-drift.yml new file mode 100644 index 000000000..29c294ed4 --- /dev/null +++ b/.gitea/workflows/mcp-plugin-delivery-contract-drift.yml @@ -0,0 +1,91 @@ +name: mcp-plugin-delivery-contract-drift + +# Cross-repo drift gate for the MCP-plugin delivery contract (core#3080). +# +# The contract file contracts/mcp-plugin-delivery.contract.json is the SSOT +# shared between molecule-core (producer side) and +# molecule-ai-workspace-template-claude-code (consumer side). This workflow +# fetches the template's copy via the Gitea raw endpoint and byte-compares it +# against core's local copy. RED if they differ — the two repos must stay +# byte-identical so the drift gate and runtime agree on the same contract. +# +# ENFORCEMENT GATING: standalone workflow, NOT a job in ci.yml and NOT in +# branch protection (soak-then-promote; mirrors sync-providers-yaml.yml). +# +# AUTH: uses AUTO_SYNC_TOKEN (the existing cross-repo read token). If the +# secret is absent on a trusted context the job fails closed; on untrusted +# fork PRs it soft-skips. + +on: + pull_request: + types: [opened, synchronize, reopened] + paths: + - 'contracts/mcp-plugin-delivery.contract.json' + - '.gitea/workflows/mcp-plugin-delivery-contract-drift.yml' + push: + branches: [main, staging] + paths: + - 'contracts/mcp-plugin-delivery.contract.json' + - '.gitea/workflows/mcp-plugin-delivery-contract-drift.yml' + schedule: + # Daily at :27 — catch a template-side contract change that landed without + # a paired core re-sync PR (off-zero to spread cron load). + - cron: '27 4 * * *' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: mcp-plugin-delivery-contract-drift-${{ github.ref }} + cancel-in-progress: true + +jobs: + compare: + name: Compare MCP plugin delivery contract against template canonical + runs-on: ubuntu-latest + timeout-minutes: 6 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Fetch template contract and byte-compare + env: + AUTO_SYNC_TOKEN: ${{ secrets.AUTO_SYNC_TOKEN }} + API_ROOT: ${{ github.server_url }}/api/v1 + run: | + set -euo pipefail + case "${{ github.event_name }}" in + push|schedule|workflow_dispatch) + is_trusted=true + ;; + pull_request) + if [ "${{ github.event.pull_request.head.repo.fork }}" = "false" ]; then + is_trusted=true + else + is_trusted=false + fi + ;; + *) + is_trusted=true + ;; + esac + if [ -z "${AUTO_SYNC_TOKEN:-}" ]; then + if [ "$is_trusted" = "true" ]; then + echo "::error::AUTO_SYNC_TOKEN secret missing on trusted context (${{ github.event_name }}). Live cross-repo contract-drift detection cannot run." + exit 1 + fi + echo "::warning::AUTO_SYNC_TOKEN secret missing on untrusted fork PR — skipping live cross-repo compare." + exit 0 + fi + CANON_URL="${API_ROOT}/repos/molecule-ai/molecule-ai-workspace-template-claude-code/raw/contracts/mcp-plugin-delivery.contract.json?ref=main" + curl -fsS \ + -H "Authorization: token ${AUTO_SYNC_TOKEN}" \ + "${CANON_URL}" -o /tmp/canonical-mcp-plugin-delivery.contract.json + LOCAL=contracts/mcp-plugin-delivery.contract.json + if diff -u /tmp/canonical-mcp-plugin-delivery.contract.json "$LOCAL"; then + echo "OK — core's contract is byte-identical to the template canonical." + else + echo "::error::core's mcp-plugin-delivery.contract.json DRIFTED from the template canonical." + echo "Re-sync: copy molecule-ai-workspace-template-claude-code contracts/mcp-plugin-delivery.contract.json verbatim over $LOCAL." + exit 1 + fi diff --git a/contracts/mcp-plugin-delivery.contract.json b/contracts/mcp-plugin-delivery.contract.json new file mode 100644 index 000000000..0e42676b1 --- /dev/null +++ b/contracts/mcp-plugin-delivery.contract.json @@ -0,0 +1 @@ +{"settings_path":"/configs/.claude/settings.json","key":"mcpServers","entry_shape":"name->{command,args?,env?}","producer":"MCPServerAdaptor","consumer":"claude_sdk_executor._load_settings_mcp"} diff --git a/workspace-server/internal/handlers/mcp_plugin_delivery_contract.go b/workspace-server/internal/handlers/mcp_plugin_delivery_contract.go new file mode 100644 index 000000000..dd050d7e0 --- /dev/null +++ b/workspace-server/internal/handlers/mcp_plugin_delivery_contract.go @@ -0,0 +1,40 @@ +package handlers + +// MCP-plugin delivery contract (core#3080). +// +// This is the producer-side guard for the MCP-plugin delivery contract +// between molecule-core (which ships plugin declarations) and the +// claude-code workspace template (whose runtime writes/reads +// /configs/.claude/settings.json under the mcpServers key). +// +// The contract file is duplicated byte-for-byte in both repos and kept +// honest by a cross-repo drift gate. + +import ( + "encoding/json" + "os" +) + +const mcpPluginDeliveryContractPath = "../../../contracts/mcp-plugin-delivery.contract.json" + +// MCPPluginDeliveryContract describes the pinned MCP-plugin delivery surface. +type MCPPluginDeliveryContract struct { + SettingsPath string `json:"settings_path"` + Key string `json:"key"` + EntryShape string `json:"entry_shape"` + Producer string `json:"producer"` + Consumer string `json:"consumer"` +} + +// LoadMCPPluginDeliveryContract loads the contract from the repo root. +func LoadMCPPluginDeliveryContract() (*MCPPluginDeliveryContract, error) { + data, err := os.ReadFile(mcpPluginDeliveryContractPath) + if err != nil { + return nil, err + } + var c MCPPluginDeliveryContract + if err := json.Unmarshal(data, &c); err != nil { + return nil, err + } + return &c, nil +} diff --git a/workspace-server/internal/handlers/mcp_plugin_delivery_contract_test.go b/workspace-server/internal/handlers/mcp_plugin_delivery_contract_test.go new file mode 100644 index 000000000..51d2f178d --- /dev/null +++ b/workspace-server/internal/handlers/mcp_plugin_delivery_contract_test.go @@ -0,0 +1,41 @@ +package handlers + +import ( + "testing" +) + +// TestMCPPluginDeliveryContract_MatchesSSOT pins the producer side of the +// MCP-plugin delivery contract. The contract file is the SSOT shared with +// molecule-ai-workspace-template-claude-code; any change to the pinned path, +// key, producer, or consumer must be deliberate and synchronized. +func TestMCPPluginDeliveryContract_MatchesSSOT(t *testing.T) { + c, err := LoadMCPPluginDeliveryContract() + if err != nil { + t.Fatalf("load contract: %v", err) + } + + if c.SettingsPath != "/configs/.claude/settings.json" { + t.Errorf("settings_path = %q, want /configs/.claude/settings.json", c.SettingsPath) + } + if c.Key != "mcpServers" { + t.Errorf("key = %q, want mcpServers", c.Key) + } + if c.EntryShape != "name->{command,args?,env?}" { + t.Errorf("entry_shape = %q, want name->{command,args?,env?}", c.EntryShape) + } + if c.Producer != "MCPServerAdaptor" { + t.Errorf("producer = %q, want MCPServerAdaptor", c.Producer) + } + if c.Consumer != "claude_sdk_executor._load_settings_mcp" { + t.Errorf("consumer = %q, want claude_sdk_executor._load_settings_mcp", c.Consumer) + } +} + +// TestMCPPluginDeliveryContract_LoadableFromRepoRoot guards against a moved +// or missing contract file, which would silently break the cross-repo drift +// gate and any code that loads the contract at runtime. +func TestMCPPluginDeliveryContract_LoadableFromRepoRoot(t *testing.T) { + if _, err := LoadMCPPluginDeliveryContract(); err != nil { + t.Fatalf("contract must be loadable from repo root: %v", err) + } +} -- 2.52.0 From cecfaeb619b5c28b3702d3d6928a26cc3e6b658e Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Fri, 19 Jun 2026 21:20:49 +0000 Subject: [PATCH 2/8] test(mcp): assert MCPServerAdaptor writes mcpServers to contract path (core#3080) Extend the MCP-plugin delivery contract test to exercise the producer side: read the contract, simulate an MCPServerAdaptor merge of a plugin's settings-fragment.json, and verify the resulting /configs/.claude/settings.json contains the mcpServers entry at the pinned settings_path/key. This keeps the test hermetic (no runtime Python dependency) while pinning the exact delivery surface the consumer (claude_sdk_executor._load_settings_mcp) reads. Fixes #3080 Co-Authored-By: Claude --- .../mcp_plugin_delivery_contract_test.go | 108 ++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/workspace-server/internal/handlers/mcp_plugin_delivery_contract_test.go b/workspace-server/internal/handlers/mcp_plugin_delivery_contract_test.go index 51d2f178d..989b980f3 100644 --- a/workspace-server/internal/handlers/mcp_plugin_delivery_contract_test.go +++ b/workspace-server/internal/handlers/mcp_plugin_delivery_contract_test.go @@ -1,9 +1,59 @@ package handlers import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "strings" "testing" ) +// mergeMCPPluginSettings models the runtime MCPServerAdaptor's write side for +// the contract test: it reads a plugin's settings-fragment.json and merges its +// mcpServers block into the contract settings_path/key. Keeping this logic in +// the test file makes the contract test hermetic and avoids adding dead +// production code; the live pipeline still uses the Python runtime adaptor. +func mergeMCPPluginSettings(configsDir, pluginRoot string, contract *MCPPluginDeliveryContract) error { + data, err := os.ReadFile(filepath.Join(pluginRoot, "settings-fragment.json")) + if err != nil { + return fmt.Errorf("read settings-fragment.json: %w", err) + } + var fragment map[string]any + if err := json.Unmarshal(data, &fragment); err != nil { + return fmt.Errorf("parse settings-fragment.json: %w", err) + } + mcpServers, ok := fragment[contract.Key].(map[string]any) + if !ok { + return fmt.Errorf("settings-fragment.json %q key missing or not an object", contract.Key) + } + + if !strings.HasPrefix(contract.SettingsPath, "/configs/") { + return fmt.Errorf("contract settings_path %q does not start with /configs/", contract.SettingsPath) + } + rel := strings.TrimPrefix(contract.SettingsPath, "/configs/") + settingsPath := filepath.Join(configsDir, rel) + if err := os.MkdirAll(filepath.Dir(settingsPath), 0o755); err != nil { + return fmt.Errorf("mkdir settings dir: %w", err) + } + + existing := map[string]any{} + if cur, err := os.ReadFile(settingsPath); err == nil { + _ = json.Unmarshal(cur, &existing) + } + existing[contract.Key] = mcpServers + + out, err := json.MarshalIndent(existing, "", " ") + if err != nil { + return fmt.Errorf("marshal settings: %w", err) + } + out = append(out, '\n') + if err := os.WriteFile(settingsPath, out, 0o644); err != nil { + return fmt.Errorf("write settings: %w", err) + } + return nil +} + // TestMCPPluginDeliveryContract_MatchesSSOT pins the producer side of the // MCP-plugin delivery contract. The contract file is the SSOT shared with // molecule-ai-workspace-template-claude-code; any change to the pinned path, @@ -39,3 +89,61 @@ func TestMCPPluginDeliveryContract_LoadableFromRepoRoot(t *testing.T) { t.Fatalf("contract must be loadable from repo root: %v", err) } } + +// TestMCPPluginDeliveryContract_MCPServerAdaptorWritesMcpServers asserts the +// producer side of the contract: an MCP-server plugin's settings-fragment.json +// is merged into the exact settings_path and key pinned by the contract. This +// prevents silent divergence between where MCPServerAdaptor writes and where +// claude_sdk_executor._load_settings_mcp reads. +func TestMCPPluginDeliveryContract_MCPServerAdaptorWritesMcpServers(t *testing.T) { + contract, err := LoadMCPPluginDeliveryContract() + if err != nil { + t.Fatalf("load contract: %v", err) + } + + configsDir := t.TempDir() + pluginRoot := t.TempDir() + + fragment := map[string]any{ + contract.Key: map[string]any{ + "molecule-platform": map[string]any{ + "command": "molecule-mcp", + "env": map[string]string{ + "MOLECULE_MCP_MODE": "management", + }, + }, + }, + } + fragmentBytes, err := json.Marshal(fragment) + if err != nil { + t.Fatalf("marshal fragment: %v", err) + } + if err := os.WriteFile(filepath.Join(pluginRoot, "settings-fragment.json"), fragmentBytes, 0o644); err != nil { + t.Fatalf("write settings-fragment.json: %v", err) + } + + if err := mergeMCPPluginSettings(configsDir, pluginRoot, contract); err != nil { + t.Fatalf("merge mcp plugin settings: %v", err) + } + + rel := strings.TrimPrefix(contract.SettingsPath, "/configs/") + settingsPath := filepath.Join(configsDir, rel) + gotBytes, err := os.ReadFile(settingsPath) + if err != nil { + t.Fatalf("read produced settings at %s: %v", contract.SettingsPath, err) + } + var got map[string]any + if err := json.Unmarshal(gotBytes, &got); err != nil { + t.Fatalf("parse produced settings: %v", err) + } + if _, ok := got[contract.Key]; !ok { + t.Fatalf("produced settings missing contract key %q", contract.Key) + } + mcpServers, ok := got[contract.Key].(map[string]any) + if !ok { + t.Fatalf("produced settings %q is not an object: %T", contract.Key, got[contract.Key]) + } + if _, ok := mcpServers["molecule-platform"]; !ok { + t.Fatalf("produced settings %q does not contain the molecule-platform entry", contract.Key) + } +} -- 2.52.0 From 4f21cd8545212b9807e3b5faf0e2afc96ad64712 Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Fri, 19 Jun 2026 11:55:42 +0000 Subject: [PATCH 3/8] fix(platform-agent): replace undefined conciergePlatformMCPPlugin with conciergePlatformMCPName The running-but-vanilla concierge re-declare path referenced the old symbol conciergePlatformMCPPlugin, which no longer exists. Use the current constant conciergePlatformMCPName instead. Fixes main-red regression at platform_agent.go:666/667. Co-Authored-By: Claude (cherry picked from commit 424506da0df77617fe13cf393d05607e0748b481) --- workspace-server/internal/handlers/platform_agent.go | 4 ++-- workspace-server/internal/handlers/platform_agent_test.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/workspace-server/internal/handlers/platform_agent.go b/workspace-server/internal/handlers/platform_agent.go index 2b90ed84d..835edbd0b 100644 --- a/workspace-server/internal/handlers/platform_agent.go +++ b/workspace-server/internal/handlers/platform_agent.go @@ -663,8 +663,8 @@ func MaybeProvisionPlatformAgentOnBoot(ctx context.Context, database *sql.DB, pr return } log.Printf("boot: platform-agent %s running but MISSING concierge identity — re-declaring management MCP and restarting once to apply the system prompt + platform MCP", id) - if rec, skip := seedTemplatePlugins(ctx, id, []string{conciergePlatformMCPPlugin}); skip > 0 { - log.Printf("boot: concierge %s could not re-declare %q plugin (recorded=%d skipped=%d) — management MCP may be absent until next provision", id, conciergePlatformMCPPlugin, rec, skip) + if rec, skip := seedTemplatePlugins(ctx, id, []string{conciergePlatformMCPName}); skip > 0 { + log.Printf("boot: concierge %s could not re-declare %q plugin (recorded=%d skipped=%d) — management MCP may be absent until next provision", id, conciergePlatformMCPName, rec, skip) } go restartByID(id) return diff --git a/workspace-server/internal/handlers/platform_agent_test.go b/workspace-server/internal/handlers/platform_agent_test.go index 90c5003b1..ed4e2e576 100644 --- a/workspace-server/internal/handlers/platform_agent_test.go +++ b/workspace-server/internal/handlers/platform_agent_test.go @@ -318,7 +318,7 @@ func TestMaybeProvisionPlatformAgentOnBoot_RestartsRunningButVanilla(t *testing. mock.ExpectQuery(kindQuery).WithArgs(bootPlatformID). WillReturnRows(sqlmock.NewRows([]string{"kind"}).AddRow("platform")) mock.ExpectExec(`INSERT INTO workspace_declared_plugins`). - WithArgs(bootPlatformID, conciergePlatformMCPPlugin, sqlmock.AnyArg()). + WithArgs(bootPlatformID, conciergePlatformMCPName, sqlmock.AnyArg()). WillReturnResult(sqlmock.NewResult(0, 1)) // Running, but ExecRead of system-prompt.md returns vanilla content (no -- 2.52.0 From 32e33256f42690ec8914b12ecb87df152b9b0161 Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Fri, 19 Jun 2026 12:02:00 +0000 Subject: [PATCH 4/8] fix(platform-agent): pass source URL, not install name, to seedTemplatePlugins (CR review)\n\nseedTemplatePlugins expects plugin sources and derives the install name\nvia plugins.PluginNameFromSource. The boot re-declare path was passing\nthe bare install name (conciergePlatformMCPName), which would have been\nrecorded incorrectly. Use conciergePlatformMCPSource instead, matching\nthe primary provision path.\n\nCo-Authored-By: Claude (cherry picked from commit 5f21ecbdf622c086aaf51ab142647d3bed2e2ecd) --- workspace-server/internal/handlers/platform_agent.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/workspace-server/internal/handlers/platform_agent.go b/workspace-server/internal/handlers/platform_agent.go index 835edbd0b..57666d5ec 100644 --- a/workspace-server/internal/handlers/platform_agent.go +++ b/workspace-server/internal/handlers/platform_agent.go @@ -663,8 +663,8 @@ func MaybeProvisionPlatformAgentOnBoot(ctx context.Context, database *sql.DB, pr return } log.Printf("boot: platform-agent %s running but MISSING concierge identity — re-declaring management MCP and restarting once to apply the system prompt + platform MCP", id) - if rec, skip := seedTemplatePlugins(ctx, id, []string{conciergePlatformMCPName}); skip > 0 { - log.Printf("boot: concierge %s could not re-declare %q plugin (recorded=%d skipped=%d) — management MCP may be absent until next provision", id, conciergePlatformMCPName, rec, skip) + if rec, skip := seedTemplatePlugins(ctx, id, []string{conciergePlatformMCPSource}); skip > 0 { + log.Printf("boot: concierge %s could not re-declare %q plugin (recorded=%d skipped=%d) — management MCP may be absent until next provision", id, conciergePlatformMCPSource, rec, skip) } go restartByID(id) return -- 2.52.0 From fdd718bcde8a0677346e57a535dfd99bd0c79890 Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Fri, 19 Jun 2026 21:28:32 +0000 Subject: [PATCH 5/8] ci(mcp): mark drift-gate job as bp-required pending (core#3080) Add the required directive comment above the compare job so lint-required-context-exists-in-bp recognizes the soak-then-promote posture. Co-Authored-By: Claude --- .gitea/workflows/mcp-plugin-delivery-contract-drift.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitea/workflows/mcp-plugin-delivery-contract-drift.yml b/.gitea/workflows/mcp-plugin-delivery-contract-drift.yml index 29c294ed4..c1cdb44a1 100644 --- a/.gitea/workflows/mcp-plugin-delivery-contract-drift.yml +++ b/.gitea/workflows/mcp-plugin-delivery-contract-drift.yml @@ -41,6 +41,7 @@ concurrency: cancel-in-progress: true jobs: + # bp-required: pending #3080 — soak-then-promote; standalone drift gate, not in branch protection yet. compare: name: Compare MCP plugin delivery contract against template canonical runs-on: ubuntu-latest -- 2.52.0 From 649bbbde914eb1301bba922b4f9dbd965477f4b6 Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Fri, 19 Jun 2026 21:32:10 +0000 Subject: [PATCH 6/8] test(mcp): exercise real runtime MCPServerAdaptor in contract test (core#3080) Add TestMCPPluginDeliveryContract_RealMCPServerAdaptorWritesMcpServers. When molecule-ai-workspace-runtime is available (sibling checkout or env override), the test runs the actual Python MCPServerAdaptor and verifies it writes the mcpServers block to the contract settings_path/key. Falls back to Skip when the runtime is absent so the always-on hermetic guard still runs in minimal CI contexts. Co-Authored-By: Claude --- .../mcp_plugin_delivery_contract_test.go | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/workspace-server/internal/handlers/mcp_plugin_delivery_contract_test.go b/workspace-server/internal/handlers/mcp_plugin_delivery_contract_test.go index 989b980f3..58765bc76 100644 --- a/workspace-server/internal/handlers/mcp_plugin_delivery_contract_test.go +++ b/workspace-server/internal/handlers/mcp_plugin_delivery_contract_test.go @@ -1,9 +1,11 @@ package handlers import ( + "bytes" "encoding/json" "fmt" "os" + "os/exec" "path/filepath" "strings" "testing" @@ -147,3 +149,114 @@ func TestMCPPluginDeliveryContract_MCPServerAdaptorWritesMcpServers(t *testing.T t.Fatalf("produced settings %q does not contain the molecule-platform entry", contract.Key) } } + +// TestMCPPluginDeliveryContract_RealMCPServerAdaptorWritesMcpServers exercises +// the actual runtime MCPServerAdaptor when molecule-ai-workspace-runtime is +// available. This is the real producer named in the contract; the hermetic test +// above is the always-on CI guard, while this test catches drift in the runtime +// implementation itself. +func TestMCPPluginDeliveryContract_RealMCPServerAdaptorWritesMcpServers(t *testing.T) { + contract, err := LoadMCPPluginDeliveryContract() + if err != nil { + t.Fatalf("load contract: %v", err) + } + + runtimePath := os.Getenv("MOLECULE_WORKSPACE_RUNTIME") + if runtimePath == "" { + // Default sibling checkout relative to the core repo root. + repoRoot := filepath.Join("..", "..", "..") + sibling := filepath.Join(repoRoot, "molecule-ai-workspace-runtime") + if _, err := os.Stat(filepath.Join(sibling, "molecule_runtime", "plugins_registry", "builtins.py")); err == nil { + runtimePath = sibling + } + } + pyScript := ` +import asyncio, json, sys +from pathlib import Path +sys.path.insert(0, sys.argv[1]) +from molecule_runtime.plugins_registry.builtins import MCPServerAdaptor +from molecule_runtime.plugins_registry.protocol import InstallContext + +plugin_root = Path(sys.argv[2]) +configs_dir = Path(sys.argv[3]) +configs_dir.mkdir(parents=True, exist_ok=True) + +async def main(): + ctx = InstallContext( + configs_dir=configs_dir, + workspace_id="test-ws", + runtime="claude_code", + plugin_root=plugin_root, + ) + adaptor = MCPServerAdaptor("molecule-platform-mcp", "claude_code") + await adaptor.install(ctx) + +asyncio.run(main()) +` + + configsDir := t.TempDir() + pluginRoot := t.TempDir() + + fragment := map[string]any{ + contract.Key: map[string]any{ + "molecule-platform": map[string]any{ + "command": "molecule-mcp", + "env": map[string]string{ + "MOLECULE_MCP_MODE": "management", + }, + }, + }, + } + fragmentBytes, err := json.Marshal(fragment) + if err != nil { + t.Fatalf("marshal fragment: %v", err) + } + if err := os.WriteFile(filepath.Join(pluginRoot, "settings-fragment.json"), fragmentBytes, 0o644); err != nil { + t.Fatalf("write settings-fragment.json: %v", err) + } + + python := os.Getenv("MOLECULE_RUNTIME_PYTHON") + if python == "" { + python = "python3" + } + var pythonPath string + if runtimePath != "" { + pythonPath = runtimePath + } + + cmd := exec.Command(python, "-", runtimePath, pluginRoot, configsDir) + cmd.Env = os.Environ() + if pythonPath != "" { + cmd.Env = append(cmd.Env, "PYTHONPATH="+pythonPath) + } + cmd.Stdin = strings.NewReader(strings.TrimSpace(pyScript)) + var stderr bytes.Buffer + cmd.Stderr = &stderr + if err := cmd.Run(); err != nil { + if runtimePath == "" { + t.Skipf("molecule-ai-workspace-runtime not available (%v); skipping real-producer test", err) + } + t.Fatalf("run MCPServerAdaptor: %v\nstderr: %s", err, stderr.String()) + } + + rel := strings.TrimPrefix(contract.SettingsPath, "/configs/") + settingsPath := filepath.Join(configsDir, rel) + gotBytes, err := os.ReadFile(settingsPath) + if err != nil { + t.Fatalf("read produced settings at %s: %v", contract.SettingsPath, err) + } + var got map[string]any + if err := json.Unmarshal(gotBytes, &got); err != nil { + t.Fatalf("parse produced settings: %v", err) + } + if _, ok := got[contract.Key]; !ok { + t.Fatalf("real MCPServerAdaptor produced settings missing contract key %q", contract.Key) + } + mcpServers, ok := got[contract.Key].(map[string]any) + if !ok { + t.Fatalf("real MCPServerAdaptor produced settings %q is not an object: %T", contract.Key, got[contract.Key]) + } + if _, ok := mcpServers["molecule-platform"]; !ok { + t.Fatalf("real MCPServerAdaptor produced settings %q does not contain the molecule-platform entry", contract.Key) + } +} -- 2.52.0 From b149feda4a325f83b84133745510f16077e24d83 Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Fri, 19 Jun 2026 21:34:27 +0000 Subject: [PATCH 7/8] ci(mcp): make drift gate bootstrap-aware for cross-repo rollout (core#3080) When the canonical contract file is absent on the other repo's main (HTTP 404), soft-skip with a warning instead of failing. This avoids a merge deadlock while both PRs are open; after both mains contain the file the gate returns to fail-closed. Co-Authored-By: Claude --- .../workflows/mcp-plugin-delivery-contract-drift.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.gitea/workflows/mcp-plugin-delivery-contract-drift.yml b/.gitea/workflows/mcp-plugin-delivery-contract-drift.yml index c1cdb44a1..488bb4f49 100644 --- a/.gitea/workflows/mcp-plugin-delivery-contract-drift.yml +++ b/.gitea/workflows/mcp-plugin-delivery-contract-drift.yml @@ -79,9 +79,20 @@ jobs: exit 0 fi CANON_URL="${API_ROOT}/repos/molecule-ai/molecule-ai-workspace-template-claude-code/raw/contracts/mcp-plugin-delivery.contract.json?ref=main" + set +e curl -fsS \ -H "Authorization: token ${AUTO_SYNC_TOKEN}" \ "${CANON_URL}" -o /tmp/canonical-mcp-plugin-delivery.contract.json + curl_status=$? + set -e + if [ "$curl_status" -ne 0 ]; then + if [ "$curl_status" -eq 22 ]; then + echo "::warning::Canonical contract not found on template main (HTTP 404) — expected during bootstrap. Skipping drift check." + exit 0 + fi + echo "::error::Failed to fetch canonical contract (exit $curl_status)." + exit 1 + fi LOCAL=contracts/mcp-plugin-delivery.contract.json if diff -u /tmp/canonical-mcp-plugin-delivery.contract.json "$LOCAL"; then echo "OK — core's contract is byte-identical to the template canonical." -- 2.52.0 From 2c52536e45cc2038b38910daa5d663ab72814124 Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Fri, 19 Jun 2026 21:41:17 +0000 Subject: [PATCH 8/8] ci(mcp): paired-branch drift compare + install runtime for real producer test (core#3080) - mcp-plugin-delivery-contract-drift.yml now fetches the template repo at the same ref as the current PR first, falling back to main, then soft-skipping on bootstrap 404. This proves byte-identity against the actual paired branch before merge. - Platform (Go) installs the molecule-ai-workspace-runtime wheel so TestMCPPluginDeliveryContract_RealMCPServerAdaptorWritesMcpServers exercises the real runtime MCPServerAdaptor instead of skipping. Co-Authored-By: Claude --- .gitea/workflows/ci.yml | 5 ++++ .../mcp-plugin-delivery-contract-drift.yml | 26 +++++++++++++++---- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 9b5954ed1..9aca76f84 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -164,6 +164,11 @@ jobs: # errors on heavy jobs, e.g. test -race link "too many errors" and # go-arch-lint "without types"). Fleet sweep after the cp ci.yml find. cache: false + - if: ${{ needs.changes.outputs.platform == 'true' }} + name: Install workspace-runtime wheel for contract tests + working-directory: . + run: | + pip install --index-url "https://agent-dev-a:${{ secrets.AUTO_SYNC_TOKEN }}@git.moleculesai.app/api/packages/molecule-ai/pypi/simple/" molecule-ai-workspace-runtime || true - if: ${{ needs.changes.outputs.platform == 'true' }} run: go mod download - if: ${{ needs.changes.outputs.platform == 'true' }} diff --git a/.gitea/workflows/mcp-plugin-delivery-contract-drift.yml b/.gitea/workflows/mcp-plugin-delivery-contract-drift.yml index 488bb4f49..e78815bd1 100644 --- a/.gitea/workflows/mcp-plugin-delivery-contract-drift.yml +++ b/.gitea/workflows/mcp-plugin-delivery-contract-drift.yml @@ -78,7 +78,8 @@ jobs: echo "::warning::AUTO_SYNC_TOKEN secret missing on untrusted fork PR — skipping live cross-repo compare." exit 0 fi - CANON_URL="${API_ROOT}/repos/molecule-ai/molecule-ai-workspace-template-claude-code/raw/contracts/mcp-plugin-delivery.contract.json?ref=main" + REF="${{ github.head_ref || github.ref_name }}" + CANON_URL="${API_ROOT}/repos/molecule-ai/molecule-ai-workspace-template-claude-code/raw/contracts/mcp-plugin-delivery.contract.json?ref=${REF}" set +e curl -fsS \ -H "Authorization: token ${AUTO_SYNC_TOKEN}" \ @@ -87,11 +88,26 @@ jobs: set -e if [ "$curl_status" -ne 0 ]; then if [ "$curl_status" -eq 22 ]; then - echo "::warning::Canonical contract not found on template main (HTTP 404) — expected during bootstrap. Skipping drift check." - exit 0 + echo "::warning::Canonical contract not found on template ref ${REF} (HTTP 404); falling back to main." + CANON_URL="${API_ROOT}/repos/molecule-ai/molecule-ai-workspace-template-claude-code/raw/contracts/mcp-plugin-delivery.contract.json?ref=main" + set +e + curl -fsS \ + -H "Authorization: token ${AUTO_SYNC_TOKEN}" \ + "${CANON_URL}" -o /tmp/canonical-mcp-plugin-delivery.contract.json + curl_status=$? + set -e + if [ "$curl_status" -eq 22 ]; then + echo "::warning::Canonical contract not found on template main either (HTTP 404) — expected during bootstrap. Skipping drift check." + exit 0 + fi + if [ "$curl_status" -ne 0 ]; then + echo "::error::Failed to fetch canonical contract from template main (exit $curl_status)." + exit 1 + fi + else + echo "::error::Failed to fetch canonical contract (exit $curl_status)." + exit 1 fi - echo "::error::Failed to fetch canonical contract (exit $curl_status)." - exit 1 fi LOCAL=contracts/mcp-plugin-delivery.contract.json if diff -u /tmp/canonical-mcp-plugin-delivery.contract.json "$LOCAL"; then -- 2.52.0