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
4 changes: 2 additions & 2 deletions src/core/codex-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<CodexSecurity, 'run' | 'close'>;
export type SecurityFactory = (
config: CodexSecurityConfig,
) => SecurityClient | Promise<SecurityClient>;
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(),
Expand Down
5 changes: 3 additions & 2 deletions src/core/scan-plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<ScanInputs, 'scopePaths' | 'changes'>) {
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 });
Expand Down Expand Up @@ -141,7 +142,7 @@ export async function withSdk<T>(
codexOverrides: JsonObject,
use: (client: CodexSecurity, sdk: typeof import('@openai/codex-security')) => Promise<T>,
): Promise<T> {
const sdk = await import('@openai/codex-security');
const sdk = await loadSdk();
const client = new sdk.CodexSecurity({ codexOverrides });
try {
return await use(client, sdk);
Expand Down
28 changes: 28 additions & 0 deletions src/core/sdk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';

let ready: Promise<typeof import('@openai/codex-security')> | 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;
})());
}
5 changes: 3 additions & 2 deletions src/core/security-operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions src/core/sign-in.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
Expand Down
17 changes: 17 additions & 0 deletions tests/sdk.test.mjs
Original file line number Diff line number Diff line change
@@ -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;
}
});
Loading