diff --git a/packages/opencode/src/server/cors.ts b/packages/opencode/src/server/cors.ts index 92296a3b7dbf..9213b34df553 100644 --- a/packages/opencode/src/server/cors.ts +++ b/packages/opencode/src/server/cors.ts @@ -15,6 +15,7 @@ export function isAllowedCorsOrigin(input: string | undefined, opts?: CorsOption if (input.startsWith("oc://renderer")) return true if (input === "tauri://localhost" || input === "http://tauri.localhost" || input === "https://tauri.localhost") return true + if (input.startsWith("vscode-webview://")) return true if (opencodeOrigin.test(input)) return true return opts?.cors?.includes(input) ?? false } diff --git a/packages/opencode/test/server/httpapi-cors.test.ts b/packages/opencode/test/server/httpapi-cors.test.ts index 4e9680c7ce4b..4bd8e6ffe7c8 100644 --- a/packages/opencode/test/server/httpapi-cors.test.ts +++ b/packages/opencode/test/server/httpapi-cors.test.ts @@ -119,4 +119,27 @@ describe("HttpApi CORS", () => { expect(rejected.headers.get("access-control-allow-origin")).not.toBe("https://evil.example") }), ) + + it.live("allows vscode-webview:// origins without an explicit --cors entry", () => + Effect.gen(function* () { + // VS Code assigns each WebviewPanel a fresh `vscode-webview://` + // origin. Without this allowance, extensions that embed opencode would + // have to restart the process on every new panel to refresh the --cors + // allowlist (killing any in-flight turn). Mirrors the existing tauri + // allowance for the same reason. + const response = yield* HttpClientRequest.options(InstancePaths.path).pipe( + HttpClientRequest.setHeaders({ + origin: "vscode-webview://0abc1234-5678-90de-fghi-jklmnopqrstu", + "access-control-request-method": "GET", + "access-control-request-headers": "authorization", + }), + HttpClient.execute, + ) + + expect(response.status).toBe(204) + expect(response.headers["access-control-allow-origin"]).toBe( + "vscode-webview://0abc1234-5678-90de-fghi-jklmnopqrstu", + ) + }), + ) })