diff --git a/extensions/ql-vscode/src/config.ts b/extensions/ql-vscode/src/config.ts index 1d262c15634..aabd1061b92 100644 --- a/extensions/ql-vscode/src/config.ts +++ b/extensions/ql-vscode/src/config.ts @@ -961,3 +961,9 @@ export const AUTOFIX_MODEL = new Setting("model", AUTOFIX_SETTING); export function getAutofixModel(): string | undefined { return AUTOFIX_MODEL.getValue() || undefined; } + +export const AUTOFIX_CAPI_DEV_KEY = new Setting("capiDevKey", AUTOFIX_SETTING); + +export function getAutofixCapiDevKey(): string | undefined { + return AUTOFIX_CAPI_DEV_KEY.getValue() || undefined; +} diff --git a/extensions/ql-vscode/src/variant-analysis/view-autofixes.ts b/extensions/ql-vscode/src/variant-analysis/view-autofixes.ts index 0d0214b07cf..87324baf4ca 100644 --- a/extensions/ql-vscode/src/variant-analysis/view-autofixes.ts +++ b/extensions/ql-vscode/src/variant-analysis/view-autofixes.ts @@ -38,9 +38,11 @@ import type { VariantAnalysisResultsManager } from "./variant-analysis-results-m import { getAutofixPath, getAutofixModel, + getAutofixCapiDevKey, downloadTimeout, AUTOFIX_PATH, AUTOFIX_MODEL, + AUTOFIX_CAPI_DEV_KEY, } from "../config"; import { asError, getErrorMessage } from "../common/helpers-pure"; import { createTimeoutSignal } from "../common/fetch-stream"; @@ -155,6 +157,39 @@ async function findLocalAutofix(): Promise { return localAutofixPath; } +/** + * Finds and resolves the Copilot API dev key from the `codeQL.autofix.capiDevKey` setting. + * The key can be specified as an environment variable reference (e.g., `env:MY_ENV_VAR`) + * or a 1Password secret reference (e.g., `op://vault/item/field`). By default, it uses + * the environment variable `CAPI_DEV_KEY`. + * + * @returns The resolved Copilot API dev key. + * @throws Error if the Copilot API dev key is not found or invalid. + */ +async function findCapiDevKey(): Promise { + let capiDevKey = getAutofixCapiDevKey() || "env:CAPI_DEV_KEY"; + + if (!capiDevKey.startsWith("env:") && !capiDevKey.startsWith("op://")) { + // Don't allow literal keys in config.json for security reasons + throw new Error( + `Invalid CAPI dev key format. Use 'env:' or 'op://<1PASSWORD_SECRET_REFERENCE>'.`, + ); + } + if (capiDevKey.startsWith("env:")) { + const envVarName = capiDevKey.substring("env:".length); + capiDevKey = process.env[envVarName] || ""; + } + if (capiDevKey.startsWith("op://")) { + capiDevKey = await opRead(capiDevKey); + } + if (!capiDevKey) { + throw new Error( + `Copilot API dev key not found. Make sure ${AUTOFIX_CAPI_DEV_KEY.qualifiedName} is set correctly.`, + ); + } + return capiDevKey; +} + /** * Overrides the query help from a given variant analysis * at a location within the `localAutofixPath` directory . @@ -214,7 +249,9 @@ async function overrideQueryHelp( // Note: the path to this directory may change in the future. const queryHelpOverrideDirectory = join( localAutofixPath, - "prompt-templates", + "pkg", + "autofix", + "prompt", "qhelps", `${queryIdWithDash}.md`, ); @@ -607,9 +644,9 @@ async function runAutofixForRepository( } = await getRepoStoragePaths(autofixOutputStoragePath, nwo); // Get autofix binary. - // Switch to Go binary in the future and have user pass full path + // In the future, have user pass full path // in an environment variable instead of hardcoding part here. - const cocofixBin = join(process.cwd(), localAutofixPath, "bin", "cocofix.js"); + const autofixBin = join(process.cwd(), localAutofixPath, "bin", "autofix"); // Limit number of fixes generated. const limitFixesBoolean: boolean = resultCount > MAX_NUM_FIXES; @@ -642,7 +679,7 @@ async function runAutofixForRepository( transcriptFiles.push(tempTranscriptFilePath); await runAutofixOnResults( - cocofixBin, + autofixBin, sarifFile, srcRootPath, tempOutputTextFilePath, @@ -661,7 +698,7 @@ async function runAutofixForRepository( } else { // Run autofix once for all alerts. await runAutofixOnResults( - cocofixBin, + autofixBin, sarifFile, srcRootPath, outputTextFilePath, @@ -707,7 +744,7 @@ async function getRepoStoragePaths( * Runs autofix on the results in the given SARIF file. */ async function runAutofixOnResults( - cocofixBin: string, + autofixBin: string, sarifFile: string, srcRootPath: string, outputTextFilePath: string, @@ -751,12 +788,12 @@ async function runAutofixOnResults( } await execAutofix( - cocofixBin, + autofixBin, autofixArgs, { cwd: workDir, env: { - CAPI_DEV_KEY: process.env.CAPI_DEV_KEY, + CAPI_DEV_KEY: await findCapiDevKey(), PATH: process.env.PATH, }, }, @@ -826,6 +863,42 @@ function execAutofix( }); } +/** Execute the 1Password CLI command `op read `, if the `op` command exists on the PATH. */ +async function opRead(secretReference: string): Promise { + return new Promise((resolve, reject) => { + const opProcess = spawn("op", ["read", secretReference], { + stdio: ["ignore", "pipe", "pipe"], + }); + + let stdoutBuffer = ""; + let stderrBuffer = ""; + + opProcess.stdout?.on("data", (data) => { + stdoutBuffer += data.toString(); + }); + + opProcess.stderr?.on("data", (data) => { + stderrBuffer += data.toString(); + }); + + opProcess.on("error", (error) => { + reject(error); + }); + + opProcess.on("exit", (code) => { + if (code === 0) { + resolve(stdoutBuffer.trim()); + } else { + reject( + new Error( + `1Password CLI exited with code ${code}. Stderr: ${stderrBuffer.trim()}`, + ), + ); + } + }); + }); +} + /** * Creates a new file path by appending the given suffix. * @param filePath The original file path.