From 2f5c7e0545a7c29f19bc947a5f07391ffc7ba95c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Jun 2026 00:25:23 +0000 Subject: [PATCH 1/6] fix: resolve Copilot SDK custom provider in standalone driver main() The copilot_sdk_driver.cjs standalone main() was not resolving a custom provider from the AWF reflect data before calling runWithCopilotSDK. In offline+BYOK mode the AWF entrypoint unsets COPILOT_GITHUB_TOKEN, so the SDK client must use a provider config pointing at the AWF API proxy instead of token-based Copilot auth. Without a provider the SDK server rejects the session with "Session was not created with authentication info or custom provider", which then surfaces as an unhandled promise rejection and a hard process exit. Fix: move resolveCopilotSDKCustomProviderFromReflect from copilot_harness.cjs into awf_reflect.cjs (the module that already owns all reflect-data helpers), re-export it from copilot_harness.cjs for backward compatibility, and call it inside copilot_sdk_driver.cjs main() so every standalone driver run resolves and passes the provider before creating the SDK session. Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/awf_reflect.cjs | 73 +++++++++++++++++++++ actions/setup/js/awf_reflect.test.cjs | 85 +++++++++++++++++++++++++ actions/setup/js/copilot_harness.cjs | 68 +------------------- actions/setup/js/copilot_sdk_driver.cjs | 20 +++++- 4 files changed, 178 insertions(+), 68 deletions(-) diff --git a/actions/setup/js/awf_reflect.cjs b/actions/setup/js/awf_reflect.cjs index ed0491dc56d..2f7a4742d36 100644 --- a/actions/setup/js/awf_reflect.cjs +++ b/actions/setup/js/awf_reflect.cjs @@ -300,6 +300,78 @@ async function fetchAWFReflect(options) { } } +/** + * Resolve Copilot SDK BYOK custom provider configuration from saved AWF /reflect data. + * Chooses a configured endpoint and maps it to an OpenAI-compatible provider base URL. + * Returns null when no suitable endpoint is found (e.g. no reflect data, or endpoints not + * configured), in which case the SDK client should fall back to token-based auth. + * + * @param {{ + * model?: string, + * reflectPath?: string, + * readFileSync?: (path: string, encoding: BufferEncoding) => string, + * logger?: (msg: string) => void, + * }} [options] + * @returns {{ model: string, provider: { type: "openai", baseUrl: string } } | null} + */ +function resolveCopilotSDKCustomProviderFromReflect(options) { + const configuredModel = typeof options?.model === "string" ? options.model.trim() : ""; + const reflectPath = (options && options.reflectPath) || AWF_REFLECT_OUTPUT_PATH; + const readFile = (options && options.readFileSync) || fs.readFileSync; + const logger = (options && options.logger) || DEFAULT_REFLECT_LOGGER; + + try { + const raw = readFile(reflectPath, "utf8"); + const reflectData = JSON.parse(raw); + const endpoints = Array.isArray(reflectData?.endpoints) ? reflectData.endpoints.filter(ep => ep && ep.configured === true) : []; + if (endpoints.length === 0) { + logger(`sdk-mode: no configured endpoints in ${reflectPath}; skipping custom provider config`); + return null; + } + + const endpoint = + (configuredModel ? endpoints.find(ep => Array.isArray(ep.models) && ep.models.includes(configuredModel)) : null) || + endpoints.find(ep => String(ep.provider || "").toLowerCase() === "copilot") || + endpoints[0]; + + let baseUrl = ""; + if (typeof endpoint?.models_url === "string" && endpoint.models_url) { + try { + baseUrl = new URL(endpoint.models_url).origin; + } catch { + // ignore malformed URL and fall back to port-based construction below + } + } + if (!baseUrl && endpoint?.port != null) { + baseUrl = `http://api-proxy:${String(endpoint.port)}`; + } + if (!baseUrl) { + logger("sdk-mode: unable to derive provider baseUrl from awf-reflect endpoint data; skipping custom provider config"); + return null; + } + + let model = configuredModel; + if (!model && Array.isArray(endpoint?.models)) { + const firstModel = endpoint.models.find(m => typeof m === "string" && m.trim().length > 0); + model = typeof firstModel === "string" ? firstModel.trim() : ""; + } + if (!model) { + logger("sdk-mode: unable to derive model for custom provider from awf-reflect; skipping custom provider config"); + return null; + } + + logger(`sdk-mode: custom provider resolved from awf-reflect (provider=${String(endpoint.provider || "unknown")} baseUrl=${baseUrl} model=${model})`); + return { + model, + provider: { type: "openai", baseUrl }, + }; + } catch (error) { + const err = /** @type {Error} */ (error); + logger(`sdk-mode: unable to read custom provider config from ${reflectPath}: ${err.message}`); + return null; + } +} + if (typeof module !== "undefined" && module.exports) { module.exports = { AWF_API_PROXY_REFLECT_URL, @@ -314,5 +386,6 @@ if (typeof module !== "undefined" && module.exports) { extractModelIds, fetchAWFReflect, fetchModelsFromUrl, + resolveCopilotSDKCustomProviderFromReflect, }; } diff --git a/actions/setup/js/awf_reflect.test.cjs b/actions/setup/js/awf_reflect.test.cjs index 30b904a2cfb..27a40be4850 100644 --- a/actions/setup/js/awf_reflect.test.cjs +++ b/actions/setup/js/awf_reflect.test.cjs @@ -18,6 +18,7 @@ const { extractModelIds, fetchAWFReflect, fetchModelsFromUrl, + resolveCopilotSDKCustomProviderFromReflect, } = require("./awf_reflect.cjs"); describe("awf_reflect.cjs", () => { @@ -305,4 +306,88 @@ describe("awf_reflect.cjs", () => { expect(collected.length).toBeGreaterThan(0); }); }); + + describe("resolveCopilotSDKCustomProviderFromReflect", () => { + it("resolves provider baseUrl and model from port when models_url is absent", () => { + const reflectFile = path.join(os.tmpdir(), `awf-reflect-sdk-provider-${Date.now()}.json`); + try { + fs.writeFileSync( + reflectFile, + JSON.stringify({ + endpoints: [{ provider: "copilot", port: 10002, configured: true, models: ["gpt-5.4", "claude-sonnet-4.6"] }], + }), + "utf8" + ); + expect(resolveCopilotSDKCustomProviderFromReflect({ reflectPath: reflectFile })).toEqual({ + model: "gpt-5.4", + provider: { type: "openai", baseUrl: "http://api-proxy:10002" }, + }); + } finally { + fs.unlinkSync(reflectFile); + } + }); + + it("prefers the endpoint matching the configured model", () => { + const reflectFile = path.join(os.tmpdir(), `awf-reflect-sdk-model-${Date.now()}.json`); + try { + fs.writeFileSync( + reflectFile, + JSON.stringify({ + endpoints: [ + { provider: "openai", port: 10001, configured: true, models: ["gpt-4o"] }, + { provider: "anthropic", port: 10002, configured: true, models: ["claude-sonnet-4.6"] }, + ], + }), + "utf8" + ); + expect(resolveCopilotSDKCustomProviderFromReflect({ reflectPath: reflectFile, model: "claude-sonnet-4.6" })).toEqual({ + model: "claude-sonnet-4.6", + provider: { type: "openai", baseUrl: "http://api-proxy:10002" }, + }); + } finally { + fs.unlinkSync(reflectFile); + } + }); + + it("derives baseUrl from models_url origin when available", () => { + const reflectFile = path.join(os.tmpdir(), `awf-reflect-sdk-url-${Date.now()}.json`); + try { + fs.writeFileSync( + reflectFile, + JSON.stringify({ + endpoints: [{ provider: "copilot", port: 10002, configured: true, models: ["gpt-4o"], models_url: "http://172.30.0.30:10002/v1/models" }], + }), + "utf8" + ); + expect(resolveCopilotSDKCustomProviderFromReflect({ reflectPath: reflectFile })).toEqual({ + model: "gpt-4o", + provider: { type: "openai", baseUrl: "http://172.30.0.30:10002" }, + }); + } finally { + fs.unlinkSync(reflectFile); + } + }); + + it("returns null when no configured endpoints exist", () => { + const reflectFile = path.join(os.tmpdir(), `awf-reflect-sdk-empty-${Date.now()}.json`); + try { + fs.writeFileSync(reflectFile, JSON.stringify({ endpoints: [{ provider: "copilot", port: 10002, configured: false, models: [] }] }), "utf8"); + const logs = []; + expect(resolveCopilotSDKCustomProviderFromReflect({ reflectPath: reflectFile, logger: msg => logs.push(msg) })).toBeNull(); + expect(logs.some(l => l.includes("no configured endpoints"))).toBe(true); + } finally { + fs.unlinkSync(reflectFile); + } + }); + + it("returns null when the reflect file is missing", () => { + const logs = []; + const result = resolveCopilotSDKCustomProviderFromReflect({ + reflectPath: "/tmp/gh-aw-nonexistent-reflect.json", + logger: msg => logs.push(msg), + }); + expect(result).toBeNull(); + expect(logs.some(l => l.includes("unable to read custom provider config"))).toBe(true); + }); + }); }); diff --git a/actions/setup/js/copilot_harness.cjs b/actions/setup/js/copilot_harness.cjs index 05223828746..b18015d0374 100644 --- a/actions/setup/js/copilot_harness.cjs +++ b/actions/setup/js/copilot_harness.cjs @@ -57,6 +57,7 @@ const { extractModelIds, fetchAWFReflect, fetchModelsFromUrl, + resolveCopilotSDKCustomProviderFromReflect, } = require("./awf_reflect.cjs"); const { runSafeOutputsCLI, buildMissingToolAlternatives, emitMissingToolPermissionIssue, emitInfrastructureIncomplete } = require("./safeoutputs_cli.cjs"); const { countPermissionDeniedIssues, hasNumerousPermissionDeniedIssues, extractDeniedCommands, buildMissingToolPermissionIssuePayload } = require("./permission_denied_helpers.cjs"); @@ -230,73 +231,6 @@ function isModelAvailableInReflectFile(model, options) { } } -/** - * Resolve Copilot SDK BYOK custom provider configuration from saved AWF /reflect data. - * Chooses a configured endpoint and maps it to an OpenAI-compatible provider base URL. - * - * @param {{ - * model?: string, - * reflectPath?: string, - * readFileSync?: (path: string, encoding: string) => string, - * logger?: (msg: string) => void, - * }} [options] - * @returns {{ model: string, provider: { type: "openai", baseUrl: string } } | null} - */ -function resolveCopilotSDKCustomProviderFromReflect(options) { - const configuredModel = typeof options?.model === "string" ? options.model.trim() : ""; - const reflectPath = (options && options.reflectPath) || AWF_REFLECT_OUTPUT_PATH; - const readFile = (options && options.readFileSync) || fs.readFileSync; - const logger = (options && options.logger) || log; - - try { - const raw = readFile(reflectPath, "utf8"); - const reflectData = JSON.parse(raw); - const endpoints = Array.isArray(reflectData?.endpoints) ? reflectData.endpoints.filter(ep => ep && ep.configured === true) : []; - if (endpoints.length === 0) { - logger(`sdk-mode: no configured endpoints in ${reflectPath}; skipping custom provider config`); - return null; - } - - const endpoint = (configuredModel ? endpoints.find(ep => Array.isArray(ep.models) && ep.models.includes(configuredModel)) : null) || endpoints.find(ep => String(ep.provider || "").toLowerCase() === "copilot") || endpoints[0]; - - let baseUrl = ""; - if (typeof endpoint?.models_url === "string" && endpoint.models_url) { - try { - baseUrl = new URL(endpoint.models_url).origin; - } catch { - // ignore malformed URL and fall back to port-based construction below - } - } - if (!baseUrl && endpoint?.port != null) { - baseUrl = `http://api-proxy:${String(endpoint.port)}`; - } - if (!baseUrl) { - logger("sdk-mode: unable to derive provider baseUrl from awf-reflect endpoint data; skipping custom provider config"); - return null; - } - - let model = configuredModel; - if (!model && Array.isArray(endpoint?.models)) { - const firstModel = endpoint.models.find(m => typeof m === "string" && m.trim().length > 0); - model = typeof firstModel === "string" ? firstModel.trim() : ""; - } - if (!model) { - logger("sdk-mode: unable to derive model for custom provider from awf-reflect; skipping custom provider config"); - return null; - } - - logger(`sdk-mode: custom provider resolved from awf-reflect (provider=${String(endpoint.provider || "unknown")} baseUrl=${baseUrl} model=${model})`); - return { - model, - provider: { type: "openai", baseUrl }, - }; - } catch (error) { - const err = /** @type {Error} */ error; - logger(`sdk-mode: unable to read custom provider config from ${reflectPath}: ${err.message}`); - return null; - } -} - /** * Determines if the collected output contains a "No authentication information found" error. * This means no auth token (COPILOT_GITHUB_TOKEN, GH_TOKEN, or GITHUB_TOKEN) is available diff --git a/actions/setup/js/copilot_sdk_driver.cjs b/actions/setup/js/copilot_sdk_driver.cjs index 7e9e307a7d4..7f738b133ce 100644 --- a/actions/setup/js/copilot_sdk_driver.cjs +++ b/actions/setup/js/copilot_sdk_driver.cjs @@ -39,6 +39,7 @@ const fs = require("fs"); const path = require("path"); const os = require("os"); +const { resolveCopilotSDKCustomProviderFromReflect } = require("./awf_reflect.cjs"); // Default timeout for a single sendAndWait call: 10 minutes. // This is intentionally generous — the headless Copilot CLI has its own internal @@ -492,14 +493,31 @@ async function main() { log(`connecting to sidecar at ${sdkUri}`); + // --- Resolve custom provider from AWF reflect data ------------------ + // In offline+BYOK mode COPILOT_GITHUB_TOKEN is unset by the AWF entrypoint. + // The SDK server and client must use a custom provider pointing at the AWF + // API proxy instead of token-based Copilot auth. The AWF harness fetches + // the /reflect payload before launching the driver, so the file is available + // at the well-known path when this function runs. + const customProvider = resolveCopilotSDKCustomProviderFromReflect({ + model, + logger: log, + }); + const provider = customProvider ? customProvider.provider : undefined; + // When the reflect data provides a model, prefer it over the env-var model so + // the provider and model stay consistent (e.g. BYOK endpoint may enforce a + // specific model ID). + const resolvedModel = customProvider ? customProvider.model : model; + // --- Run SDK session ------------------------------------------------- const result = await runWithCopilotSDK({ sdkUri, prompt, logger: log, - model, + model: resolvedModel, connectionToken, + provider, }); process.exit(result.exitCode); From 12677b5a0ea292eb459dec3e0cff8dc77d2911ea Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 4 Jun 2026 00:36:33 +0000 Subject: [PATCH 2/6] Add changeset --- .changeset/patch-fix-copilot-sdk-provider.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/patch-fix-copilot-sdk-provider.md diff --git a/.changeset/patch-fix-copilot-sdk-provider.md b/.changeset/patch-fix-copilot-sdk-provider.md new file mode 100644 index 00000000000..1ac7bc6daf4 --- /dev/null +++ b/.changeset/patch-fix-copilot-sdk-provider.md @@ -0,0 +1,5 @@ +--- +"gh-aw": patch +--- + +Fixed the standalone Copilot SDK driver so it resolves the custom provider from the AWF reflect payload and can authenticate in offline/BYOK mode. From 20da69b552a28bdb10c38301eac50ec2dca4ecfb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Jun 2026 00:52:02 +0000 Subject: [PATCH 3/6] fix: use live reflect data for BYOK provider, fail if not configured - fetchAWFReflect now returns reflectData in success result - resolveCopilotSDKCustomProviderFromReflect accepts reflectData directly (live data from harness), falling back to file reading only when not provided - copilot_harness.cjs: fetch reflect data before building childEnv, resolve BYOK provider from live data, fail fast if provider not configured, pass GH_AW_COPILOT_SDK_PROVIDER_BASE_URL to driver subprocess env - copilot_sdk_driver.cjs: read GH_AW_COPILOT_SDK_PROVIDER_BASE_URL from env, fail immediately if not set (BYOK is the only supported mode) - tests: add reflectData param coverage, update fetchAWFReflect result assertion Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/awf_reflect.cjs | 104 ++++++++++++++---------- actions/setup/js/awf_reflect.test.cjs | 28 +++++++ actions/setup/js/copilot_harness.cjs | 47 ++++++++--- actions/setup/js/copilot_sdk_driver.cjs | 31 ++++--- 4 files changed, 137 insertions(+), 73 deletions(-) diff --git a/actions/setup/js/awf_reflect.cjs b/actions/setup/js/awf_reflect.cjs index 2f7a4742d36..b822c64034a 100644 --- a/actions/setup/js/awf_reflect.cjs +++ b/actions/setup/js/awf_reflect.cjs @@ -226,6 +226,7 @@ async function enrichReflectModels(reflectData, timeoutMs, logger) { * reflectUrl: string, * outputPath: string, * bytesWritten?: number, + * reflectData?: object, * reason?: "unexpected_status"|"timeout"|"request_failed", * status?: number, * error?: string, @@ -275,6 +276,7 @@ async function fetchAWFReflect(options) { reflectUrl, outputPath, bytesWritten: enrichedBody.length, + reflectData, }; } catch (err) { const e = /** @type {Error} */ err; @@ -301,13 +303,17 @@ async function fetchAWFReflect(options) { } /** - * Resolve Copilot SDK BYOK custom provider configuration from saved AWF /reflect data. + * Resolve Copilot SDK BYOK custom provider configuration from AWF /reflect data. * Chooses a configured endpoint and maps it to an OpenAI-compatible provider base URL. * Returns null when no suitable endpoint is found (e.g. no reflect data, or endpoints not - * configured), in which case the SDK client should fall back to token-based auth. + * configured). + * + * Accepts live reflect data directly via `reflectData` (preferred — avoids re-reading the + * persisted file). Falls back to reading from `reflectPath` when `reflectData` is not provided. * * @param {{ * model?: string, + * reflectData?: object | null, * reflectPath?: string, * readFileSync?: (path: string, encoding: BufferEncoding) => string, * logger?: (msg: string) => void, @@ -316,60 +322,68 @@ async function fetchAWFReflect(options) { */ function resolveCopilotSDKCustomProviderFromReflect(options) { const configuredModel = typeof options?.model === "string" ? options.model.trim() : ""; - const reflectPath = (options && options.reflectPath) || AWF_REFLECT_OUTPUT_PATH; - const readFile = (options && options.readFileSync) || fs.readFileSync; const logger = (options && options.logger) || DEFAULT_REFLECT_LOGGER; - try { - const raw = readFile(reflectPath, "utf8"); - const reflectData = JSON.parse(raw); - const endpoints = Array.isArray(reflectData?.endpoints) ? reflectData.endpoints.filter(ep => ep && ep.configured === true) : []; - if (endpoints.length === 0) { - logger(`sdk-mode: no configured endpoints in ${reflectPath}; skipping custom provider config`); + let reflectData; + if (options && "reflectData" in options) { + // Live data passed directly by the harness — use it without touching the file system. + reflectData = options.reflectData; + } else { + // Fallback: read from the persisted reflect file. + const reflectPath = (options && options.reflectPath) || AWF_REFLECT_OUTPUT_PATH; + const readFile = (options && options.readFileSync) || fs.readFileSync; + try { + const raw = readFile(reflectPath, "utf8"); + reflectData = JSON.parse(raw); + } catch (error) { + const err = /** @type {Error} */ (error); + logger(`sdk-mode: unable to read custom provider config from ${reflectPath}: ${err.message}`); return null; } + } - const endpoint = - (configuredModel ? endpoints.find(ep => Array.isArray(ep.models) && ep.models.includes(configuredModel)) : null) || - endpoints.find(ep => String(ep.provider || "").toLowerCase() === "copilot") || - endpoints[0]; + const endpoints = Array.isArray(reflectData?.endpoints) ? reflectData.endpoints.filter(ep => ep && ep.configured === true) : []; + if (endpoints.length === 0) { + logger("sdk-mode: no configured endpoints in awf-reflect data; cannot resolve custom provider"); + return null; + } - let baseUrl = ""; - if (typeof endpoint?.models_url === "string" && endpoint.models_url) { - try { - baseUrl = new URL(endpoint.models_url).origin; - } catch { - // ignore malformed URL and fall back to port-based construction below - } - } - if (!baseUrl && endpoint?.port != null) { - baseUrl = `http://api-proxy:${String(endpoint.port)}`; - } - if (!baseUrl) { - logger("sdk-mode: unable to derive provider baseUrl from awf-reflect endpoint data; skipping custom provider config"); - return null; - } + const endpoint = + (configuredModel ? endpoints.find(ep => Array.isArray(ep.models) && ep.models.includes(configuredModel)) : null) || + endpoints.find(ep => String(ep.provider || "").toLowerCase() === "copilot") || + endpoints[0]; - let model = configuredModel; - if (!model && Array.isArray(endpoint?.models)) { - const firstModel = endpoint.models.find(m => typeof m === "string" && m.trim().length > 0); - model = typeof firstModel === "string" ? firstModel.trim() : ""; - } - if (!model) { - logger("sdk-mode: unable to derive model for custom provider from awf-reflect; skipping custom provider config"); - return null; + let baseUrl = ""; + if (typeof endpoint?.models_url === "string" && endpoint.models_url) { + try { + baseUrl = new URL(endpoint.models_url).origin; + } catch { + // ignore malformed URL and fall back to port-based construction below } + } + if (!baseUrl && endpoint?.port != null) { + baseUrl = `http://api-proxy:${String(endpoint.port)}`; + } + if (!baseUrl) { + logger("sdk-mode: unable to derive provider baseUrl from awf-reflect endpoint data; cannot resolve custom provider"); + return null; + } - logger(`sdk-mode: custom provider resolved from awf-reflect (provider=${String(endpoint.provider || "unknown")} baseUrl=${baseUrl} model=${model})`); - return { - model, - provider: { type: "openai", baseUrl }, - }; - } catch (error) { - const err = /** @type {Error} */ (error); - logger(`sdk-mode: unable to read custom provider config from ${reflectPath}: ${err.message}`); + let model = configuredModel; + if (!model && Array.isArray(endpoint?.models)) { + const firstModel = endpoint.models.find(m => typeof m === "string" && m.trim().length > 0); + model = typeof firstModel === "string" ? firstModel.trim() : ""; + } + if (!model) { + logger("sdk-mode: unable to derive model for custom provider from awf-reflect; cannot resolve custom provider"); return null; } + + logger(`sdk-mode: custom provider resolved from awf-reflect (provider=${String(endpoint.provider || "unknown")} baseUrl=${baseUrl} model=${model})`); + return { + model, + provider: { type: "openai", baseUrl }, + }; } if (typeof module !== "undefined" && module.exports) { diff --git a/actions/setup/js/awf_reflect.test.cjs b/actions/setup/js/awf_reflect.test.cjs index 27a40be4850..bf19bede072 100644 --- a/actions/setup/js/awf_reflect.test.cjs +++ b/actions/setup/js/awf_reflect.test.cjs @@ -245,6 +245,7 @@ describe("awf_reflect.cjs", () => { reflectUrl: "http://api-proxy:10000/reflect", outputPath, bytesWritten: expect.any(Number), + reflectData: expect.objectContaining({ endpoints: expect.any(Array) }), }); const saved = JSON.parse(fs.readFileSync(outputPath, "utf8")); expect(saved.endpoints[0].models).toEqual(["gpt-4o", "gpt-4o-mini"]); @@ -389,5 +390,32 @@ describe("awf_reflect.cjs", () => { expect(result).toBeNull(); expect(logs.some(l => l.includes("unable to read custom provider config"))).toBe(true); }); + + it("resolves provider from live reflectData without touching the file system", () => { + const reflectData = { + endpoints: [{ provider: "copilot", port: 10003, configured: true, models: ["gpt-5.4"] }], + }; + expect(resolveCopilotSDKCustomProviderFromReflect({ reflectData })).toEqual({ + model: "gpt-5.4", + provider: { type: "openai", baseUrl: "http://api-proxy:10003" }, + }); + }); + + it("returns null when reflectData has no configured endpoints", () => { + const logs = []; + const result = resolveCopilotSDKCustomProviderFromReflect({ + reflectData: { endpoints: [{ provider: "copilot", port: 10002, configured: false, models: ["gpt-4o"] }] }, + logger: msg => logs.push(msg), + }); + expect(result).toBeNull(); + expect(logs.some(l => l.includes("no configured endpoints"))).toBe(true); + }); + + it("returns null when reflectData is null", () => { + const logs = []; + const result = resolveCopilotSDKCustomProviderFromReflect({ reflectData: null, logger: msg => logs.push(msg) }); + expect(result).toBeNull(); + expect(logs.some(l => l.includes("no configured endpoints"))).toBe(true); + }); }); }); diff --git a/actions/setup/js/copilot_harness.cjs b/actions/setup/js/copilot_harness.cjs index b18015d0374..79e6a867438 100644 --- a/actions/setup/js/copilot_harness.cjs +++ b/actions/setup/js/copilot_harness.cjs @@ -538,14 +538,6 @@ async function main() { log("copilot-sdk mode active: generated per-run COPILOT_CONNECTION_TOKEN"); log(`copilot-sdk mode active: COPILOT_SDK_URI=${sdkEnv.COPILOT_SDK_URI || "(not set)"}`); } - // Merge SDK env additions into the child process env only when the SDK helper - // returned at least one variable; otherwise leave the env undefined so that - // runProcess inherits the full process.env (the common case). - // sdkEnv already contains SDK-mode variables (e.g. COPILOT_SDK_URI) when enabled. - // Always attach the generated per-run COPILOT_CONNECTION_TOKEN so both the sidecar - // (started by the harness) and the SDK client share the same token. - const sdkChildEnv = copilotSDKMode ? { ...sdkEnv, COPILOT_CONNECTION_TOKEN: copilotConnectionToken } : sdkEnv; - const childEnv = Object.keys(sdkChildEnv).length > 0 ? { ...process.env, ...sdkChildEnv } : undefined; // In driver mode the args are the driver command + copilot binary path; no stdin payload. // In CLI mode, args are resolved to inline prompt text. @@ -556,13 +548,46 @@ async function main() { resolvedArgs = resolvePromptFileArgs(args); } - // Fetch AWF API proxy reflection data before running the agent to capture initial proxy state. - // This is best-effort: failures are logged but do not affect the agent run. + // Fetch AWF API proxy reflection data before running the agent. + // In SDK/BYOK mode the live data is used immediately to resolve the custom provider + // configuration that is injected into the driver subprocess environment. // Skip when AWF_REFLECT_ENABLED is not "1" (e.g. sandbox.agent: false — no api-proxy running). + let awfReflectData = null; if (process.env.AWF_REFLECT_ENABLED === "1") { - await fetchAWFReflect({ logger: log }); + const reflectResult = await fetchAWFReflect({ logger: log }); + if (reflectResult.ok && reflectResult.reflectData) { + awfReflectData = reflectResult.reflectData; + } + } + + // Resolve BYOK custom provider from live reflect data (SDK mode only). + // BYOK is the only supported mode for SDK sessions — fail immediately if the provider + // cannot be resolved so retries are not wasted on a misconfigured environment. + let sdkProviderBaseUrl = ""; + if (copilotSDKMode) { + const configuredModel = process.env.COPILOT_MODEL || ""; + const customProvider = resolveCopilotSDKCustomProviderFromReflect({ model: configuredModel, reflectData: awfReflectData, logger: log }); + if (!customProvider) { + log("copilot-sdk driver mode: BYOK provider is required but could not be resolved from awf-reflect data — aborting"); + process.exit(1); + } + sdkProviderBaseUrl = customProvider.provider.baseUrl; + log(`copilot-sdk driver mode: BYOK provider resolved (baseUrl=${sdkProviderBaseUrl})`); } + // Merge SDK env additions into the child process env only when the SDK helper + // returned at least one variable; otherwise leave the env undefined so that + // runProcess inherits the full process.env (the common case). + // sdkEnv already contains SDK-mode variables (e.g. COPILOT_SDK_URI) when enabled. + // Always attach the generated per-run COPILOT_CONNECTION_TOKEN so both the sidecar + // (started by the harness) and the SDK client share the same token. + // In SDK mode also inject the resolved BYOK provider base URL so the driver subprocess + // does not need to re-read the reflect file. + const sdkChildEnv = copilotSDKMode + ? { ...sdkEnv, COPILOT_CONNECTION_TOKEN: copilotConnectionToken, GH_AW_COPILOT_SDK_PROVIDER_BASE_URL: sdkProviderBaseUrl } + : sdkEnv; + const childEnv = Object.keys(sdkChildEnv).length > 0 ? { ...process.env, ...sdkChildEnv } : undefined; + let delay = INITIAL_DELAY_MS; let lastExitCode = 1; const isScheduledRun = process.env.GITHUB_EVENT_NAME === "schedule"; diff --git a/actions/setup/js/copilot_sdk_driver.cjs b/actions/setup/js/copilot_sdk_driver.cjs index 7f738b133ce..953cd18b3ad 100644 --- a/actions/setup/js/copilot_sdk_driver.cjs +++ b/actions/setup/js/copilot_sdk_driver.cjs @@ -39,7 +39,6 @@ const fs = require("fs"); const path = require("path"); const os = require("os"); -const { resolveCopilotSDKCustomProviderFromReflect } = require("./awf_reflect.cjs"); // Default timeout for a single sendAndWait call: 10 minutes. // This is intentionally generous — the headless Copilot CLI has its own internal @@ -493,21 +492,19 @@ async function main() { log(`connecting to sidecar at ${sdkUri}`); - // --- Resolve custom provider from AWF reflect data ------------------ - // In offline+BYOK mode COPILOT_GITHUB_TOKEN is unset by the AWF entrypoint. - // The SDK server and client must use a custom provider pointing at the AWF - // API proxy instead of token-based Copilot auth. The AWF harness fetches - // the /reflect payload before launching the driver, so the file is available - // at the well-known path when this function runs. - const customProvider = resolveCopilotSDKCustomProviderFromReflect({ - model, - logger: log, - }); - const provider = customProvider ? customProvider.provider : undefined; - // When the reflect data provides a model, prefer it over the env-var model so - // the provider and model stay consistent (e.g. BYOK endpoint may enforce a - // specific model ID). - const resolvedModel = customProvider ? customProvider.model : model; + // --- Resolve BYOK custom provider from environment ------------------ + // The harness resolves the BYOK provider from live AWF reflect data before launching + // this driver and injects the result as GH_AW_COPILOT_SDK_PROVIDER_BASE_URL. + // BYOK is the only supported mode — fail immediately if the env var is missing. + const providerBaseUrl = process.env.GH_AW_COPILOT_SDK_PROVIDER_BASE_URL; + if (!providerBaseUrl) { + process.stderr.write( + "[copilot-sdk-driver] error: GH_AW_COPILOT_SDK_PROVIDER_BASE_URL is not set — " + + "BYOK provider is required; ensure the harness resolved a custom provider from awf-reflect data\n" + ); + process.exit(1); + } + const provider = /** @type {import("@github/copilot-sdk").ProviderConfig} */ ({ type: "openai", baseUrl: providerBaseUrl }); // --- Run SDK session ------------------------------------------------- @@ -515,7 +512,7 @@ async function main() { sdkUri, prompt, logger: log, - model: resolvedModel, + model, connectionToken, provider, }); From 8fe756d9497e8fa8a6ebd2fd066e579a4a72c1df Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Jun 2026 00:54:13 +0000 Subject: [PATCH 4/6] fix: address code review - pass resolved model to driver, clarify null check - resolveCopilotSDKCustomProviderFromReflect: use != null instead of 'in' operator so null/undefined reflectData falls back to file reading - copilot_harness.cjs: capture resolved model from custom provider and inject it as COPILOT_MODEL into driver subprocess env for consistency - tests: update reflectData:null test to reflect fallback-to-file behavior Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/awf_reflect.cjs | 2 +- actions/setup/js/awf_reflect.test.cjs | 11 ++++++++--- actions/setup/js/copilot_harness.cjs | 15 +++++++++++---- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/actions/setup/js/awf_reflect.cjs b/actions/setup/js/awf_reflect.cjs index b822c64034a..68cef6bde81 100644 --- a/actions/setup/js/awf_reflect.cjs +++ b/actions/setup/js/awf_reflect.cjs @@ -325,7 +325,7 @@ function resolveCopilotSDKCustomProviderFromReflect(options) { const logger = (options && options.logger) || DEFAULT_REFLECT_LOGGER; let reflectData; - if (options && "reflectData" in options) { + if (options != null && options.reflectData != null) { // Live data passed directly by the harness — use it without touching the file system. reflectData = options.reflectData; } else { diff --git a/actions/setup/js/awf_reflect.test.cjs b/actions/setup/js/awf_reflect.test.cjs index bf19bede072..b59cdcb0239 100644 --- a/actions/setup/js/awf_reflect.test.cjs +++ b/actions/setup/js/awf_reflect.test.cjs @@ -411,11 +411,16 @@ describe("awf_reflect.cjs", () => { expect(logs.some(l => l.includes("no configured endpoints"))).toBe(true); }); - it("returns null when reflectData is null", () => { + it("falls back to file reading when reflectData is null", () => { const logs = []; - const result = resolveCopilotSDKCustomProviderFromReflect({ reflectData: null, logger: msg => logs.push(msg) }); + // null reflectData triggers file fallback; the default path doesn't exist in test env + const result = resolveCopilotSDKCustomProviderFromReflect({ + reflectData: null, + reflectPath: "/tmp/gh-aw-nonexistent-reflect-null.json", + logger: msg => logs.push(msg), + }); expect(result).toBeNull(); - expect(logs.some(l => l.includes("no configured endpoints"))).toBe(true); + expect(logs.some(l => l.includes("unable to read custom provider config"))).toBe(true); }); }); }); diff --git a/actions/setup/js/copilot_harness.cjs b/actions/setup/js/copilot_harness.cjs index 79e6a867438..ee9f98671d5 100644 --- a/actions/setup/js/copilot_harness.cjs +++ b/actions/setup/js/copilot_harness.cjs @@ -564,6 +564,7 @@ async function main() { // BYOK is the only supported mode for SDK sessions — fail immediately if the provider // cannot be resolved so retries are not wasted on a misconfigured environment. let sdkProviderBaseUrl = ""; + let sdkResolvedModel = ""; if (copilotSDKMode) { const configuredModel = process.env.COPILOT_MODEL || ""; const customProvider = resolveCopilotSDKCustomProviderFromReflect({ model: configuredModel, reflectData: awfReflectData, logger: log }); @@ -572,7 +573,8 @@ async function main() { process.exit(1); } sdkProviderBaseUrl = customProvider.provider.baseUrl; - log(`copilot-sdk driver mode: BYOK provider resolved (baseUrl=${sdkProviderBaseUrl})`); + sdkResolvedModel = customProvider.model; + log(`copilot-sdk driver mode: BYOK provider resolved (baseUrl=${sdkProviderBaseUrl} model=${sdkResolvedModel})`); } // Merge SDK env additions into the child process env only when the SDK helper @@ -581,10 +583,15 @@ async function main() { // sdkEnv already contains SDK-mode variables (e.g. COPILOT_SDK_URI) when enabled. // Always attach the generated per-run COPILOT_CONNECTION_TOKEN so both the sidecar // (started by the harness) and the SDK client share the same token. - // In SDK mode also inject the resolved BYOK provider base URL so the driver subprocess - // does not need to re-read the reflect file. + // In SDK mode also inject the resolved BYOK provider base URL and model so the driver + // subprocess does not need to re-read the reflect file. const sdkChildEnv = copilotSDKMode - ? { ...sdkEnv, COPILOT_CONNECTION_TOKEN: copilotConnectionToken, GH_AW_COPILOT_SDK_PROVIDER_BASE_URL: sdkProviderBaseUrl } + ? { + ...sdkEnv, + COPILOT_CONNECTION_TOKEN: copilotConnectionToken, + GH_AW_COPILOT_SDK_PROVIDER_BASE_URL: sdkProviderBaseUrl, + COPILOT_MODEL: sdkResolvedModel, + } : sdkEnv; const childEnv = Object.keys(sdkChildEnv).length > 0 ? { ...process.env, ...sdkChildEnv } : undefined; From 85504d65f255a5541cf1126a333440a5d02aed8e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Jun 2026 00:55:21 +0000 Subject: [PATCH 5/6] fix: rename sdkProviderBaseUrl/sdkResolvedModel to providerBaseUrl/resolvedModel Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/copilot_harness.cjs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/actions/setup/js/copilot_harness.cjs b/actions/setup/js/copilot_harness.cjs index ee9f98671d5..8e53179e836 100644 --- a/actions/setup/js/copilot_harness.cjs +++ b/actions/setup/js/copilot_harness.cjs @@ -563,8 +563,8 @@ async function main() { // Resolve BYOK custom provider from live reflect data (SDK mode only). // BYOK is the only supported mode for SDK sessions — fail immediately if the provider // cannot be resolved so retries are not wasted on a misconfigured environment. - let sdkProviderBaseUrl = ""; - let sdkResolvedModel = ""; + let providerBaseUrl = ""; + let resolvedModel = ""; if (copilotSDKMode) { const configuredModel = process.env.COPILOT_MODEL || ""; const customProvider = resolveCopilotSDKCustomProviderFromReflect({ model: configuredModel, reflectData: awfReflectData, logger: log }); @@ -572,9 +572,9 @@ async function main() { log("copilot-sdk driver mode: BYOK provider is required but could not be resolved from awf-reflect data — aborting"); process.exit(1); } - sdkProviderBaseUrl = customProvider.provider.baseUrl; - sdkResolvedModel = customProvider.model; - log(`copilot-sdk driver mode: BYOK provider resolved (baseUrl=${sdkProviderBaseUrl} model=${sdkResolvedModel})`); + providerBaseUrl = customProvider.provider.baseUrl; + resolvedModel = customProvider.model; + log(`copilot-sdk driver mode: BYOK provider resolved (baseUrl=${providerBaseUrl} model=${resolvedModel})`); } // Merge SDK env additions into the child process env only when the SDK helper @@ -589,8 +589,8 @@ async function main() { ? { ...sdkEnv, COPILOT_CONNECTION_TOKEN: copilotConnectionToken, - GH_AW_COPILOT_SDK_PROVIDER_BASE_URL: sdkProviderBaseUrl, - COPILOT_MODEL: sdkResolvedModel, + GH_AW_COPILOT_SDK_PROVIDER_BASE_URL: providerBaseUrl, + COPILOT_MODEL: resolvedModel, } : sdkEnv; const childEnv = Object.keys(sdkChildEnv).length > 0 ? { ...process.env, ...sdkChildEnv } : undefined; From 477bb90c1d4cc23026755a263e995d1be1cfc996 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Jun 2026 01:27:22 +0000 Subject: [PATCH 6/6] fix: remove file fallback from resolveCopilotSDKCustomProviderFromReflect Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com> --- actions/setup/js/awf_reflect.cjs | 27 ++---- actions/setup/js/awf_reflect.test.cjs | 115 +++++++------------------- 2 files changed, 38 insertions(+), 104 deletions(-) diff --git a/actions/setup/js/awf_reflect.cjs b/actions/setup/js/awf_reflect.cjs index 68cef6bde81..b3a393b4389 100644 --- a/actions/setup/js/awf_reflect.cjs +++ b/actions/setup/js/awf_reflect.cjs @@ -308,14 +308,11 @@ async function fetchAWFReflect(options) { * Returns null when no suitable endpoint is found (e.g. no reflect data, or endpoints not * configured). * - * Accepts live reflect data directly via `reflectData` (preferred — avoids re-reading the - * persisted file). Falls back to reading from `reflectPath` when `reflectData` is not provided. + * Requires live reflect data passed directly via `reflectData`. * * @param {{ * model?: string, - * reflectData?: object | null, - * reflectPath?: string, - * readFileSync?: (path: string, encoding: BufferEncoding) => string, + * reflectData: object | null | undefined, * logger?: (msg: string) => void, * }} [options] * @returns {{ model: string, provider: { type: "openai", baseUrl: string } } | null} @@ -324,22 +321,10 @@ function resolveCopilotSDKCustomProviderFromReflect(options) { const configuredModel = typeof options?.model === "string" ? options.model.trim() : ""; const logger = (options && options.logger) || DEFAULT_REFLECT_LOGGER; - let reflectData; - if (options != null && options.reflectData != null) { - // Live data passed directly by the harness — use it without touching the file system. - reflectData = options.reflectData; - } else { - // Fallback: read from the persisted reflect file. - const reflectPath = (options && options.reflectPath) || AWF_REFLECT_OUTPUT_PATH; - const readFile = (options && options.readFileSync) || fs.readFileSync; - try { - const raw = readFile(reflectPath, "utf8"); - reflectData = JSON.parse(raw); - } catch (error) { - const err = /** @type {Error} */ (error); - logger(`sdk-mode: unable to read custom provider config from ${reflectPath}: ${err.message}`); - return null; - } + const reflectData = options?.reflectData; + if (reflectData == null) { + logger("sdk-mode: no reflect data provided; cannot resolve custom provider"); + return null; } const endpoints = Array.isArray(reflectData?.endpoints) ? reflectData.endpoints.filter(ep => ep && ep.configured === true) : []; diff --git a/actions/setup/js/awf_reflect.test.cjs b/actions/setup/js/awf_reflect.test.cjs index b59cdcb0239..a37d0d1ff49 100644 --- a/actions/setup/js/awf_reflect.test.cjs +++ b/actions/setup/js/awf_reflect.test.cjs @@ -310,117 +310,66 @@ describe("awf_reflect.cjs", () => { describe("resolveCopilotSDKCustomProviderFromReflect", () => { it("resolves provider baseUrl and model from port when models_url is absent", () => { - const reflectFile = path.join(os.tmpdir(), `awf-reflect-sdk-provider-${Date.now()}.json`); - try { - fs.writeFileSync( - reflectFile, - JSON.stringify({ - endpoints: [{ provider: "copilot", port: 10002, configured: true, models: ["gpt-5.4", "claude-sonnet-4.6"] }], - }), - "utf8" - ); - expect(resolveCopilotSDKCustomProviderFromReflect({ reflectPath: reflectFile })).toEqual({ - model: "gpt-5.4", - provider: { type: "openai", baseUrl: "http://api-proxy:10002" }, - }); - } finally { - fs.unlinkSync(reflectFile); - } + const reflectData = { + endpoints: [{ provider: "copilot", port: 10002, configured: true, models: ["gpt-5.4", "claude-sonnet-4.6"] }], + }; + expect(resolveCopilotSDKCustomProviderFromReflect({ reflectData })).toEqual({ + model: "gpt-5.4", + provider: { type: "openai", baseUrl: "http://api-proxy:10002" }, + }); }); it("prefers the endpoint matching the configured model", () => { - const reflectFile = path.join(os.tmpdir(), `awf-reflect-sdk-model-${Date.now()}.json`); - try { - fs.writeFileSync( - reflectFile, - JSON.stringify({ - endpoints: [ - { provider: "openai", port: 10001, configured: true, models: ["gpt-4o"] }, - { provider: "anthropic", port: 10002, configured: true, models: ["claude-sonnet-4.6"] }, - ], - }), - "utf8" - ); - expect(resolveCopilotSDKCustomProviderFromReflect({ reflectPath: reflectFile, model: "claude-sonnet-4.6" })).toEqual({ - model: "claude-sonnet-4.6", - provider: { type: "openai", baseUrl: "http://api-proxy:10002" }, - }); - } finally { - fs.unlinkSync(reflectFile); - } + const reflectData = { + endpoints: [ + { provider: "openai", port: 10001, configured: true, models: ["gpt-4o"] }, + { provider: "anthropic", port: 10002, configured: true, models: ["claude-sonnet-4.6"] }, + ], + }; + expect(resolveCopilotSDKCustomProviderFromReflect({ reflectData, model: "claude-sonnet-4.6" })).toEqual({ + model: "claude-sonnet-4.6", + provider: { type: "openai", baseUrl: "http://api-proxy:10002" }, + }); }); it("derives baseUrl from models_url origin when available", () => { - const reflectFile = path.join(os.tmpdir(), `awf-reflect-sdk-url-${Date.now()}.json`); - try { - fs.writeFileSync( - reflectFile, - JSON.stringify({ - endpoints: [{ provider: "copilot", port: 10002, configured: true, models: ["gpt-4o"], models_url: "http://172.30.0.30:10002/v1/models" }], - }), - "utf8" - ); - expect(resolveCopilotSDKCustomProviderFromReflect({ reflectPath: reflectFile })).toEqual({ - model: "gpt-4o", - provider: { type: "openai", baseUrl: "http://172.30.0.30:10002" }, - }); - } finally { - fs.unlinkSync(reflectFile); - } + const reflectData = { + endpoints: [{ provider: "copilot", port: 10002, configured: true, models: ["gpt-4o"], models_url: "http://172.30.0.30:10002/v1/models" }], + }; + expect(resolveCopilotSDKCustomProviderFromReflect({ reflectData })).toEqual({ + model: "gpt-4o", + provider: { type: "openai", baseUrl: "http://172.30.0.30:10002" }, + }); }); it("returns null when no configured endpoints exist", () => { - const reflectFile = path.join(os.tmpdir(), `awf-reflect-sdk-empty-${Date.now()}.json`); - try { - fs.writeFileSync(reflectFile, JSON.stringify({ endpoints: [{ provider: "copilot", port: 10002, configured: false, models: [] }] }), "utf8"); - const logs = []; - expect(resolveCopilotSDKCustomProviderFromReflect({ reflectPath: reflectFile, logger: msg => logs.push(msg) })).toBeNull(); - expect(logs.some(l => l.includes("no configured endpoints"))).toBe(true); - } finally { - fs.unlinkSync(reflectFile); - } - }); - - it("returns null when the reflect file is missing", () => { const logs = []; const result = resolveCopilotSDKCustomProviderFromReflect({ - reflectPath: "/tmp/gh-aw-nonexistent-reflect.json", + reflectData: { endpoints: [{ provider: "copilot", port: 10002, configured: false, models: [] }] }, logger: msg => logs.push(msg), }); expect(result).toBeNull(); - expect(logs.some(l => l.includes("unable to read custom provider config"))).toBe(true); - }); - - it("resolves provider from live reflectData without touching the file system", () => { - const reflectData = { - endpoints: [{ provider: "copilot", port: 10003, configured: true, models: ["gpt-5.4"] }], - }; - expect(resolveCopilotSDKCustomProviderFromReflect({ reflectData })).toEqual({ - model: "gpt-5.4", - provider: { type: "openai", baseUrl: "http://api-proxy:10003" }, - }); + expect(logs.some(l => l.includes("no configured endpoints"))).toBe(true); }); - it("returns null when reflectData has no configured endpoints", () => { + it("returns null when reflectData is null", () => { const logs = []; const result = resolveCopilotSDKCustomProviderFromReflect({ - reflectData: { endpoints: [{ provider: "copilot", port: 10002, configured: false, models: ["gpt-4o"] }] }, + reflectData: null, logger: msg => logs.push(msg), }); expect(result).toBeNull(); - expect(logs.some(l => l.includes("no configured endpoints"))).toBe(true); + expect(logs.some(l => l.includes("no reflect data provided"))).toBe(true); }); - it("falls back to file reading when reflectData is null", () => { + it("returns null when reflectData is undefined", () => { const logs = []; - // null reflectData triggers file fallback; the default path doesn't exist in test env const result = resolveCopilotSDKCustomProviderFromReflect({ - reflectData: null, - reflectPath: "/tmp/gh-aw-nonexistent-reflect-null.json", + reflectData: undefined, logger: msg => logs.push(msg), }); expect(result).toBeNull(); - expect(logs.some(l => l.includes("unable to read custom provider config"))).toBe(true); + expect(logs.some(l => l.includes("no reflect data provided"))).toBe(true); }); }); });