Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 1 addition & 25 deletions .github/drivers/copilot_sdk_driver_sample_node.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"use strict";

const fs = require("node:fs");
const { isValidProviderConfig, isValidModelConfig, parseMultiProviderJson } = require("../../actions/setup/js/copilot_sdk_multi_provider.cjs");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] The relative path ../../actions/setup/js/copilot_sdk_multi_provider.cjs is fragile — if either file moves, the link silently breaks at runtime with no compile-time signal.

💡 Suggestion

Consider documenting why the relative path is used (e.g. in a comment) so future movers know this dependency exists. Alternatively, if the repo ever adopts a workspace/package structure, a named package import would be more robust. At minimum, a comment like // shared with actions/setup/js — do not move without updating this path keeps the coupling visible.

@copilot please address this.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The require() path ../../actions/setup/js/copilot_sdk_multi_provider.cjs is a brittle relative path from a .github/drivers/ file into actions/setup/js/. This will silently break if either directory is moved or if the sample driver is copied elsewhere.\n\nThe production driver in actions/setup/js/ uses a safe local ./copilot_sdk_multi_provider.cjs reference. Consider:\n1. Embedding the sample driver's helpers via a thin wrapper re-export in the production driver (already done for parseMultiProviderJson), and importing from the production driver instead; or\n2. Adding a path-integrity test that verifies the relative path resolves correctly.\n\nAt minimum, add a comment explaining the expected repo layout so a future directory refactor doesn't silently break this.\n\n@copilot please address this.


// Default timeout for a single sendAndWait call: 10 minutes.
// Override via the COPILOT_SDK_SEND_TIMEOUT_MS environment variable.
Expand Down Expand Up @@ -43,31 +44,6 @@ function extractAssistantContent(message) {
return "";
}

function isValidProviderConfig(p) {
return p && typeof p.name === "string" && typeof p.type === "string" && typeof p.baseUrl === "string";
}

function isValidModelConfig(m) {
return m && typeof m.id === "string" && typeof m.provider === "string";
}

function parseMultiProviderJson(raw) {
if (!raw) return null;
try {
const parsed = JSON.parse(raw);
if (!parsed || typeof parsed !== "object") return null;
if (!Array.isArray(parsed.providers) || parsed.providers.length < 1) return null;
if (!Array.isArray(parsed.models) || parsed.models.length < 1) return null;
// Validate minimal shape: providers must have name/type/baseUrl, models must have id/provider
if (!parsed.providers.every(isValidProviderConfig)) return null;
if (!parsed.models.every(isValidModelConfig)) return null;
const model = typeof parsed.model === "string" ? parsed.model.trim() : "";
return { model, providers: parsed.providers, models: parsed.models };
} catch {
return null;
}
}

