From fcabdcae3ce313e7c8645d56684bb7c4217b199c Mon Sep 17 00:00:00 2001 From: Declan Carroll Date: Fri, 4 Sep 2026 07:58:32 +0000 Subject: [PATCH] feat: Add Flaky MCP to headless OpenCode sessions (no-changelog) (#37809) --- .devcontainer/codespaces/README.md | 12 +++--- .devcontainer/codespaces/agent-worker.mjs | 23 ++++++++++-- .../codespaces/agent-worker.test.mjs | 37 ++++++++++++++++++- 3 files changed, 62 insertions(+), 10 deletions(-) diff --git a/.devcontainer/codespaces/README.md b/.devcontainer/codespaces/README.md index 41f595b8a25..2765d056e75 100644 --- a/.devcontainer/codespaces/README.md +++ b/.devcontainer/codespaces/README.md @@ -65,7 +65,7 @@ It runs the turn. It sends the result to the turn's resume URL. It uses no tunnel, no open port, and no domain. The worker starts on each container start (`postStartCommand`). It needs three -secrets and uses one optional secret. Add them at +secrets and uses three optional secrets. Add them at [github.com/settings/codespaces](https://github.com/settings/codespaces), the same way as `ANTHROPIC_API_KEY`: @@ -73,6 +73,7 @@ same way as `ANTHROPIC_API_KEY`: - `N8N_DEQUEUE_URL` — the n8n webhook that returns a pending turn. - `OPENROUTER_API_KEY` — the model provider key for Sol. - `SLACK_BOT_TOKEN` — optional bot token for progress messages. It needs `chat:write` only. +- `FLAKY_MCP_URL` and `FLAKY_MCP_TOKEN` — optional Flaky MCP connection for OpenCode. The worker explicitly reloads `/usr/local/lib/codespaces-env.sh` when its tmux session starts. An existing tmux server can otherwise retain an environment @@ -147,11 +148,12 @@ session rarely needs a cold `pnpm install` or a full `pnpm build`. Both are slow ## Flaky tools (MCP) -Claude sessions get the `flaky` MCP server automatically: Currents +Claude sessions and the headless OpenCode worker get the `flaky` MCP server automatically: Currents flaky/quarantine data, the `qa_*` BigQuery dataset, Sentry RCA, live Linear, -and repo investigation. Login registers it from the repo-level -`FLAKY_MCP_TOKEN` / `FLAKY_MCP_URL` secrets. Forks have no secrets and skip -it. Tell the agent to call `get_flaky_context` first — it returns the rules +and repo investigation. The worker keeps the token in its environment and puts +only an environment reference in the OpenCode config. Claude login registers +the same server without writing the token to disk. Forks have no secrets and +skip it. Tell the agent to call `get_flaky_context` first — it returns the rules the tools assume. ## Quality and security skills (Claude plugins) diff --git a/.devcontainer/codespaces/agent-worker.mjs b/.devcontainer/codespaces/agent-worker.mjs index adb4e652359..2055434f9f7 100644 --- a/.devcontainer/codespaces/agent-worker.mjs +++ b/.devcontainer/codespaces/agent-worker.mjs @@ -19,9 +19,24 @@ const INITIAL_POLL_INTERVAL_MS = 3000; const MAX_POLL_INTERVAL_MS = 30_000; const SLACK_UPDATE_INTERVAL_MS = 1500; const SLACK_TEXT_LIMIT = 3900; -const OPENCODE_CONFIG_CONTENT = JSON.stringify({ - provider: { openrouter: { options: { apiKey: '{env:OPENROUTER_API_KEY}' } } }, -}); + +export function openCodeConfig(environment) { + const config = { + provider: { openrouter: { options: { apiKey: '{env:OPENROUTER_API_KEY}' } } }, + }; + if (environment.FLAKY_MCP_URL && environment.FLAKY_MCP_TOKEN) { + config.mcp = { + flaky: { + type: 'remote', + url: environment.FLAKY_MCP_URL, + enabled: true, + oauth: false, + headers: { Authorization: 'Bearer {env:FLAKY_MCP_TOKEN}' }, + }, + }; + } + return config; +} function posNum(name, fallback) { const raw = process.env[name]; @@ -74,7 +89,7 @@ export function openCodeEnvironment(environment) { delete childEnvironment.AGENT_WORKER_TOKEN; delete childEnvironment.N8N_DEQUEUE_URL; delete childEnvironment.SLACK_BOT_TOKEN; - childEnvironment.OPENCODE_CONFIG_CONTENT = OPENCODE_CONFIG_CONTENT; + childEnvironment.OPENCODE_CONFIG_CONTENT = JSON.stringify(openCodeConfig(childEnvironment)); return childEnvironment; } diff --git a/.devcontainer/codespaces/agent-worker.test.mjs b/.devcontainer/codespaces/agent-worker.test.mjs index c7a7e6a1f73..5b1d3937cbb 100644 --- a/.devcontainer/codespaces/agent-worker.test.mjs +++ b/.devcontainer/codespaces/agent-worker.test.mjs @@ -5,7 +5,13 @@ import { PassThrough } from 'node:stream'; import { test } from 'node:test'; import { setTimeout as sleep } from 'node:timers/promises'; -import { openCodeEnvironment, pollOnce, runOpenCode, startSlackProgress } from './agent-worker.mjs'; +import { + openCodeConfig, + openCodeEnvironment, + pollOnce, + runOpenCode, + startSlackProgress, +} from './agent-worker.mjs'; const turn = { turnId: 'turn-1', @@ -341,3 +347,32 @@ test('keeps broker credentials out of the OpenCode process', () => { GITHUB_TOKEN: 'github', }); }); + +test('configures Flaky MCP without serializing its token', () => { + const token = 'flaky-secret'; + const environment = openCodeEnvironment({ + FLAKY_MCP_URL: 'https://example.com/mcp', + FLAKY_MCP_TOKEN: token, + OPENROUTER_API_KEY: 'openrouter', + }); + + assert.deepEqual(JSON.parse(environment.OPENCODE_CONFIG_CONTENT), { + provider: { openrouter: { options: { apiKey: '{env:OPENROUTER_API_KEY}' } } }, + mcp: { + flaky: { + type: 'remote', + url: 'https://example.com/mcp', + enabled: true, + oauth: false, + headers: { Authorization: 'Bearer {env:FLAKY_MCP_TOKEN}' }, + }, + }, + }); + assert.equal(environment.FLAKY_MCP_TOKEN, token); + assert.equal(environment.OPENCODE_CONFIG_CONTENT.includes(token), false); +}); + +test('omits Flaky MCP when either setting is missing', () => { + assert.equal(openCodeConfig({ FLAKY_MCP_URL: 'https://example.com/mcp' }).mcp, undefined); + assert.equal(openCodeConfig({ FLAKY_MCP_TOKEN: 'flaky-secret' }).mcp, undefined); +});