diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 6ac5d27..e555522 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -441,3 +441,11 @@ export ZCODE_TUI_NOTIFICATION_METHOD=auto # auto|osc9|bel|native|off export ZCODE_TUI_NOTIFICATION_CONDITION=always # unfocused|always zcode ``` + +## Official MCP Availability + +When the bundled runtime has no official MCP trusted-origin registry, official +HTTP MCP services are reported as disabled with an `official_auth_unavailable` +diagnostic. Other plugin components remain available. This does not disable +certificate, origin, or permission checks, and does not suppress services when +the runtime provides the required registry. No user configuration is rewritten. diff --git a/scripts/check-runtime.ts b/scripts/check-runtime.ts index 42fabd4..2f45610 100755 --- a/scripts/check-runtime.ts +++ b/scripts/check-runtime.ts @@ -18,6 +18,7 @@ import { patchRuntimeHttpNoContent, patchRuntimeLoginModelDefaults, patchRuntimeNetworkRetryClassification, + patchRuntimeOfficialMcpAvailability, patchRuntimeStreamEofFinishGuard, parseRuntimePatchReports, runtimePatchPlan, @@ -70,6 +71,7 @@ if (!metadataCapabilities throw new Error("The extracted runtime capability manifest is missing or stale; run `bun run sync` again."); } if (patchRuntimeLoginModelDefaults(runtimeSource) !== runtimeSource + || (patchEnabled("official-mcp-availability") && patchRuntimeOfficialMcpAvailability(runtimeSource) !== runtimeSource) || (patchEnabled("goal-failure-pause") && patchRuntimeGoalFailurePause(runtimeSource) !== runtimeSource) || patchRuntimeHttpNoContent(runtimeSource) !== runtimeSource || !hasRuntimeHttpNoContentGuard(runtimeSource) diff --git a/scripts/sync-runtime.ts b/scripts/sync-runtime.ts index f808870..5cb288b 100755 --- a/scripts/sync-runtime.ts +++ b/scripts/sync-runtime.ts @@ -410,6 +410,26 @@ export function patchRuntimeCliHelpContract(runtime: string): string { return patched; } +/** Report missing official authentication support without weakening origin checks. */ +export function patchRuntimeOfficialMcpAvailability(runtime: string): string { + const marker = "zcode_cli_official_mcp_unavailable"; + if (runtime.includes(marker)) return runtime; + const anchor = /await this\.closeRecord\(([A-Za-z_$][\w$]*)\),([A-Za-z_$][\w$]*)\.enabled===!1\)\{let ([A-Za-z_$][\w$]*)=this\.createStatus\(\2,"disabled"\);/gu; + let matches = 0; + const patched = runtime.replace(anchor, (_, name: string, config: string, status: string) => { + matches += 1; + const unavailable = `${config}.type==="http"&&${config}.auth?.type==="zcode_official"` + + `&&${config}.auth.provider==="jwt_token"&&${config}.official!==void 0` + + "&&!this.officialMcpAuth?.trustedOrigins"; + return `await this.closeRecord(${name}),${config}.enabled===!1||(${unavailable})){` + + `let ${status}=this.createStatus(${config},"disabled",${config}.enabled===!1?void 0:` + + `{error:"Official MCP authentication is unavailable in this runtime (${marker}).",` + + 'failureKind:"official_auth_unavailable"});'; + }); + if (matches !== 1) throw new Error("ZCode runtime is incompatible with the official MCP availability patch."); + return patched; +} + /** Keep short Agent calls inline, but detach long-running agents from the foreground turn. */ export function patchRuntimeAgentAutoBackground(runtime: string): string { const marker = "autoBackgroundMs:this.config.subagents?.autoBackgroundMs??1e3,outputRootDir:"; @@ -1303,6 +1323,11 @@ const terminalProjectionMarkers = [ ] as const; export const runtimePatchPlan: readonly RuntimePatchDefinition[] = [ + { + id: "official-mcp-availability", + requirement: "optional", + apply: patchRuntimeOfficialMcpAvailability + }, { id: "tui-bridge", requirement: "required", diff --git a/test/runtime-mcp-availability.test.ts b/test/runtime-mcp-availability.test.ts new file mode 100644 index 0000000..f006259 --- /dev/null +++ b/test/runtime-mcp-availability.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, test } from "bun:test"; +import { patchRuntimeOfficialMcpAvailability } from "../scripts/sync-runtime.ts"; + +// This fixture preserves the minified upstream connection branch, with no network transport. +const source = `class Adapter { + records = new Map(); + closed = []; + connected = []; + officialMcpAuth; + createStatus(config,status,extra){return {status,...extra}} + async closeRecord(name){this.closed.push(name)} + async connectServer(t,r,n={}){if(await this.closeRecord(t),r.enabled===!1){let m=this.createStatus(r,"disabled");return this.records.set(t,{config:r,status:m,tools:[]}),m}this.connected.push(t);return {status:"connected"}} +}`; + +function adapter() { + return new (new Function(`${patchRuntimeOfficialMcpAvailability(source)};return Adapter;`)())(); +} + +const official = { + type: "http", auth: { type: "zcode_official", provider: "jwt_token" }, + official: { provider: "fixture" }, url: "https://example.invalid/mcp" +}; + +describe("official MCP runtime availability (offline)", () => { + test("skips unavailable official HTTP services with an actionable status", async () => { + const runtime = adapter(); + const result = await runtime.connectServer("image_search", official); + expect(result).toMatchObject({ status: "disabled", failureKind: "official_auth_unavailable" }); + expect(result.error).toContain("authentication is unavailable"); + expect(runtime.connected).toEqual([]); + expect(runtime.closed).toEqual(["image_search"]); + expect(official).not.toHaveProperty("enabled"); + }); + + test("allows revalidation after the runtime gains trusted-origin support", async () => { + const runtime = adapter(); + await runtime.connectServer("image_search", official); + runtime.officialMcpAuth = { trustedOrigins: {} }; + expect(await runtime.connectServer("image_search", official)).toEqual({ status: "connected" }); + expect(runtime.connected).toEqual(["image_search"]); + }); + + test("preserves explicit disablement and other transports/auth paths", async () => { + const runtime = adapter(); + expect(await runtime.connectServer("disabled", { ...official, enabled: false })).toEqual({ status: "disabled" }); + for (const config of [{ type: "http" }, { ...official, type: "stdio" }, { ...official, official: undefined }]) { + expect(await runtime.connectServer("other", config)).toEqual({ status: "connected" }); + } + }); + + test("is idempotent and rejects missing or ambiguous patch anchors", () => { + const patched = patchRuntimeOfficialMcpAvailability(source); + expect(patchRuntimeOfficialMcpAvailability(patched)).toBe(patched); + expect(() => patchRuntimeOfficialMcpAvailability("changed upstream")).toThrow("incompatible"); + expect(() => patchRuntimeOfficialMcpAvailability(source + source)).toThrow("incompatible"); + }); +});