function buildSessionConfig(model, onPermissionRequest) {
const config = {
onPermissionRequest,
Expand Down
45 changes: 4 additions & 41 deletions actions/setup/js/copilot_sdk_driver.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,17 @@
* client connection, runs the session, and exits.
*
* Reusable helpers live in:
* copilot_sdk_permissions.cjs — permission config parsing and handler builder
* copilot_sdk_session.cjs — session runner and JSONL event serialization
* copilot_sdk_permissions.cjs — permission config parsing and handler builder
* copilot_sdk_session.cjs — session runner and JSONL event serialization
* copilot_sdk_multi_provider.cjs — multi-provider JSON parsing and validation
*/

"use strict";

const fs = require("fs");
const { runWithCopilotSDK, extractPromptFromArgs } = require("./copilot_sdk_session.cjs");
const { parsePermissionConfigFromServerArgs } = require("./copilot_sdk_permissions.cjs");
const { parseMultiProviderJson } = require("./copilot_sdk_multi_provider.cjs");

// Re-export the session and permission helpers so that existing callers that
// require("./copilot_sdk_driver.cjs") (e.g. copilot_harness.cjs) continue to work.
Expand All @@ -45,45 +47,6 @@ function log(msg) {
process.stderr.write(`[copilot-sdk-driver] ${msg}\n`);
}

function isValidProviderConfig(p) {
return p && typeof p.name === "string" && typeof p.type === "string" && typeof p.baseUrl === "string";
}

function isValidModelConfig(m) {
return m && typeof m.id === "string" && typeof m.provider === "string";
}

/**
* Parse the GH_AW_COPILOT_SDK_MULTI_PROVIDER_JSON env var.
*
* Returns `null` when the env var is unset or contains invalid JSON.
* On success returns `{ model, providers, models }` where the shapes match the
* Copilot SDK `NamedProviderConfig` / `ProviderModelConfig` types.
*
* @param {string | undefined} value
* @returns {{
* model: string,
* providers: import("@github/copilot-sdk").NamedProviderConfig[],
* models: import("@github/copilot-sdk").ProviderModelConfig[],
* } | null}
*/
function parseMultiProviderJson(value) {
if (!value) return null;
try {
const parsed = JSON.parse(value);
if (!parsed || typeof parsed !== "object") return null;
if (!Array.isArray(parsed.providers) || parsed.providers.length < 1) return null;
if (!Array.isArray(parsed.models) || parsed.models.length < 1) return null;
// Validate minimal shape: providers must have name/type/baseUrl, models must have id/provider
if (!parsed.providers.every(isValidProviderConfig)) return null;
if (!parsed.models.every(isValidModelConfig)) return null;
const model = typeof parsed.model === "string" ? parsed.model.trim() : "";
return { model, providers: parsed.providers, models: parsed.models };
} catch {
return null;
}
}

/**
* Entry point when the driver is run directly with Node:
* node copilot_sdk_driver.cjs
Expand Down
65 changes: 65 additions & 0 deletions actions/setup/js/copilot_sdk_multi_provider.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// @ts-check

/**
* Copilot SDK Multi-Provider Helpers
*
* Shared parsing and validation utilities for the GH_AW_COPILOT_SDK_MULTI_PROVIDER_JSON
* environment variable. Consumed by both the production SDK driver
* (copilot_sdk_driver.cjs) and any custom driver (e.g. the Node sample driver
* under .github/drivers/) so that the validation logic stays in one place.
*/

"use strict";

/**
* @param {any} p
* @returns {boolean}
*/
function isValidProviderConfig(p) {
return p && typeof p.name === "string" && typeof p.type === "string" && typeof p.baseUrl === "string";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty strings pass validation in isValidProviderConfig and isValidModelConfig: a provider with { name: "", type: "", baseUrl: "" } passes the shape check and reaches the Copilot SDK, causing opaque connection or URL errors far from the validation site.

💡 Suggested fix
function isValidProviderConfig(p) {
  return (
    p &&
    typeof p.name === "string" && p.name.length > 0 &&
    typeof p.type === "string" && p.type.length > 0 &&
    typeof p.baseUrl === "string" && p.baseUrl.length > 0
  );
}

function isValidModelConfig(m) {
  return (
    m &&
    typeof m.id === "string" && m.id.length > 0 &&
    typeof m.provider === "string" && m.provider.length > 0
  );
}

Non-empty checks are cheap and surface misconfigured GH_AW_COPILOT_SDK_MULTI_PROVIDER_JSON at the validation boundary rather than as a cryptic SDK runtime failure.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The @returns {boolean} JSDoc annotation is inaccurate: when p is falsy, p && ... short-circuits and returns p itself (e.g. null, undefined) rather than false.\n\nWith // @ts-check`` enabled, TypeScript will flag callers that depend on the strict boolean type. Though `.every()` coerces truthy/falsy correctly today, the imprecise annotation is a latent maintenance risk.\n\nConsider:\njs\nfunction isValidProviderConfig(p) {\n return !!p && typeof p.name === 'string' && typeof p.type === 'string' && typeof p.baseUrl === 'string';\n}\n\n\nSame applies to `isValidModelConfig`.\n\n@copilot please address this.

}

/**
* @param {any} m
* @returns {boolean}
*/
function isValidModelConfig(m) {
return m && typeof m.id === "string" && typeof m.provider === "string";
}

/**
* Parse the GH_AW_COPILOT_SDK_MULTI_PROVIDER_JSON env var.
*
* Returns `null` when the env var is unset or contains invalid JSON.
* On success returns `{ model, providers, models }` where the shapes match the
* Copilot SDK `NamedProviderConfig` / `ProviderModelConfig` types.
Comment on lines +33 to +35
*
* @param {string | undefined} value
* @returns {{
* model: string,
* providers: import("@github/copilot-sdk").NamedProviderConfig[],
* models: import("@github/copilot-sdk").ProviderModelConfig[],
* } | null}
*/
function parseMultiProviderJson(value) {
if (!value) return null;
try {
const parsed = JSON.parse(value);
if (!parsed || typeof parsed !== "object") return null;
Comment on lines +44 to +48
if (!Array.isArray(parsed.providers) || parsed.providers.length < 1) return null;
if (!Array.isArray(parsed.models) || parsed.models.length < 1) return null;
// Validate minimal shape: providers must have name/type/baseUrl, models must have id/provider
if (!parsed.providers.every(isValidProviderConfig)) return null;
if (!parsed.models.every(isValidModelConfig)) return null;
const model = typeof parsed.model === "string" ? parsed.model.trim() : "";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

model: "" when the field is absent creates an implicit, fragile contract: the function returns model: "" when parsed.model is missing or not a string. The only documented return type is { model: string, ... } with no indication that "" means 'not set'. The production caller handles this via process.env.COPILOT_MODEL || multiProviderConfig.model || undefined — correctly, but only because "" is falsy. A future caller that checks model !== undefined or model !== null will silently use the wrong value.

💡 Suggested fix
// Return null (or omit the field) when model is not set
const model = typeof parsed.model === "string" && parsed.model.trim() ? parsed.model.trim() : null;
return { model, providers: parsed.providers, models: parsed.models };

And update the JSDoc return type:

 * `@returns` {{ model: string | null, providers: ..., models: ... } | null}

This makes the absence of a model field explicit rather than encoding it as an empty string.

return { model, providers: parsed.providers, models: parsed.models };
} catch {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silent catch destroys all diagnostic information: every error — malformed JSON, unexpected TypeError, future bugs in validators — collapses to null with no log output. This is now the single source of truth for multi-provider parsing; when it silently returns null, the caller emits only a generic 'invalid' error message and the root cause is invisible.

💡 Suggested fix
} catch (err) {
  // Log to stderr so operator errors (malformed JSON, wrong shape) are diagnosable
  process.stderr.write(
    `[copilot-sdk-multi-provider] failed to parse GH_AW_COPILOT_SDK_MULTI_PROVIDER_JSON: ${err}\n`
  );
  return null;
}

Alternatively, accept an optional logger callback so callers can control where the diagnostic goes — useful for test isolation.

return null;
}
}

module.exports = {
isValidProviderConfig,
isValidModelConfig,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] isValidProviderConfig and isValidModelConfig are implementation details of parseMultiProviderJson but are exported publicly. Exporting them widens the module's surface unnecessarily and invites callers to rely on the internal validation predicates directly.

💡 Suggestion

Keep the two helpers private (unexported) unless there is a concrete caller outside this module that needs them. If they genuinely need to be public, document their intended use case. The sample driver already only imports all three — but its module.exports re-exports them too, compounding the surface widening.

@copilot please address this.

parseMultiProviderJson,
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/tdd] No paired test file for the new shared module — this is the single source of truth for multi-provider validation, and it currently has zero test coverage.

💡 Suggested test file: copilot_sdk_multi_provider.test.cjs

Every other module in actions/setup/js/ has a paired *.test.cjs. A minimal suite should cover: null/empty/invalid-JSON inputs, missing or empty providers/models arrays, invalid provider shape (missing baseUrl), invalid model shape, and a valid round-trip that confirms the model string is trimmed.

@copilot please address this.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No test file exists for the new copilot_sdk_multi_provider.cjs module. The repository has 362 test files, and every other copilot_sdk_*.cjs sibling has coverage (e.g. copilot_sdk_driver.test.cjs). The extracted logic is the single source of truth for multi-provider config validation, so regressions here would silently affect both drivers.\n\nPlease add a copilot_sdk_multi_provider.test.cjs covering at least:\n- parseMultiProviderJson(undefined)null\n- Invalid JSON → null\n- Missing providers/modelsnull\n- Invalid provider shape → null\n- Valid input → correct { model, providers, models }\n\n@copilot please address this.

Loading