Skip to content
Open
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
24 changes: 17 additions & 7 deletions sdk/typescript/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1723,17 +1723,27 @@ export class CodexSecurity {
reason: safeErrorMessage(failure),
}).catch(() => undefined);
}
const canceled =
signal.aborted &&
(options.signal?.aborted === true ||
this.#abortController.signal.aborted) &&
!(signal.reason instanceof ScanCostLimitExceededError) &&
!(failure instanceof ScanCostLimitExceededError);
Comment thread
Hughhhhcoder marked this conversation as resolved.
try {
await workbench({ ...activeScan.options, signal: undefined }, [
"fail-scan",
canceled ? "cancel-scan" : "fail-scan",
"--scan-id",
activeScan.id,
// Scan history can be shared; never persist credential-bearing failures.
"--message",
safeErrorMessage(failure).slice(0, 2400),
...(snapshot?.cost
? ["--cost-json", JSON.stringify(snapshot.cost)]
: []),
...(canceled
? []
: [
// Scan history can be shared; never persist credential-bearing failures.
"--message",
safeErrorMessage(failure).slice(0, 2400),
...(snapshot?.cost
? ["--cost-json", JSON.stringify(snapshot.cost)]
: []),
]),
]);
} catch {}
}
Expand Down
155 changes: 155 additions & 0 deletions sdk/typescript/tests-ts/api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3028,6 +3028,161 @@ describe("CodexSecurity orchestration", () => {
await client.close();
});

test("records a caller-canceled scan as canceled instead of failed", async () => {
const root = await temporaryDirectory();
const repository = join(root, "repository");
const codexHome = join(root, "codex-home");
const scanDir = join(root, "scan");
await mkdir(repository);
await mkdir(codexHome);
await mkdir(scanDir, { mode: 0o700 });
const commands: Array<readonly string[]> = [];
const started = Promise.withResolvers<void>();
const controller = new AbortController();

const client = new TestClient(
{},
{
environment: {},
prepareRuntime: async () => preparedRuntime(codexHome),
resolvePluginPython: async () => "/managed/python",
prepareOutputDir: async () => scanDir,
repositoryRevision: async () => "deadbeef",
runWorkbench: async (
_options: unknown,
args: readonly string[],
input?: string,
): Promise<JsonObject> => {
commands.push(args);
if (args[0] === "register-cli-scan") {
return mockScanRegistration(args, input);
}
if (args[0] === "get-scan-feedback") {
return {
scanId: "scan_example_001",
targetId: "target_sha256_example",
falsePositives: [],
};
}
return {};
},
createCodex: () => ({
startThread: () => ({
id: null,
async runStreamed(
_input: string,
options: { signal: AbortSignal },
) {
async function* events(): AsyncGenerator<ThreadEvent> {
yield { type: "thread.started", thread_id: "scan-thread" };
started.resolve();
await new Promise<void>((resolve) => {
if (options.signal.aborted) resolve();
else
options.signal.addEventListener("abort", () => resolve(), {
once: true,
});
});
throw new DOMException("aborted", "AbortError");
}
return { events: events() };
},
}),
}),
},
);

const pending = client.run(repository, { signal: controller.signal });
await started.promise;
controller.abort("caller canceled");
await expect(pending).rejects.toBeInstanceOf(ScanInterruptedError);
expect(commands.map(([command]) => command)).toEqual([
"register-cli-scan",
"get-scan-feedback",
"set-scan-thread",
"cancel-scan",
]);
expect(commands.at(-1)).toEqual([
"cancel-scan",
"--scan-id",
"scan_example_001",
]);
await client.close();
});

test("records a workbench AbortError as canceled instead of failed", async () => {
const root = await temporaryDirectory();
const repository = join(root, "repository");
const codexHome = join(root, "codex-home");
const scanDir = join(root, "scan");
await mkdir(repository);
await mkdir(codexHome);
await mkdir(scanDir, { mode: 0o700 });
const commands: Array<readonly string[]> = [];
const feedbackStarted = Promise.withResolvers<void>();
const controller = new AbortController();

const client = new TestClient(
{},
{
environment: {},
prepareRuntime: async () => preparedRuntime(codexHome),
resolvePluginPython: async () => "/managed/python",
prepareOutputDir: async () => scanDir,
repositoryRevision: async () => "deadbeef",
runWorkbench: async (
options: unknown,
args: readonly string[],
input?: string,
): Promise<JsonObject> => {
commands.push(args);
if (args[0] === "register-cli-scan") {
return mockScanRegistration(args, input);
}
if (args[0] === "get-scan-feedback") {
feedbackStarted.resolve();
const signal = (options as { signal: AbortSignal }).signal;
if (signal.aborted) {
throw new DOMException("aborted", "AbortError");
}
await new Promise<never>((_resolve, reject) => {
signal.addEventListener(
"abort",
() => reject(new DOMException("aborted", "AbortError")),
{ once: true },
);
});
}
return {};
},
createCodex: () => ({
startThread: () => ({
id: null,
async runStreamed() {
throw new Error("Codex must not start before feedback loads");
},
}),
}),
},
);

const pending = client.run(repository, { signal: controller.signal });
await feedbackStarted.promise;
controller.abort("caller canceled");
await expect(pending).rejects.toBeInstanceOf(ScanInterruptedError);
expect(commands.map(([command]) => command)).toEqual([
"register-cli-scan",
"get-scan-feedback",
"cancel-scan",
]);
expect(commands.at(-1)).toEqual([
"cancel-scan",
"--scan-id",
"scan_example_001",
]);
await client.close();
});

test("reports a Deep Scan terminal failure instead of a completion-state error", async () => {
const root = await temporaryDirectory();
const repository = join(root, "repository");
Expand Down