From dc17a708e2c6902ef797291e8c3289f6f4d01b9f Mon Sep 17 00:00:00 2001 From: strantalis <18211470+strantalis@users.noreply.github.com> Date: Fri, 13 Nov 2026 00:07:11 -0500 Subject: [PATCH] fix(scan): load the SDK helper safely inside OpenCode's compiled binary The SDK's bundled helper compares process.argv[1] with its own path when it loads. In OpenCode's binary that argument is a virtual path that exists but cannot be resolved, so the load threw and every scan that reached the end failed with "completed without required artifacts". Load the helper once with the argument blanked, through one shared SDK loader. Co-Authored-By: Claude Fable 5.2 Claude-Session: https://claude.ai/code/session_016LiHJtDpo3TEPnWZ78vM2X --- src/core/codex-adapter.ts | 4 ++-- src/core/scan-plan.ts | 5 +++-- src/core/sdk.ts | 28 ++++++++++++++++++++++++++++ src/core/security-operations.ts | 5 +++-- src/core/sign-in.ts | 4 ++-- tests/sdk.test.mjs | 17 +++++++++++++++++ 6 files changed, 55 insertions(+), 8 deletions(-) create mode 100644 src/core/sdk.ts create mode 100644 tests/sdk.test.mjs diff --git a/src/core/codex-adapter.ts b/src/core/codex-adapter.ts index f498996..29e3a23 100644 --- a/src/core/codex-adapter.ts +++ b/src/core/codex-adapter.ts @@ -24,13 +24,13 @@ import { scanTarget, type PreparedScanPlan } from './scan-plan.ts'; import { userMessage } from './errors.ts'; import { savedScan } from './partial-results.ts'; import type { Finding, ScanState } from '../types.js'; +import { loadSdk } from './sdk.ts'; export type SecurityClient = Pick; export type SecurityFactory = ( config: CodexSecurityConfig, ) => SecurityClient | Promise; -const realClient: SecurityFactory = async (config) => - new (await import('@openai/codex-security')).CodexSecurity(config); +const realClient: SecurityFactory = async (config) => new (await loadSdk()).CodexSecurity(config); function initial(repository: string, revision = 0, instanceId = ''): ScanState { return { telemetry: emptyTelemetry(), diff --git a/src/core/scan-plan.ts b/src/core/scan-plan.ts index 275506a..aefec1b 100644 --- a/src/core/scan-plan.ts +++ b/src/core/scan-plan.ts @@ -12,6 +12,7 @@ import type { import { codexHome, loadSettings } from './settings.ts'; import { SecurityInputError } from './errors.ts'; import { storedState } from './sign-in.ts'; +import { loadSdk } from './sdk.ts'; export interface ScanInputs { mode?: 'standard' | 'deep'; @@ -97,7 +98,7 @@ function parseInputs(value: unknown): ScanInputs { /** The SDK target for a plan: the whole repository, paths, or a diff. */ export async function scanTarget(input: Pick) { if (input.changes) { - const { DiffTarget } = await import('@openai/codex-security'); + const { DiffTarget } = await loadSdk(); return input.changes === 'uncommitted' ? DiffTarget.workingTree() : DiffTarget.refs({ base: input.changes }); @@ -141,7 +142,7 @@ export async function withSdk( codexOverrides: JsonObject, use: (client: CodexSecurity, sdk: typeof import('@openai/codex-security')) => Promise, ): Promise { - const sdk = await import('@openai/codex-security'); + const sdk = await loadSdk(); const client = new sdk.CodexSecurity({ codexOverrides }); try { return await use(client, sdk); diff --git a/src/core/sdk.ts b/src/core/sdk.ts new file mode 100644 index 0000000..b495f4f --- /dev/null +++ b/src/core/sdk.ts @@ -0,0 +1,28 @@ +import { join } from 'node:path'; +import { pathToFileURL } from 'node:url'; + +let ready: Promise | undefined; + +/** + * The SDK, with its bundled helper module already loaded. + * + * On load the helper compares `process.argv[1]` with its own path to see whether it is the main + * script. Inside OpenCode's compiled binary that argument is a virtual path that exists but + * cannot be resolved, so the load throws, and the SDK reports it as a scan that "completed + * without required artifacts". Loading the helper once with the argument blanked avoids that; + * later imports reuse the loaded module. + */ +export function loadSdk() { + return (ready ??= (async () => { + const sdk = await import('@openai/codex-security'); + const helper = pathToFileURL(join(await sdk.bundledPluginRoot(), 'mcp/helpers.mjs')).href; + const invoked = process.argv[1]; + process.argv[1] = ''; + try { + await import(helper); + } finally { + process.argv[1] = invoked!; + } + return sdk; + })()); +} diff --git a/src/core/security-operations.ts b/src/core/security-operations.ts index 31da381..a6d84e6 100644 --- a/src/core/security-operations.ts +++ b/src/core/security-operations.ts @@ -23,6 +23,7 @@ import { type PatchWorkspace, type PatchWorkspaceFactory, } from './patch-workspace.ts'; +import { loadSdk } from './sdk.ts'; export type SecurityOperationKind = 'policy' | 'validation' | 'fix'; export type SecurityOperationStatus = @@ -131,7 +132,7 @@ type Entry = { }; }; const realClient: SecurityOperationsFactory = async (config) => - new (await import('@openai/codex-security')).CodexSecurity(config); + new (await loadSdk()).CodexSecurity(config); const active = (state: SecurityOperationState) => state.status === 'running' || state.status === 'awaiting_answers'; const textLimit = 48_000; @@ -346,7 +347,7 @@ export class SecurityOperationsService { // Computed in Node with no model call. The SDK refuses if SECURITY.md changed since the // draft read it; the draft has been paid for, so return it without a diff instead. const diff = replacesExisting - ? await import('@openai/codex-security') + ? await loadSdk() .then((sdk) => sdk.securityPolicyDiff(result, undefined, signal)) .catch(() => undefined) : undefined; diff --git a/src/core/sign-in.ts b/src/core/sign-in.ts index 558c0d0..dcc12cc 100644 --- a/src/core/sign-in.ts +++ b/src/core/sign-in.ts @@ -3,6 +3,7 @@ import { join } from 'node:path'; import type { CodexSecurity, CodexSecurityConfig } from '@openai/codex-security'; import { userMessage } from './errors.ts'; import { codexHome, loadSettings, stateDirectory } from './settings.ts'; +import { loadSdk } from './sdk.ts'; /** * Whether Codex Security has a ChatGPT sign-in of its own. Only relevant when the route uses @@ -41,8 +42,7 @@ export class SignIn { #pending?: { url: string; code?: string; cancel(): void }; #error?: string; constructor( - createClient: LoginFactory = async (config) => - new (await import('@openai/codex-security')).CodexSecurity(config), + createClient: LoginFactory = async (config) => new (await loadSdk()).CodexSecurity(config), ) { this.#factory = createClient; } diff --git a/tests/sdk.test.mjs b/tests/sdk.test.mjs new file mode 100644 index 0000000..4c2fcc6 --- /dev/null +++ b/tests/sdk.test.mjs @@ -0,0 +1,17 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { loadSdk } from '../src/core/sdk.ts'; + +test('the SDK helper loads even when the host reports an unresolvable main script', async () => { + const invoked = process.argv[1]; + // What OpenCode's compiled binary reports: a path that cannot be resolved. + process.argv[1] = '/$bunfs/root/opencode'; + try { + const sdk = await loadSdk(); + assert.equal(typeof sdk.CodexSecurity, 'function'); + assert.equal(process.argv[1], '/$bunfs/root/opencode'); + assert.equal(await loadSdk(), sdk); + } finally { + process.argv[1] = invoked; + } +});