diff --git a/config/knip.config.ts b/config/knip.config.ts index 7c030b16239b..119ad944b234 100644 --- a/config/knip.config.ts +++ b/config/knip.config.ts @@ -665,10 +665,7 @@ const config = { }, [`${BUNDLED_PLUGIN_ROOT_DIR}/amazon-bedrock-mantle`]: bundledPluginWorkspace(), [`${BUNDLED_PLUGIN_ROOT_DIR}/amazon-bedrock`]: bundledPluginWorkspace(), - [`${BUNDLED_PLUGIN_ROOT_DIR}/anthropic`]: bundledPluginWorkspace([ - // The plugin-SDK anthropic-cli facade resolves this shipped artifact by basename. - "cli-api.ts!", - ]), + [`${BUNDLED_PLUGIN_ROOT_DIR}/anthropic`]: bundledPluginWorkspace(), [`${BUNDLED_PLUGIN_ROOT_DIR}/anthropic-vertex`]: bundledPluginWorkspace(), [`${BUNDLED_PLUGIN_ROOT_DIR}/acpx`]: bundledPluginWorkspace([ // Copied as executable runtime internals by the package artifact manifest. diff --git a/extensions/anthropic/cli-api.ts b/extensions/anthropic/cli-api.ts deleted file mode 100644 index a60afd90c0ed..000000000000 --- a/extensions/anthropic/cli-api.ts +++ /dev/null @@ -1,5 +0,0 @@ -// Lightweight Claude CLI identity artifact for core facades. The full api.js -// barrel drags the whole plugin graph through jiti on source checkouts -// (~130s per cold worker on CI); keep this surface to static facts only. -export { CLAUDE_CLI_BACKEND_ID } from "./cli-constants.js"; -export { isClaudeCliProvider } from "./cli-shared.js"; diff --git a/src/agents/cli-runner.spawn.test.ts b/src/agents/cli-runner.spawn.test.ts index 8ef55cc9d1c9..e3d8ec5b7968 100644 --- a/src/agents/cli-runner.spawn.test.ts +++ b/src/agents/cli-runner.spawn.test.ts @@ -84,11 +84,6 @@ vi.mock("../gateway/mcp-http.loopback-runtime.js", async (importOriginal) => { }; }); -vi.mock("../plugin-sdk/anthropic-cli.js", () => ({ - CLAUDE_CLI_BACKEND_ID: "claude-cli", - isClaudeCliProvider: (providerId: string) => providerId === "claude-cli", -})); - function emitClaudeInputStarted(stdout: ((chunk: string) => void) | undefined, data: string): void { const event = createClaudeInputStartedEvent(data); if (event) { diff --git a/src/agents/cli-runner/claude-live-background-tasks.test.ts b/src/agents/cli-runner/claude-live-background-tasks.test.ts index 635d20a098a3..b8800c6ee799 100644 --- a/src/agents/cli-runner/claude-live-background-tasks.test.ts +++ b/src/agents/cli-runner/claude-live-background-tasks.test.ts @@ -22,11 +22,6 @@ import { setCliRunnerExecuteTestDeps } from "./execute.test-support.js"; import { writeCliSystemPromptFile } from "./helpers.js"; import type { PreparedCliRunContext } from "./types.js"; -vi.mock("../../plugin-sdk/anthropic-cli.js", () => ({ - CLAUDE_CLI_BACKEND_ID: "claude-cli", - isClaudeCliProvider: (providerId: string) => providerId === "claude-cli", -})); - type ProcessSupervisor = ReturnType; type SupervisorSpawnFn = ProcessSupervisor["spawn"]; diff --git a/src/agents/cli-runner/claude-live-process-approval.test.ts b/src/agents/cli-runner/claude-live-process-approval.test.ts index 57f518476af9..6ba6e76f25b7 100644 --- a/src/agents/cli-runner/claude-live-process-approval.test.ts +++ b/src/agents/cli-runner/claude-live-process-approval.test.ts @@ -25,11 +25,6 @@ import { callGatewayTool } from "../tools/gateway.js"; import { resetClaudeLiveSessionsForTest } from "./claude-live-session.test-support.js"; import { executePreparedCliRun } from "./execute.js"; -vi.mock("../../plugin-sdk/anthropic-cli.js", () => ({ - CLAUDE_CLI_BACKEND_ID: "claude-cli", - isClaudeCliProvider: (providerId: string) => providerId === "claude-cli", -})); - vi.mock("../tools/gateway.js", () => ({ callGatewayTool: vi.fn(), })); diff --git a/src/agents/cli-runner/claude-live-process.test.ts b/src/agents/cli-runner/claude-live-process.test.ts index b4ca469aeb4b..e3ce337b9e39 100644 --- a/src/agents/cli-runner/claude-live-process.test.ts +++ b/src/agents/cli-runner/claude-live-process.test.ts @@ -31,11 +31,6 @@ import { executePreparedCliRun } from "./execute.js"; import { cliBackendLog } from "./log.js"; import type { PreparedCliRunContext } from "./types.js"; -vi.mock("../../plugin-sdk/anthropic-cli.js", () => ({ - CLAUDE_CLI_BACKEND_ID: "claude-cli", - isClaudeCliProvider: (providerId: string) => providerId === "claude-cli", -})); - vi.mock("../tools/gateway.js", () => ({ callGatewayTool: vi.fn(), })); diff --git a/src/agents/cli-runner/claude-live-registry.test.ts b/src/agents/cli-runner/claude-live-registry.test.ts index 6097f6280d17..c4bf0b4db5d2 100644 --- a/src/agents/cli-runner/claude-live-registry.test.ts +++ b/src/agents/cli-runner/claude-live-registry.test.ts @@ -34,11 +34,6 @@ import { setCliRunnerExecuteTestDeps } from "./execute.test-support.js"; import { writeCliSystemPromptFile } from "./helpers.js"; import { cliBackendLog } from "./log.js"; -vi.mock("../../plugin-sdk/anthropic-cli.js", () => ({ - CLAUDE_CLI_BACKEND_ID: "claude-cli", - isClaudeCliProvider: (providerId: string) => providerId === "claude-cli", -})); - type ProcessSupervisor = ReturnType; type SupervisorSpawnFn = ProcessSupervisor["spawn"]; const tempDirs = useAutoCleanupTempDirTracker(afterEach); diff --git a/src/commands/onboard-channels.e2e.test.ts b/src/commands/onboard-channels.e2e.test.ts index 15e4cbc3a5d4..f125fa4edd09 100644 --- a/src/commands/onboard-channels.e2e.test.ts +++ b/src/commands/onboard-channels.e2e.test.ts @@ -519,11 +519,6 @@ vi.mock("../plugins/manifest-registry.js", async () => { }; }); -vi.mock("../plugin-sdk/matrix-deps.js", () => ({ - ensureMatrixSdkInstalled: vi.fn(async () => {}), - isMatrixSdkAvailable: vi.fn(() => true), -})); - vi.mock("../channels/plugins/bundled.js", () => ({ getBundledChannelSetupPlugin: (channel: string) => channel === "telegram" diff --git a/src/plugin-sdk/anthropic-cli.ts b/src/plugin-sdk/anthropic-cli.ts deleted file mode 100644 index db900710d185..000000000000 --- a/src/plugin-sdk/anthropic-cli.ts +++ /dev/null @@ -1,22 +0,0 @@ -// Manual facade. Keep loader boundary explicit. -import { loadBundledPluginPublicSurfaceModuleSyncCore } from "./facade-loader.js"; - -type FacadeModule = { - CLAUDE_CLI_BACKEND_ID: string; - isClaudeCliProvider: (providerId: string) => boolean; -}; - -function loadFacadeModule(): FacadeModule { - // cli-api.js, not api.js: this facade evaluates at module scope, and the - // full barrel costs ~130s per cold jiti worker on source checkouts. - return loadBundledPluginPublicSurfaceModuleSyncCore({ - dirName: "anthropic", - artifactBasename: "cli-api.js", - }); -} -/** Anthropic plugin backend id for Claude CLI provider detection. */ -export const CLAUDE_CLI_BACKEND_ID: FacadeModule["CLAUDE_CLI_BACKEND_ID"] = - loadFacadeModule()["CLAUDE_CLI_BACKEND_ID"]; -/** Returns whether a provider id belongs to the Claude CLI backend family. */ -export const isClaudeCliProvider: FacadeModule["isClaudeCliProvider"] = ((...args) => - loadFacadeModule()["isClaudeCliProvider"](...args)) as FacadeModule["isClaudeCliProvider"]; diff --git a/src/plugin-sdk/browser-node-host.test.ts b/src/plugin-sdk/browser-node-host.test.ts deleted file mode 100644 index 1b7007c8bb14..000000000000 --- a/src/plugin-sdk/browser-node-host.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Tests browser node-host facade delegation and unavailable facade behavior. - */ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -const loadActivatedBundledPluginPublicSurfaceModuleSync = vi.hoisted(() => vi.fn()); - -vi.mock("./facade-runtime.js", () => ({ - loadActivatedBundledPluginPublicSurfaceModuleSync, -})); - -describe("browser node-host facade", () => { - beforeEach(() => { - loadActivatedBundledPluginPublicSurfaceModuleSync.mockReset(); - }); - - it("stays cold until the proxy command is called", async () => { - await import("./browser-node-host.js"); - - expect(loadActivatedBundledPluginPublicSurfaceModuleSync).not.toHaveBeenCalled(); - }); - - it("delegates the proxy command through the activated runtime facade", async () => { - const runBrowserProxyCommand = vi.fn(async () => '{"ok":true}'); - loadActivatedBundledPluginPublicSurfaceModuleSync.mockReturnValue({ - runBrowserProxyCommand, - }); - - const facade = await import("./browser-node-host.js"); - - await expect(facade.runBrowserProxyCommand('{"path":"/"}')).resolves.toBe('{"ok":true}'); - expect(loadActivatedBundledPluginPublicSurfaceModuleSync).toHaveBeenCalledWith({ - dirName: "browser", - artifactBasename: "runtime-api.js", - }); - expect(runBrowserProxyCommand).toHaveBeenCalledWith('{"path":"/"}'); - }); -}); diff --git a/src/plugin-sdk/browser-node-host.ts b/src/plugin-sdk/browser-node-host.ts deleted file mode 100644 index 2ef45e66245e..000000000000 --- a/src/plugin-sdk/browser-node-host.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Public SDK facade for invoking browser plugin node-host proxy commands. - */ -import { loadActivatedBundledPluginPublicSurfaceModuleSync } from "./facade-runtime.js"; - -type BrowserNodeHostFacadeModule = { - runBrowserProxyCommand(paramsJSON?: string | null): Promise; -}; - -function loadFacadeModule(): BrowserNodeHostFacadeModule { - return loadActivatedBundledPluginPublicSurfaceModuleSync({ - dirName: "browser", - artifactBasename: "runtime-api.js", - }); -} - -/** Runs a serialized browser proxy command through the activated browser plugin facade. */ -export async function runBrowserProxyCommand(paramsJSON?: string | null): Promise { - return await loadFacadeModule().runBrowserProxyCommand(paramsJSON); -} diff --git a/src/plugin-sdk/fetch-auth.test.ts b/src/plugin-sdk/fetch-auth.test.ts deleted file mode 100644 index 54846e77a220..000000000000 --- a/src/plugin-sdk/fetch-auth.test.ts +++ /dev/null @@ -1,185 +0,0 @@ -// Fetch auth tests cover scoped bearer fallback retries and request header preservation. -import { describe, expect, it, vi } from "vitest"; -import { fetchWithBearerAuthScopeFallback } from "./fetch-auth.js"; -import { resolveRequestUrl } from "./request-url.js"; - -const asFetch = (fn: unknown): typeof fetch => fn as typeof fetch; - -function fetchCall(fetchFn: ReturnType, index: number): [unknown, RequestInit?] { - const call = fetchFn.mock.calls[index]; - if (!call) { - throw new Error(`expected fetch call ${index}`); - } - return call as [unknown, RequestInit?]; -} - -describe("fetchWithBearerAuthScopeFallback", () => { - it("rejects non-https urls when https is required", async () => { - await expect( - fetchWithBearerAuthScopeFallback({ - url: "http://example.com/file", - scopes: [], - requireHttps: true, - }), - ).rejects.toThrow("URL must use HTTPS"); - }); - - it.each([ - { - name: "returns immediately when the first attempt succeeds", - url: "https://example.com/file", - scopes: ["https://graph.microsoft.com"], - responses: [new Response("ok", { status: 200 })], - shouldAttachAuth: undefined, - expectedStatus: 200, - expectedFetchCalls: 1, - expectedTokenCalls: [] as string[], - expectedAuthHeader: null, - }, - { - name: "retries with auth scopes after a 401 response", - url: "https://graph.microsoft.com/v1.0/me", - scopes: ["https://graph.microsoft.com", "https://api.botframework.com"], - responses: [ - new Response("unauthorized", { status: 401 }), - new Response("ok", { status: 200 }), - ], - shouldAttachAuth: undefined, - expectedStatus: 200, - expectedFetchCalls: 2, - expectedTokenCalls: ["https://graph.microsoft.com"], - expectedAuthHeader: "Bearer token-1", - }, - { - name: "does not attach auth when host predicate rejects url", - url: "https://example.com/file", - scopes: ["https://graph.microsoft.com"], - responses: [new Response("unauthorized", { status: 401 })], - shouldAttachAuth: () => false, - expectedStatus: 401, - expectedFetchCalls: 1, - expectedTokenCalls: [] as string[], - expectedAuthHeader: null, - }, - ])( - "$name", - async ({ - url, - scopes, - responses, - shouldAttachAuth, - expectedStatus, - expectedFetchCalls, - expectedTokenCalls, - expectedAuthHeader, - }) => { - const fetchFn = vi.fn(); - for (const response of responses) { - fetchFn.mockResolvedValueOnce(response); - } - const tokenProvider = { getAccessToken: vi.fn(async () => "token-1") }; - - const response = await fetchWithBearerAuthScopeFallback({ - url, - scopes, - fetchFn: asFetch(fetchFn), - tokenProvider, - shouldAttachAuth, - }); - - expect(response.status).toBe(expectedStatus); - expect(fetchFn).toHaveBeenCalledTimes(expectedFetchCalls); - const tokenCalls = tokenProvider.getAccessToken.mock.calls as unknown as Array<[string]>; - expect(tokenCalls.map(([scope]) => scope)).toEqual(expectedTokenCalls); - if (expectedAuthHeader === null) { - return; - } - const secondCallInit = fetchCall(fetchFn, 1)[1]; - const secondHeaders = new Headers(secondCallInit?.headers); - expect(secondHeaders.get("authorization")).toBe(expectedAuthHeader); - }, - ); - - it("continues across scopes when token retrieval fails", async () => { - const fetchFn = vi - .fn() - .mockResolvedValueOnce(new Response("unauthorized", { status: 401 })) - .mockResolvedValueOnce(new Response("ok", { status: 200 })); - const tokenProvider = { - getAccessToken: vi - .fn() - .mockRejectedValueOnce(new Error("first scope failed")) - .mockResolvedValueOnce("token-2"), - }; - - const response = await fetchWithBearerAuthScopeFallback({ - url: "https://graph.microsoft.com/v1.0/me", - scopes: ["https://first.example", "https://second.example"], - fetchFn: asFetch(fetchFn), - tokenProvider, - }); - - expect(response.status).toBe(200); - expect(tokenProvider.getAccessToken).toHaveBeenCalledTimes(2); - expect(tokenProvider.getAccessToken).toHaveBeenNthCalledWith(1, "https://first.example"); - expect(tokenProvider.getAccessToken).toHaveBeenNthCalledWith(2, "https://second.example"); - }); - - it("normalizes symbol-bearing request headers across unauthenticated and retry attempts", async () => { - const headers = { Accept: "application/json" } as Record & { - [key: symbol]: unknown; - }; - Object.defineProperty(headers, Symbol("sensitiveHeaders"), { - value: new Set(["accept"]), - enumerable: false, - }); - const fetchFn = vi.fn(async (_url: string, init?: RequestInit) => { - const normalizedHeaders = new Headers(init?.headers); - expect(normalizedHeaders.get("accept")).toBe("application/json"); - return fetchFn.mock.calls.length === 1 - ? new Response("unauthorized", { status: 401 }) - : new Response("ok", { status: 200 }); - }); - const tokenProvider = { getAccessToken: vi.fn(async () => "token-1") }; - - const response = await fetchWithBearerAuthScopeFallback({ - url: "https://graph.microsoft.com/v1.0/me", - scopes: ["https://graph.microsoft.com"], - fetchFn: asFetch(fetchFn), - tokenProvider, - requestInit: { headers }, - }); - - expect(response.status).toBe(200); - expect(fetchFn).toHaveBeenCalledTimes(2); - expect(Object.getOwnPropertySymbols(fetchCall(fetchFn, 0)[1]?.headers as object)).toStrictEqual( - [], - ); - expect(new Headers(fetchCall(fetchFn, 1)[1]?.headers).get("authorization")).toBe( - "Bearer token-1", - ); - expect(Object.getOwnPropertySymbols(headers)).toHaveLength(1); - }); -}); - -describe("resolveRequestUrl", () => { - it.each([ - { - name: "resolves string input", - input: "https://example.com/a", - expected: "https://example.com/a", - }, - { - name: "resolves URL input", - input: new URL("https://example.com/b"), - expected: "https://example.com/b", - }, - { - name: "resolves object input with url field", - input: { url: "https://example.com/c" } as unknown as RequestInfo, - expected: "https://example.com/c", - }, - ])("$name", ({ input, expected }) => { - expect(resolveRequestUrl(input)).toBe(expected); - }); -}); diff --git a/src/plugin-sdk/fetch-auth.ts b/src/plugin-sdk/fetch-auth.ts deleted file mode 100644 index f6e0dfd783f7..000000000000 --- a/src/plugin-sdk/fetch-auth.ts +++ /dev/null @@ -1,89 +0,0 @@ -// Fetch auth helpers provide scoped bearer-token retries for plugin HTTP requests. -import { - normalizeHeadersInitForFetch, - normalizeRequestInitHeadersForFetch, -} from "../infra/fetch-headers.js"; - -/** Token source used by scoped bearer-auth fetch retries. */ -export type ScopeTokenProvider = { - /** Return a bearer token for the requested OAuth/API scope. */ - getAccessToken: (scope: string) => Promise; -}; - -function isAuthFailureStatus(status: number): boolean { - return status === 401 || status === 403; -} - -/** Retry a fetch with bearer tokens from the provided scopes when the unauthenticated attempt fails. */ -export async function fetchWithBearerAuthScopeFallback(params: { - /** Absolute URL to request. */ - url: string; - /** Token scopes to try in order after the initial unauthenticated request fails. */ - scopes: readonly string[]; - /** Optional token source; when omitted, only the unauthenticated request is attempted. */ - tokenProvider?: ScopeTokenProvider; - /** Fetch implementation override for tests or plugin runtimes. Defaults to global `fetch`. */ - fetchFn?: typeof fetch; - /** Request options reused across unauthenticated and authenticated attempts. */ - requestInit?: RequestInit; - /** Reject non-HTTPS URLs before any request is sent. */ - requireHttps?: boolean; - /** Optional policy gate for whether this URL is allowed to receive bearer auth. */ - shouldAttachAuth?: (url: string) => boolean; - /** Override which responses should trigger scoped-token retries. Defaults to 401/403. */ - shouldRetry?: (response: Response) => boolean; -}): Promise { - const fetchFn = params.fetchFn ?? fetch; - let parsedUrl: URL; - try { - parsedUrl = new URL(params.url); - } catch { - throw new Error(`Invalid URL: ${params.url}`); - } - if (params.requireHttps === true && parsedUrl.protocol !== "https:") { - throw new Error(`URL must use HTTPS: ${params.url}`); - } - - const requestInit = normalizeRequestInitHeadersForFetch(params.requestInit); - const fetchOnce = (headers?: Headers): Promise => - fetchFn(params.url, { - ...requestInit, - ...(headers ? { headers } : {}), - }); - - const firstAttempt = await fetchOnce(); - if (firstAttempt.ok) { - return firstAttempt; - } - if (!params.tokenProvider) { - return firstAttempt; - } - - const shouldRetry = - params.shouldRetry ?? ((response: Response) => isAuthFailureStatus(response.status)); - if (!shouldRetry(firstAttempt)) { - return firstAttempt; - } - if (params.shouldAttachAuth && !params.shouldAttachAuth(params.url)) { - return firstAttempt; - } - - for (const scope of params.scopes) { - try { - const token = await params.tokenProvider.getAccessToken(scope); - const authHeaders = new Headers(normalizeHeadersInitForFetch(requestInit?.headers)); - authHeaders.set("Authorization", `Bearer ${token}`); - const authAttempt = await fetchOnce(authHeaders); - if (authAttempt.ok) { - return authAttempt; - } - if (!shouldRetry(authAttempt)) { - continue; - } - } catch { - // Ignore token/fetch errors and continue trying remaining scopes. - } - } - - return firstAttempt; -} diff --git a/src/plugin-sdk/matrix-deps.ts b/src/plugin-sdk/matrix-deps.ts deleted file mode 100644 index 88c2a946b634..000000000000 --- a/src/plugin-sdk/matrix-deps.ts +++ /dev/null @@ -1,25 +0,0 @@ -// Manual facade. Keep loader boundary explicit. -import type { RuntimeEnv } from "../runtime.js"; -import { loadBundledPluginPublicSurfaceModuleSyncCore } from "./facade-loader.js"; - -type FacadeModule = { - ensureMatrixSdkInstalled: (params: { - runtime: RuntimeEnv; - confirm?: (message: string) => Promise; - }) => Promise; - isMatrixSdkAvailable: () => boolean; -}; - -function loadFacadeModule(): FacadeModule { - return loadBundledPluginPublicSurfaceModuleSyncCore({ - dirName: "matrix", - artifactBasename: "runtime-api.js", - }); -} - -/** Ensure Matrix plugin runtime dependencies are available before Matrix setup/use. */ -export const ensureMatrixSdkInstalled: FacadeModule["ensureMatrixSdkInstalled"] = ((...args) => - loadFacadeModule().ensureMatrixSdkInstalled(...args)) as FacadeModule["ensureMatrixSdkInstalled"]; -/** Returns whether Matrix SDK dependencies are currently importable. */ -export const isMatrixSdkAvailable: FacadeModule["isMatrixSdkAvailable"] = ((...args) => - loadFacadeModule().isMatrixSdkAvailable(...args)) as FacadeModule["isMatrixSdkAvailable"]; diff --git a/src/plugin-sdk/request-url.test.ts b/src/plugin-sdk/request-url.test.ts new file mode 100644 index 000000000000..d0344583f359 --- /dev/null +++ b/src/plugin-sdk/request-url.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from "vitest"; +import { resolveRequestUrl } from "./request-url.js"; + +describe("resolveRequestUrl", () => { + it.each([ + { + name: "resolves string input", + input: "https://example.com/a", + expected: "https://example.com/a", + }, + { + name: "resolves URL input", + input: new URL("https://example.com/b"), + expected: "https://example.com/b", + }, + { + name: "resolves object input with url field", + input: { url: "https://example.com/c" } as unknown as RequestInfo, + expected: "https://example.com/c", + }, + ])("$name", ({ input, expected }) => { + expect(resolveRequestUrl(input)).toBe(expected); + }); +});