From fb16eab4fb5ec3844a7cf4942c04316806189bac Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Wed, 16 Apr 2025 13:48:34 -0700 Subject: [PATCH 01/41] Back out @lib indirection in tsconfig.json --- codex-cli/package-lock.json | 4 +- codex-cli/src/app.tsx | 2 +- codex-cli/src/{lib => }/approvals.ts | 2 +- codex-cli/src/cli.tsx | 4 +- .../src/components/chat/multiline-editor.tsx | 2 +- .../chat/terminal-chat-tool-call-item.tsx | 2 +- .../src/components/chat/terminal-chat.tsx | 4 +- codex-cli/src/{lib => }/format-command.ts | 0 codex-cli/src/{lib => }/parse-apply-patch.ts | 0 codex-cli/src/{lib => }/text-buffer.ts | 0 codex-cli/src/utils/agent/agent-loop.ts | 2 +- codex-cli/src/utils/agent/exec.ts | 2 +- .../src/utils/agent/handle-exec-command.ts | 6 +- codex-cli/src/utils/agent/review.ts | 2 +- codex-cli/src/utils/parsers.ts | 55 ++----------------- codex-cli/tests/agent-cancel-early.test.ts | 4 +- .../tests/agent-cancel-prev-response.test.ts | 4 +- codex-cli/tests/agent-cancel-race.test.ts | 4 +- codex-cli/tests/agent-cancel.test.ts | 4 +- .../tests/agent-function-call-id.test.ts | 4 +- .../tests/agent-generic-network-error.test.ts | 4 +- .../tests/agent-invalid-request-error.test.ts | 4 +- .../tests/agent-max-tokens-error.test.ts | 4 +- codex-cli/tests/agent-network-errors.test.ts | 4 +- codex-cli/tests/agent-project-doc.test.ts | 4 +- .../tests/agent-rate-limit-error.test.ts | 4 +- codex-cli/tests/agent-server-retry.test.ts | 4 +- codex-cli/tests/agent-terminate.test.ts | 4 +- codex-cli/tests/agent-thinking-time.test.ts | 4 +- codex-cli/tests/approvals.test.ts | 4 +- codex-cli/tests/external-editor.test.ts | 2 +- codex-cli/tests/format-command.test.ts | 2 +- .../tests/invalid-command-handling.test.ts | 4 +- ...ultiline-external-editor-shortcut.test.tsx | 2 +- .../tests/multiline-history-behavior.test.tsx | 6 +- codex-cli/tests/parse-apply-patch.test.ts | 2 +- .../tests/text-buffer-copy-paste.test.ts | 2 +- codex-cli/tests/text-buffer-crlf.test.ts | 2 +- codex-cli/tests/text-buffer-gaps.test.ts | 2 +- codex-cli/tests/text-buffer-word.test.ts | 2 +- codex-cli/tests/text-buffer.test.ts | 2 +- codex-cli/tsconfig.json | 3 - 42 files changed, 65 insertions(+), 113 deletions(-) rename codex-cli/src/{lib => }/approvals.ts (99%) rename codex-cli/src/{lib => }/format-command.ts (100%) rename codex-cli/src/{lib => }/parse-apply-patch.ts (100%) rename codex-cli/src/{lib => }/text-buffer.ts (100%) diff --git a/codex-cli/package-lock.json b/codex-cli/package-lock.json index d29561b53a..589ee59659 100644 --- a/codex-cli/package-lock.json +++ b/codex-cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "@openai/codex", - "version": "0.1.04160940", + "version": "0.1.04161241", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@openai/codex", - "version": "0.1.04160940", + "version": "0.1.04161241", "license": "Apache-2.0", "dependencies": { "@inkjs/ui": "^2.0.0", diff --git a/codex-cli/src/app.tsx b/codex-cli/src/app.tsx index dbb0cdedc8..c0b8c6f4e3 100644 --- a/codex-cli/src/app.tsx +++ b/codex-cli/src/app.tsx @@ -1,5 +1,5 @@ +import type { ApprovalPolicy } from "./approvals"; import type { AppConfig } from "./utils/config"; -import type { ApprovalPolicy } from "@lib/approvals"; import type { ResponseItem } from "openai/resources/responses/responses"; import TerminalChat from "./components/chat/terminal-chat"; diff --git a/codex-cli/src/lib/approvals.ts b/codex-cli/src/approvals.ts similarity index 99% rename from codex-cli/src/lib/approvals.ts rename to codex-cli/src/approvals.ts index 8985939a43..0cf3703b54 100644 --- a/codex-cli/src/lib/approvals.ts +++ b/codex-cli/src/approvals.ts @@ -3,7 +3,7 @@ import type { ParseEntry, ControlOperator } from "shell-quote"; import { identify_files_added, identify_files_needed, -} from "../utils/agent/apply-patch"; +} from "./utils/agent/apply-patch"; import * as path from "path"; import { parse } from "shell-quote"; diff --git a/codex-cli/src/cli.tsx b/codex-cli/src/cli.tsx index 5e43dff1f3..0af421a431 100644 --- a/codex-cli/src/cli.tsx +++ b/codex-cli/src/cli.tsx @@ -1,9 +1,9 @@ #!/usr/bin/env node import type { AppRollout } from "./app"; +import type { ApprovalPolicy } from "./approvals"; import type { CommandConfirmation } from "./utils/agent/agent-loop"; import type { AppConfig } from "./utils/config"; -import type { ApprovalPolicy } from "@lib/approvals"; import type { ResponseItem } from "openai/resources/responses/responses"; import App from "./app"; @@ -124,7 +124,7 @@ const cli = meow( fullContext: { type: "boolean", aliases: ["f"], - description: `Run in full-context editing approach. The model is given the whole code + description: `Run in full-context editing approach. The model is given the whole code directory as context and performs changes in one go without acting.`, }, }, diff --git a/codex-cli/src/components/chat/multiline-editor.tsx b/codex-cli/src/components/chat/multiline-editor.tsx index c18555447e..c99961bbcd 100644 --- a/codex-cli/src/components/chat/multiline-editor.tsx +++ b/codex-cli/src/components/chat/multiline-editor.tsx @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { useTerminalSize } from "../../hooks/use-terminal-size"; -import TextBuffer from "../../lib/text-buffer.js"; +import TextBuffer from "../../text-buffer.js"; import chalk from "chalk"; import { Box, Text, useInput, useStdin } from "ink"; import { EventEmitter } from "node:events"; diff --git a/codex-cli/src/components/chat/terminal-chat-tool-call-item.tsx b/codex-cli/src/components/chat/terminal-chat-tool-call-item.tsx index 1aeb7d7e98..5853460884 100644 --- a/codex-cli/src/components/chat/terminal-chat-tool-call-item.tsx +++ b/codex-cli/src/components/chat/terminal-chat-tool-call-item.tsx @@ -1,5 +1,5 @@ +import { parseApplyPatch } from "../../parse-apply-patch"; import { shortenPath } from "../../utils/short-path"; -import { parseApplyPatch } from "@lib/parse-apply-patch"; import chalk from "chalk"; import { Text } from "ink"; import React from "react"; diff --git a/codex-cli/src/components/chat/terminal-chat.tsx b/codex-cli/src/components/chat/terminal-chat.tsx index bbc9bb052a..35fecec564 100644 --- a/codex-cli/src/components/chat/terminal-chat.tsx +++ b/codex-cli/src/components/chat/terminal-chat.tsx @@ -1,6 +1,6 @@ +import type { ApplyPatchCommand, ApprovalPolicy } from "../../approvals.js"; import type { CommandConfirmation } from "../../utils/agent/agent-loop.js"; import type { AppConfig } from "../../utils/config.js"; -import type { ApplyPatchCommand, ApprovalPolicy } from "@lib/approvals.js"; import type { ColorName } from "chalk"; import type { ResponseItem } from "openai/resources/responses/responses.mjs"; import type { ReviewDecision } from "src/utils/agent/review.ts"; @@ -12,6 +12,7 @@ import { uniqueById, } from "./terminal-chat-utils.js"; import TerminalMessageHistory from "./terminal-message-history.js"; +import { formatCommandForDisplay } from "../../format-command.js"; import { useConfirmation } from "../../hooks/use-confirmation.js"; import { useTerminalSize } from "../../hooks/use-terminal-size.js"; import { AgentLoop } from "../../utils/agent/agent-loop.js"; @@ -25,7 +26,6 @@ import ApprovalModeOverlay from "../approval-mode-overlay.js"; import HelpOverlay from "../help-overlay.js"; import HistoryOverlay from "../history-overlay.js"; import ModelOverlay from "../model-overlay.js"; -import { formatCommandForDisplay } from "@lib/format-command.js"; import { Box, Text } from "ink"; import React, { useEffect, useMemo, useState } from "react"; import { inspect } from "util"; diff --git a/codex-cli/src/lib/format-command.ts b/codex-cli/src/format-command.ts similarity index 100% rename from codex-cli/src/lib/format-command.ts rename to codex-cli/src/format-command.ts diff --git a/codex-cli/src/lib/parse-apply-patch.ts b/codex-cli/src/parse-apply-patch.ts similarity index 100% rename from codex-cli/src/lib/parse-apply-patch.ts rename to codex-cli/src/parse-apply-patch.ts diff --git a/codex-cli/src/lib/text-buffer.ts b/codex-cli/src/text-buffer.ts similarity index 100% rename from codex-cli/src/lib/text-buffer.ts rename to codex-cli/src/text-buffer.ts diff --git a/codex-cli/src/utils/agent/agent-loop.ts b/codex-cli/src/utils/agent/agent-loop.ts index 65127fd85c..d0db6f125d 100644 --- a/codex-cli/src/utils/agent/agent-loop.ts +++ b/codex-cli/src/utils/agent/agent-loop.ts @@ -1,6 +1,6 @@ import type { ReviewDecision } from "./review.js"; +import type { ApplyPatchCommand, ApprovalPolicy } from "../../approvals.js"; import type { AppConfig } from "../config.js"; -import type { ApplyPatchCommand, ApprovalPolicy } from "@lib/approvals.js"; import type { ResponseFunctionToolCall, ResponseInputItem, diff --git a/codex-cli/src/utils/agent/exec.ts b/codex-cli/src/utils/agent/exec.ts index aade68a860..a441f192f8 100644 --- a/codex-cli/src/utils/agent/exec.ts +++ b/codex-cli/src/utils/agent/exec.ts @@ -5,7 +5,7 @@ import { process_patch } from "./apply-patch.js"; import { SandboxType } from "./sandbox/interface.js"; import { execWithSeatbelt } from "./sandbox/macos-seatbelt.js"; import { exec as rawExec } from "./sandbox/raw-exec.js"; -import { formatCommandForDisplay } from "@lib/format-command.js"; +import { formatCommandForDisplay } from "../../format-command.js"; import fs from "fs"; import os from "os"; diff --git a/codex-cli/src/utils/agent/handle-exec-command.ts b/codex-cli/src/utils/agent/handle-exec-command.ts index 5eaa11d660..41b7abbcd4 100644 --- a/codex-cli/src/utils/agent/handle-exec-command.ts +++ b/codex-cli/src/utils/agent/handle-exec-command.ts @@ -1,7 +1,7 @@ import type { CommandConfirmation } from "./agent-loop.js"; import type { AppConfig } from "../config.js"; import type { ExecInput } from "./sandbox/interface.js"; -import type { ApplyPatchCommand, ApprovalPolicy } from "@lib/approvals.js"; +import type { ApplyPatchCommand, ApprovalPolicy } from "../../approvals.js"; import type { ResponseInputItem } from "openai/resources/responses/responses.mjs"; import { exec, execApplyPatch } from "./exec.js"; @@ -9,8 +9,8 @@ import { isLoggingEnabled, log } from "./log.js"; import { ReviewDecision } from "./review.js"; import { FullAutoErrorMode } from "../auto-approval-mode.js"; import { SandboxType } from "./sandbox/interface.js"; -import { canAutoApprove } from "@lib/approvals.js"; -import { formatCommandForDisplay } from "@lib/format-command.js"; +import { canAutoApprove } from "../../approvals.js"; +import { formatCommandForDisplay } from "../../format-command.js"; import { access } from "fs/promises"; // --------------------------------------------------------------------------- diff --git a/codex-cli/src/utils/agent/review.ts b/codex-cli/src/utils/agent/review.ts index ed2af0ce17..a370388569 100644 --- a/codex-cli/src/utils/agent/review.ts +++ b/codex-cli/src/utils/agent/review.ts @@ -1,4 +1,4 @@ -import type { SafeCommandReason } from "@lib/approvals"; +import type { SafeCommandReason } from "../../approvals"; export type CommandReviewDetails = { cmd: Array; diff --git a/codex-cli/src/utils/parsers.ts b/codex-cli/src/utils/parsers.ts index cb477c1d7d..815e7b2f00 100644 --- a/codex-cli/src/utils/parsers.ts +++ b/codex-cli/src/utils/parsers.ts @@ -3,11 +3,13 @@ import type { ExecInput, ExecOutputMetadata, } from "./agent/sandbox/interface.js"; -import type { SafeCommandReason } from "@lib/approvals.js"; import type { ResponseFunctionToolCall } from "openai/resources/responses/responses.mjs"; +import { isSafeCommand, type SafeCommandReason } from "../approvals.js"; import { log } from "node:console"; import process from "process"; +import { parse } from "shell-quote"; +import { formatCommandForDisplay } from "src/format-command.js"; // The console utility import is intentionally explicit to avoid bundlers from // including the entire `console` module when only the `log` function is @@ -23,52 +25,6 @@ const SAFE_SHELL_OPERATORS: ReadonlySet = new Set([ ";", ]); -// Lazily resolve heavy dependencies at runtime to avoid test environments -// (which might not have the @lib alias configured) from failing at import -// time. If the modules cannot be loaded we fall back to permissive stub -// implementations so that basic functionality – like unit‑testing small UI -// helpers – continues to work without the full codex‑lib dependency tree. - -let isSafeCommand: (cmd: Array) => SafeCommandReason | null = () => - null; -let shellQuoteParse: - | ((cmd: string, env?: Record) => Array) - | undefined; -let formatCommandForDisplay: (cmd: Array) => string = (cmd) => - cmd.join(" "); - -async function loadLibs(): Promise { - try { - const approvals = await import("@lib/approvals.js"); - if (typeof approvals.isSafeCommand === "function") { - isSafeCommand = approvals.isSafeCommand; - } - } catch { - // ignore – keep stub - } - try { - const fmt = await import("@lib/format-command.js"); - if (typeof fmt.formatCommandForDisplay === "function") { - formatCommandForDisplay = fmt.formatCommandForDisplay; - } - } catch { - // ignore – keep stub - } - try { - const sq = await import("shell-quote"); - if (typeof sq.parse === "function") { - shellQuoteParse = sq.parse as typeof shellQuoteParse; - } - } catch { - // ignore – keep stub - } -} - -// Trigger the dynamic import in the background; callers that need the real -// implementation should await the returned promise (parsers currently does not -// require this for correctness during tests). -void loadLibs(); - export function parseToolCallOutput(toolCallOutput: string): { output: string; metadata: ExecOutputMetadata; @@ -175,10 +131,9 @@ function computeAutoApproval(cmd: Array): SafeCommandReason | null { cmd.length === 3 && cmd[0] === "bash" && cmd[1] === "-lc" && - typeof cmd[2] === "string" && - shellQuoteParse + typeof cmd[2] === "string" ) { - const parsed = shellQuoteParse(cmd[2], process.env ?? {}); + const parsed = parse(cmd[2], process.env ?? {}); if (parsed.length === 0) { return null; } diff --git a/codex-cli/tests/agent-cancel-early.test.ts b/codex-cli/tests/agent-cancel-early.test.ts index b7b5fdcae7..b235a6d6bc 100644 --- a/codex-cli/tests/agent-cancel-early.test.ts +++ b/codex-cli/tests/agent-cancel-early.test.ts @@ -64,13 +64,13 @@ vi.mock("openai", () => { }; }); -vi.mock("@lib/approvals.js", () => ({ +vi.mock("../src/approvals.js", () => ({ __esModule: true, alwaysApprovedCommands: new Set(), canAutoApprove: () => ({ type: "auto-approve", runInSandbox: false } as any), })); -vi.mock("@lib/format-command.js", () => ({ +vi.mock("../src/format-command.js", () => ({ __esModule: true, formatCommandForDisplay: (c: Array) => c.join(" "), })); diff --git a/codex-cli/tests/agent-cancel-prev-response.test.ts b/codex-cli/tests/agent-cancel-prev-response.test.ts index 4047f88452..fe73c338cc 100644 --- a/codex-cli/tests/agent-cancel-prev-response.test.ts +++ b/codex-cli/tests/agent-cancel-prev-response.test.ts @@ -71,13 +71,13 @@ vi.mock("openai", () => { }); // Stub helpers not relevant for this test. -vi.mock("@lib/approvals.js", () => ({ +vi.mock("../src/approvals.js", () => ({ __esModule: true, alwaysApprovedCommands: new Set(), canAutoApprove: () => ({ type: "auto-approve", runInSandbox: false } as any), })); -vi.mock("@lib/format-command.js", () => ({ +vi.mock("../src/format-command.js", () => ({ __esModule: true, formatCommandForDisplay: (c: Array) => c.join(" "), })); diff --git a/codex-cli/tests/agent-cancel-race.test.ts b/codex-cli/tests/agent-cancel-race.test.ts index c9c1845d8e..89e7cca744 100644 --- a/codex-cli/tests/agent-cancel-race.test.ts +++ b/codex-cli/tests/agent-cancel-race.test.ts @@ -67,11 +67,11 @@ vi.mock("openai", () => { }); // Stubs for external helpers referenced indirectly. -vi.mock("@lib/approvals.js", () => ({ +vi.mock("../src/approvals.js", () => ({ __esModule: true, isSafeCommand: () => null, })); -vi.mock("@lib/format-command.js", () => ({ +vi.mock("../src/format-command.js", () => ({ __esModule: true, formatCommandForDisplay: (c: Array) => c.join(" "), })); diff --git a/codex-cli/tests/agent-cancel.test.ts b/codex-cli/tests/agent-cancel.test.ts index 93ff5ba317..69c17f7f7e 100644 --- a/codex-cli/tests/agent-cancel.test.ts +++ b/codex-cli/tests/agent-cancel.test.ts @@ -47,7 +47,7 @@ vi.mock("openai", () => { }); // Mock the approvals and formatCommand helpers referenced by handle‑exec‑command. -vi.mock("@lib/approvals.js", () => { +vi.mock("../src/approvals.js", () => { return { __esModule: true, alwaysApprovedCommands: new Set(), @@ -57,7 +57,7 @@ vi.mock("@lib/approvals.js", () => { }; }); -vi.mock("@lib/format-command.js", () => { +vi.mock("../src/format-command.js", () => { return { __esModule: true, formatCommandForDisplay: (cmd: Array) => cmd.join(" "), diff --git a/codex-cli/tests/agent-function-call-id.test.ts b/codex-cli/tests/agent-function-call-id.test.ts index 7d0a55d122..d50c08eea4 100644 --- a/codex-cli/tests/agent-function-call-id.test.ts +++ b/codex-cli/tests/agent-function-call-id.test.ts @@ -88,14 +88,14 @@ vi.mock("openai", () => { }); // Stub approvals & command formatting – not relevant for this test. -vi.mock("@lib/approvals.js", () => ({ +vi.mock("../src/approvals.js", () => ({ __esModule: true, alwaysApprovedCommands: new Set(), canAutoApprove: () => ({ type: "auto-approve", runInSandbox: false } as any), isSafeCommand: () => null, })); -vi.mock("@lib/format-command.js", () => ({ +vi.mock("../src/format-command.js", () => ({ __esModule: true, formatCommandForDisplay: (c: Array) => c.join(" "), })); diff --git a/codex-cli/tests/agent-generic-network-error.test.ts b/codex-cli/tests/agent-generic-network-error.test.ts index 8c636fbda3..942adff668 100644 --- a/codex-cli/tests/agent-generic-network-error.test.ts +++ b/codex-cli/tests/agent-generic-network-error.test.ts @@ -23,14 +23,14 @@ vi.mock("openai", () => { }); // Stub approvals / formatting helpers – unrelated to network handling. -vi.mock("@lib/approvals.js", () => ({ +vi.mock("../src/approvals.js", () => ({ __esModule: true, alwaysApprovedCommands: new Set(), canAutoApprove: () => ({ type: "auto-approve", runInSandbox: false } as any), isSafeCommand: () => null, })); -vi.mock("@lib/format-command.js", () => ({ +vi.mock("../src/format-command.js", () => ({ __esModule: true, formatCommandForDisplay: (c: Array) => c.join(" "), })); diff --git a/codex-cli/tests/agent-invalid-request-error.test.ts b/codex-cli/tests/agent-invalid-request-error.test.ts index 631d451944..090d0b52d9 100644 --- a/codex-cli/tests/agent-invalid-request-error.test.ts +++ b/codex-cli/tests/agent-invalid-request-error.test.ts @@ -22,14 +22,14 @@ vi.mock("openai", () => { }; }); -vi.mock("@lib/approvals.js", () => ({ +vi.mock("../src/approvals.js", () => ({ __esModule: true, alwaysApprovedCommands: new Set(), canAutoApprove: () => ({ type: "auto-approve", runInSandbox: false } as any), isSafeCommand: () => null, })); -vi.mock("@lib/format-command.js", () => ({ +vi.mock("../src/format-command.js", () => ({ __esModule: true, formatCommandForDisplay: (c: Array) => c.join(" "), })); diff --git a/codex-cli/tests/agent-max-tokens-error.test.ts b/codex-cli/tests/agent-max-tokens-error.test.ts index 5e01229454..de4fd17026 100644 --- a/codex-cli/tests/agent-max-tokens-error.test.ts +++ b/codex-cli/tests/agent-max-tokens-error.test.ts @@ -22,14 +22,14 @@ vi.mock("openai", () => { }; }); -vi.mock("@lib/approvals.js", () => ({ +vi.mock("../src/approvals.js", () => ({ __esModule: true, alwaysApprovedCommands: new Set(), canAutoApprove: () => ({ type: "auto-approve", runInSandbox: false } as any), isSafeCommand: () => null, })); -vi.mock("@lib/format-command.js", () => ({ +vi.mock("../src/format-command.js", () => ({ __esModule: true, formatCommandForDisplay: (c: Array) => c.join(" "), })); diff --git a/codex-cli/tests/agent-network-errors.test.ts b/codex-cli/tests/agent-network-errors.test.ts index e01b08e918..f98ea5bf19 100644 --- a/codex-cli/tests/agent-network-errors.test.ts +++ b/codex-cli/tests/agent-network-errors.test.ts @@ -42,14 +42,14 @@ vi.mock("openai", () => { }); // Stub approvals / formatting helpers – not relevant here. -vi.mock("@lib/approvals.js", () => ({ +vi.mock("../src/approvals.js", () => ({ __esModule: true, alwaysApprovedCommands: new Set(), canAutoApprove: () => ({ type: "auto-approve", runInSandbox: false } as any), isSafeCommand: () => null, })); -vi.mock("@lib/format-command.js", () => ({ +vi.mock("../src/format-command.js", () => ({ __esModule: true, formatCommandForDisplay: (c: Array) => c.join(" "), })); diff --git a/codex-cli/tests/agent-project-doc.test.ts b/codex-cli/tests/agent-project-doc.test.ts index 97ad9c8c34..d3050f3953 100644 --- a/codex-cli/tests/agent-project-doc.test.ts +++ b/codex-cli/tests/agent-project-doc.test.ts @@ -51,7 +51,7 @@ vi.mock("openai", () => { // The AgentLoop pulls these helpers in order to decide whether a command can // be auto‑approved. None of that matters for this test, so we stub the module // with minimal no‑op implementations. -vi.mock("@lib/approvals.js", () => { +vi.mock("../src/approvals.js", () => { return { __esModule: true, alwaysApprovedCommands: new Set(), @@ -61,7 +61,7 @@ vi.mock("@lib/approvals.js", () => { }; }); -vi.mock("@lib/format-command.js", () => { +vi.mock("../src/format-command.js", () => { return { __esModule: true, formatCommandForDisplay: (cmd: Array) => cmd.join(" "), diff --git a/codex-cli/tests/agent-rate-limit-error.test.ts b/codex-cli/tests/agent-rate-limit-error.test.ts index 318b52f348..abe91171c4 100644 --- a/codex-cli/tests/agent-rate-limit-error.test.ts +++ b/codex-cli/tests/agent-rate-limit-error.test.ts @@ -52,14 +52,14 @@ vi.mock("openai", () => { }); // Stub approvals / formatting helpers – not relevant to rate‑limit handling. -vi.mock("@lib/approvals.js", () => ({ +vi.mock("../src/approvals.js", () => ({ __esModule: true, alwaysApprovedCommands: new Set(), canAutoApprove: () => ({ type: "auto-approve", runInSandbox: false } as any), isSafeCommand: () => null, })); -vi.mock("@lib/format-command.js", () => ({ +vi.mock("../src/format-command.js", () => ({ __esModule: true, formatCommandForDisplay: (c: Array) => c.join(" "), })); diff --git a/codex-cli/tests/agent-server-retry.test.ts b/codex-cli/tests/agent-server-retry.test.ts index df98e69e95..e851dd9778 100644 --- a/codex-cli/tests/agent-server-retry.test.ts +++ b/codex-cli/tests/agent-server-retry.test.ts @@ -32,14 +32,14 @@ vi.mock("openai", () => { }; }); -vi.mock("@lib/approvals.js", () => ({ +vi.mock("../src/approvals.js", () => ({ __esModule: true, alwaysApprovedCommands: new Set(), canAutoApprove: () => ({ type: "auto-approve", runInSandbox: false } as any), isSafeCommand: () => null, })); -vi.mock("@lib/format-command.js", () => ({ +vi.mock("../src/format-command.js", () => ({ __esModule: true, formatCommandForDisplay: (c: Array) => c.join(" "), })); diff --git a/codex-cli/tests/agent-terminate.test.ts b/codex-cli/tests/agent-terminate.test.ts index 7a60456518..bce77437af 100644 --- a/codex-cli/tests/agent-terminate.test.ts +++ b/codex-cli/tests/agent-terminate.test.ts @@ -49,7 +49,7 @@ vi.mock("openai", () => { // --- Helpers referenced by handle‑exec‑command ----------------------------- -vi.mock("@lib/approvals.js", () => { +vi.mock("../src/approvals.js", () => { return { __esModule: true, alwaysApprovedCommands: new Set(), @@ -59,7 +59,7 @@ vi.mock("@lib/approvals.js", () => { }; }); -vi.mock("@lib/format-command.js", () => { +vi.mock("../src/format-command.js", () => { return { __esModule: true, formatCommandForDisplay: (cmd: Array) => cmd.join(" "), diff --git a/codex-cli/tests/agent-thinking-time.test.ts b/codex-cli/tests/agent-thinking-time.test.ts index c94d8a5e5e..7132070084 100644 --- a/codex-cli/tests/agent-thinking-time.test.ts +++ b/codex-cli/tests/agent-thinking-time.test.ts @@ -74,12 +74,12 @@ vi.mock("openai", () => { }); // Stub helpers referenced indirectly so we do not pull in real FS/network -vi.mock("@lib/approvals.js", () => ({ +vi.mock("../src/approvals.js", () => ({ __esModule: true, isSafeCommand: () => null, })); -vi.mock("@lib/format-command.js", () => ({ +vi.mock("../src/format-command.js", () => ({ __esModule: true, formatCommandForDisplay: (c: Array) => c.join(" "), })); diff --git a/codex-cli/tests/approvals.test.ts b/codex-cli/tests/approvals.test.ts index e06db94092..7cb0bd3d3e 100644 --- a/codex-cli/tests/approvals.test.ts +++ b/codex-cli/tests/approvals.test.ts @@ -1,6 +1,6 @@ -import type { SafetyAssessment } from "../src/lib/approvals"; +import type { SafetyAssessment } from "../src/approvals"; -import { canAutoApprove } from "../src/lib/approvals"; +import { canAutoApprove } from "../src/approvals"; import { describe, test, expect } from "vitest"; describe("canAutoApprove()", () => { diff --git a/codex-cli/tests/external-editor.test.ts b/codex-cli/tests/external-editor.test.ts index d530be5ecc..77041c2870 100644 --- a/codex-cli/tests/external-editor.test.ts +++ b/codex-cli/tests/external-editor.test.ts @@ -1,4 +1,4 @@ -import TextBuffer from "../src/lib/text-buffer"; +import TextBuffer from "../src/text-buffer"; import { describe, it, expect, vi } from "vitest"; /* ------------------------------------------------------------------------- diff --git a/codex-cli/tests/format-command.test.ts b/codex-cli/tests/format-command.test.ts index 3981d9b106..7de417308a 100644 --- a/codex-cli/tests/format-command.test.ts +++ b/codex-cli/tests/format-command.test.ts @@ -1,4 +1,4 @@ -import { formatCommandForDisplay } from "../src/lib/format-command"; +import { formatCommandForDisplay } from "../src/format-command"; import { describe, test, expect } from "vitest"; describe("formatCommandForDisplay()", () => { diff --git a/codex-cli/tests/invalid-command-handling.test.ts b/codex-cli/tests/invalid-command-handling.test.ts index 6619b4f240..a3f87a7251 100644 --- a/codex-cli/tests/invalid-command-handling.test.ts +++ b/codex-cli/tests/invalid-command-handling.test.ts @@ -22,7 +22,7 @@ describe("rawExec – invalid command handling", () => { // --------------------------------------------------------------------------- // Mock approvals and logging helpers so the test focuses on execution flow. -vi.mock("@lib/approvals.js", () => { +vi.mock("../src/approvals.js", () => { return { __esModule: true, canAutoApprove: () => @@ -31,7 +31,7 @@ vi.mock("@lib/approvals.js", () => { }; }); -vi.mock("@lib/format-command.js", () => { +vi.mock("../src/format-command.js", () => { return { __esModule: true, formatCommandForDisplay: (cmd: Array) => cmd.join(" "), diff --git a/codex-cli/tests/multiline-external-editor-shortcut.test.tsx b/codex-cli/tests/multiline-external-editor-shortcut.test.tsx index 158a5e655d..9b2e2f25e5 100644 --- a/codex-cli/tests/multiline-external-editor-shortcut.test.tsx +++ b/codex-cli/tests/multiline-external-editor-shortcut.test.tsx @@ -1,6 +1,6 @@ import { renderTui } from "./ui-test-helpers.js"; import MultilineTextEditor from "../src/components/chat/multiline-editor.js"; -import TextBuffer from "../src/lib/text-buffer.js"; +import TextBuffer from "../src/text-buffer.js"; import * as React from "react"; import { describe, it, expect, vi } from "vitest"; diff --git a/codex-cli/tests/multiline-history-behavior.test.tsx b/codex-cli/tests/multiline-history-behavior.test.tsx index 5c906837ee..cada52ddab 100644 --- a/codex-cli/tests/multiline-history-behavior.test.tsx +++ b/codex-cli/tests/multiline-history-behavior.test.tsx @@ -34,12 +34,12 @@ vi.mock("../src/utils/input-utils.js", () => ({ })), })); -// Mock the optional @lib/* dependencies so the dynamic import in parsers.ts +// Mock the optional ../src/* dependencies so the dynamic import in parsers.ts // does not fail during the test environment where the alias isn't configured. -vi.mock("@lib/format-command.js", () => ({ +vi.mock("../src/format-command.js", () => ({ formatCommandForDisplay: (cmd: Array) => cmd.join(" "), })); -vi.mock("@lib/approvals.js", () => ({ +vi.mock("../src/approvals.js", () => ({ isSafeCommand: (_cmd: Array) => null, })); diff --git a/codex-cli/tests/parse-apply-patch.test.ts b/codex-cli/tests/parse-apply-patch.test.ts index 0195542e56..53aa119093 100644 --- a/codex-cli/tests/parse-apply-patch.test.ts +++ b/codex-cli/tests/parse-apply-patch.test.ts @@ -1,4 +1,4 @@ -import { parseApplyPatch } from "../src/lib/parse-apply-patch"; +import { parseApplyPatch } from "../src/parse-apply-patch"; import { expect, test, describe } from "vitest"; // Helper function to unwrap a non‑null result in tests that expect success. diff --git a/codex-cli/tests/text-buffer-copy-paste.test.ts b/codex-cli/tests/text-buffer-copy-paste.test.ts index 311b2b9aa6..cc1fd119e5 100644 --- a/codex-cli/tests/text-buffer-copy-paste.test.ts +++ b/codex-cli/tests/text-buffer-copy-paste.test.ts @@ -1,4 +1,4 @@ -import TextBuffer from "../src/lib/text-buffer.js"; +import TextBuffer from "../src/text-buffer.js"; import { describe, it, expect } from "vitest"; // These tests ensure that the TextBuffer copy‑&‑paste logic keeps parity with diff --git a/codex-cli/tests/text-buffer-crlf.test.ts b/codex-cli/tests/text-buffer-crlf.test.ts index 4b33b498b5..736c22a27d 100644 --- a/codex-cli/tests/text-buffer-crlf.test.ts +++ b/codex-cli/tests/text-buffer-crlf.test.ts @@ -1,4 +1,4 @@ -import TextBuffer from "../src/lib/text-buffer.js"; +import TextBuffer from "../src/text-buffer.js"; import { describe, it, expect } from "vitest"; describe("TextBuffer – newline normalisation", () => { diff --git a/codex-cli/tests/text-buffer-gaps.test.ts b/codex-cli/tests/text-buffer-gaps.test.ts index 986ad37eed..046d468e8a 100644 --- a/codex-cli/tests/text-buffer-gaps.test.ts +++ b/codex-cli/tests/text-buffer-gaps.test.ts @@ -1,4 +1,4 @@ -import TextBuffer from "../src/lib/text-buffer"; +import TextBuffer from "../src/text-buffer"; import { describe, it, expect } from "vitest"; // The purpose of this test‑suite is NOT to make the implementation green today diff --git a/codex-cli/tests/text-buffer-word.test.ts b/codex-cli/tests/text-buffer-word.test.ts index 009786a28c..4ea7679450 100644 --- a/codex-cli/tests/text-buffer-word.test.ts +++ b/codex-cli/tests/text-buffer-word.test.ts @@ -1,4 +1,4 @@ -import TextBuffer from "../src/lib/text-buffer.js"; +import TextBuffer from "../src/text-buffer.js"; import { describe, test, expect } from "vitest"; describe("TextBuffer – word‑wise navigation & deletion", () => { diff --git a/codex-cli/tests/text-buffer.test.ts b/codex-cli/tests/text-buffer.test.ts index ae78f29409..c3f33d0fa1 100644 --- a/codex-cli/tests/text-buffer.test.ts +++ b/codex-cli/tests/text-buffer.test.ts @@ -1,4 +1,4 @@ -import TextBuffer from "../src/lib/text-buffer"; +import TextBuffer from "../src/text-buffer"; import { describe, it, expect } from "vitest"; describe("TextBuffer – basic editing parity with Rust suite", () => { diff --git a/codex-cli/tsconfig.json b/codex-cli/tsconfig.json index 626fc5dbf4..d1dacc9149 100644 --- a/codex-cli/tsconfig.json +++ b/codex-cli/tsconfig.json @@ -11,9 +11,6 @@ ], "types": ["node"], "baseUrl": "./", - "paths": { - "@lib/*": ["./src/lib/*"] - }, "resolveJsonModule": false, // ESM doesn't yet support JSON modules. "jsx": "react", "declaration": true, From 66904fc9256815c2603f4c06e772f3f978b1b730 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Wed, 16 Apr 2025 13:51:46 -0700 Subject: [PATCH 02/41] Back out @lib indirection in tsconfig.json --- codex-cli/src/app.tsx | 2 +- codex-cli/src/{lib => }/approvals.ts | 2 +- codex-cli/src/cli.tsx | 4 +- .../src/components/chat/multiline-editor.tsx | 2 +- .../chat/terminal-chat-tool-call-item.tsx | 2 +- .../src/components/chat/terminal-chat.tsx | 4 +- codex-cli/src/{lib => }/format-command.ts | 0 codex-cli/src/{lib => }/parse-apply-patch.ts | 0 codex-cli/src/{lib => }/text-buffer.ts | 0 codex-cli/src/utils/agent/agent-loop.ts | 2 +- codex-cli/src/utils/agent/exec.ts | 2 +- .../src/utils/agent/handle-exec-command.ts | 6 +- codex-cli/src/utils/agent/review.ts | 2 +- codex-cli/src/utils/parsers.ts | 55 ++----------------- codex-cli/tests/agent-cancel-early.test.ts | 4 +- .../tests/agent-cancel-prev-response.test.ts | 4 +- codex-cli/tests/agent-cancel-race.test.ts | 4 +- codex-cli/tests/agent-cancel.test.ts | 4 +- .../tests/agent-function-call-id.test.ts | 4 +- .../tests/agent-generic-network-error.test.ts | 4 +- .../tests/agent-invalid-request-error.test.ts | 4 +- .../tests/agent-max-tokens-error.test.ts | 4 +- codex-cli/tests/agent-network-errors.test.ts | 4 +- codex-cli/tests/agent-project-doc.test.ts | 4 +- .../tests/agent-rate-limit-error.test.ts | 4 +- codex-cli/tests/agent-server-retry.test.ts | 4 +- codex-cli/tests/agent-terminate.test.ts | 4 +- codex-cli/tests/agent-thinking-time.test.ts | 4 +- codex-cli/tests/approvals.test.ts | 4 +- codex-cli/tests/external-editor.test.ts | 2 +- codex-cli/tests/format-command.test.ts | 2 +- .../tests/invalid-command-handling.test.ts | 4 +- ...ultiline-external-editor-shortcut.test.tsx | 2 +- .../tests/multiline-history-behavior.test.tsx | 6 +- codex-cli/tests/parse-apply-patch.test.ts | 2 +- .../tests/text-buffer-copy-paste.test.ts | 2 +- codex-cli/tests/text-buffer-crlf.test.ts | 2 +- codex-cli/tests/text-buffer-gaps.test.ts | 2 +- codex-cli/tests/text-buffer-word.test.ts | 2 +- codex-cli/tests/text-buffer.test.ts | 2 +- codex-cli/tsconfig.json | 3 - 41 files changed, 63 insertions(+), 111 deletions(-) rename codex-cli/src/{lib => }/approvals.ts (99%) rename codex-cli/src/{lib => }/format-command.ts (100%) rename codex-cli/src/{lib => }/parse-apply-patch.ts (100%) rename codex-cli/src/{lib => }/text-buffer.ts (100%) diff --git a/codex-cli/src/app.tsx b/codex-cli/src/app.tsx index dbb0cdedc8..c0b8c6f4e3 100644 --- a/codex-cli/src/app.tsx +++ b/codex-cli/src/app.tsx @@ -1,5 +1,5 @@ +import type { ApprovalPolicy } from "./approvals"; import type { AppConfig } from "./utils/config"; -import type { ApprovalPolicy } from "@lib/approvals"; import type { ResponseItem } from "openai/resources/responses/responses"; import TerminalChat from "./components/chat/terminal-chat"; diff --git a/codex-cli/src/lib/approvals.ts b/codex-cli/src/approvals.ts similarity index 99% rename from codex-cli/src/lib/approvals.ts rename to codex-cli/src/approvals.ts index 8985939a43..0cf3703b54 100644 --- a/codex-cli/src/lib/approvals.ts +++ b/codex-cli/src/approvals.ts @@ -3,7 +3,7 @@ import type { ParseEntry, ControlOperator } from "shell-quote"; import { identify_files_added, identify_files_needed, -} from "../utils/agent/apply-patch"; +} from "./utils/agent/apply-patch"; import * as path from "path"; import { parse } from "shell-quote"; diff --git a/codex-cli/src/cli.tsx b/codex-cli/src/cli.tsx index 5e43dff1f3..0af421a431 100644 --- a/codex-cli/src/cli.tsx +++ b/codex-cli/src/cli.tsx @@ -1,9 +1,9 @@ #!/usr/bin/env node import type { AppRollout } from "./app"; +import type { ApprovalPolicy } from "./approvals"; import type { CommandConfirmation } from "./utils/agent/agent-loop"; import type { AppConfig } from "./utils/config"; -import type { ApprovalPolicy } from "@lib/approvals"; import type { ResponseItem } from "openai/resources/responses/responses"; import App from "./app"; @@ -124,7 +124,7 @@ const cli = meow( fullContext: { type: "boolean", aliases: ["f"], - description: `Run in full-context editing approach. The model is given the whole code + description: `Run in full-context editing approach. The model is given the whole code directory as context and performs changes in one go without acting.`, }, }, diff --git a/codex-cli/src/components/chat/multiline-editor.tsx b/codex-cli/src/components/chat/multiline-editor.tsx index c18555447e..c99961bbcd 100644 --- a/codex-cli/src/components/chat/multiline-editor.tsx +++ b/codex-cli/src/components/chat/multiline-editor.tsx @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { useTerminalSize } from "../../hooks/use-terminal-size"; -import TextBuffer from "../../lib/text-buffer.js"; +import TextBuffer from "../../text-buffer.js"; import chalk from "chalk"; import { Box, Text, useInput, useStdin } from "ink"; import { EventEmitter } from "node:events"; diff --git a/codex-cli/src/components/chat/terminal-chat-tool-call-item.tsx b/codex-cli/src/components/chat/terminal-chat-tool-call-item.tsx index 1aeb7d7e98..5853460884 100644 --- a/codex-cli/src/components/chat/terminal-chat-tool-call-item.tsx +++ b/codex-cli/src/components/chat/terminal-chat-tool-call-item.tsx @@ -1,5 +1,5 @@ +import { parseApplyPatch } from "../../parse-apply-patch"; import { shortenPath } from "../../utils/short-path"; -import { parseApplyPatch } from "@lib/parse-apply-patch"; import chalk from "chalk"; import { Text } from "ink"; import React from "react"; diff --git a/codex-cli/src/components/chat/terminal-chat.tsx b/codex-cli/src/components/chat/terminal-chat.tsx index bbc9bb052a..35fecec564 100644 --- a/codex-cli/src/components/chat/terminal-chat.tsx +++ b/codex-cli/src/components/chat/terminal-chat.tsx @@ -1,6 +1,6 @@ +import type { ApplyPatchCommand, ApprovalPolicy } from "../../approvals.js"; import type { CommandConfirmation } from "../../utils/agent/agent-loop.js"; import type { AppConfig } from "../../utils/config.js"; -import type { ApplyPatchCommand, ApprovalPolicy } from "@lib/approvals.js"; import type { ColorName } from "chalk"; import type { ResponseItem } from "openai/resources/responses/responses.mjs"; import type { ReviewDecision } from "src/utils/agent/review.ts"; @@ -12,6 +12,7 @@ import { uniqueById, } from "./terminal-chat-utils.js"; import TerminalMessageHistory from "./terminal-message-history.js"; +import { formatCommandForDisplay } from "../../format-command.js"; import { useConfirmation } from "../../hooks/use-confirmation.js"; import { useTerminalSize } from "../../hooks/use-terminal-size.js"; import { AgentLoop } from "../../utils/agent/agent-loop.js"; @@ -25,7 +26,6 @@ import ApprovalModeOverlay from "../approval-mode-overlay.js"; import HelpOverlay from "../help-overlay.js"; import HistoryOverlay from "../history-overlay.js"; import ModelOverlay from "../model-overlay.js"; -import { formatCommandForDisplay } from "@lib/format-command.js"; import { Box, Text } from "ink"; import React, { useEffect, useMemo, useState } from "react"; import { inspect } from "util"; diff --git a/codex-cli/src/lib/format-command.ts b/codex-cli/src/format-command.ts similarity index 100% rename from codex-cli/src/lib/format-command.ts rename to codex-cli/src/format-command.ts diff --git a/codex-cli/src/lib/parse-apply-patch.ts b/codex-cli/src/parse-apply-patch.ts similarity index 100% rename from codex-cli/src/lib/parse-apply-patch.ts rename to codex-cli/src/parse-apply-patch.ts diff --git a/codex-cli/src/lib/text-buffer.ts b/codex-cli/src/text-buffer.ts similarity index 100% rename from codex-cli/src/lib/text-buffer.ts rename to codex-cli/src/text-buffer.ts diff --git a/codex-cli/src/utils/agent/agent-loop.ts b/codex-cli/src/utils/agent/agent-loop.ts index e2b465733d..d88604c471 100644 --- a/codex-cli/src/utils/agent/agent-loop.ts +++ b/codex-cli/src/utils/agent/agent-loop.ts @@ -1,6 +1,6 @@ import type { ReviewDecision } from "./review.js"; +import type { ApplyPatchCommand, ApprovalPolicy } from "../../approvals.js"; import type { AppConfig } from "../config.js"; -import type { ApplyPatchCommand, ApprovalPolicy } from "@lib/approvals.js"; import type { ResponseFunctionToolCall, ResponseInputItem, diff --git a/codex-cli/src/utils/agent/exec.ts b/codex-cli/src/utils/agent/exec.ts index aade68a860..a441f192f8 100644 --- a/codex-cli/src/utils/agent/exec.ts +++ b/codex-cli/src/utils/agent/exec.ts @@ -5,7 +5,7 @@ import { process_patch } from "./apply-patch.js"; import { SandboxType } from "./sandbox/interface.js"; import { execWithSeatbelt } from "./sandbox/macos-seatbelt.js"; import { exec as rawExec } from "./sandbox/raw-exec.js"; -import { formatCommandForDisplay } from "@lib/format-command.js"; +import { formatCommandForDisplay } from "../../format-command.js"; import fs from "fs"; import os from "os"; diff --git a/codex-cli/src/utils/agent/handle-exec-command.ts b/codex-cli/src/utils/agent/handle-exec-command.ts index 5eaa11d660..41b7abbcd4 100644 --- a/codex-cli/src/utils/agent/handle-exec-command.ts +++ b/codex-cli/src/utils/agent/handle-exec-command.ts @@ -1,7 +1,7 @@ import type { CommandConfirmation } from "./agent-loop.js"; import type { AppConfig } from "../config.js"; import type { ExecInput } from "./sandbox/interface.js"; -import type { ApplyPatchCommand, ApprovalPolicy } from "@lib/approvals.js"; +import type { ApplyPatchCommand, ApprovalPolicy } from "../../approvals.js"; import type { ResponseInputItem } from "openai/resources/responses/responses.mjs"; import { exec, execApplyPatch } from "./exec.js"; @@ -9,8 +9,8 @@ import { isLoggingEnabled, log } from "./log.js"; import { ReviewDecision } from "./review.js"; import { FullAutoErrorMode } from "../auto-approval-mode.js"; import { SandboxType } from "./sandbox/interface.js"; -import { canAutoApprove } from "@lib/approvals.js"; -import { formatCommandForDisplay } from "@lib/format-command.js"; +import { canAutoApprove } from "../../approvals.js"; +import { formatCommandForDisplay } from "../../format-command.js"; import { access } from "fs/promises"; // --------------------------------------------------------------------------- diff --git a/codex-cli/src/utils/agent/review.ts b/codex-cli/src/utils/agent/review.ts index ed2af0ce17..a370388569 100644 --- a/codex-cli/src/utils/agent/review.ts +++ b/codex-cli/src/utils/agent/review.ts @@ -1,4 +1,4 @@ -import type { SafeCommandReason } from "@lib/approvals"; +import type { SafeCommandReason } from "../../approvals"; export type CommandReviewDetails = { cmd: Array; diff --git a/codex-cli/src/utils/parsers.ts b/codex-cli/src/utils/parsers.ts index cb477c1d7d..815e7b2f00 100644 --- a/codex-cli/src/utils/parsers.ts +++ b/codex-cli/src/utils/parsers.ts @@ -3,11 +3,13 @@ import type { ExecInput, ExecOutputMetadata, } from "./agent/sandbox/interface.js"; -import type { SafeCommandReason } from "@lib/approvals.js"; import type { ResponseFunctionToolCall } from "openai/resources/responses/responses.mjs"; +import { isSafeCommand, type SafeCommandReason } from "../approvals.js"; import { log } from "node:console"; import process from "process"; +import { parse } from "shell-quote"; +import { formatCommandForDisplay } from "src/format-command.js"; // The console utility import is intentionally explicit to avoid bundlers from // including the entire `console` module when only the `log` function is @@ -23,52 +25,6 @@ const SAFE_SHELL_OPERATORS: ReadonlySet = new Set([ ";", ]); -// Lazily resolve heavy dependencies at runtime to avoid test environments -// (which might not have the @lib alias configured) from failing at import -// time. If the modules cannot be loaded we fall back to permissive stub -// implementations so that basic functionality – like unit‑testing small UI -// helpers – continues to work without the full codex‑lib dependency tree. - -let isSafeCommand: (cmd: Array) => SafeCommandReason | null = () => - null; -let shellQuoteParse: - | ((cmd: string, env?: Record) => Array) - | undefined; -let formatCommandForDisplay: (cmd: Array) => string = (cmd) => - cmd.join(" "); - -async function loadLibs(): Promise { - try { - const approvals = await import("@lib/approvals.js"); - if (typeof approvals.isSafeCommand === "function") { - isSafeCommand = approvals.isSafeCommand; - } - } catch { - // ignore – keep stub - } - try { - const fmt = await import("@lib/format-command.js"); - if (typeof fmt.formatCommandForDisplay === "function") { - formatCommandForDisplay = fmt.formatCommandForDisplay; - } - } catch { - // ignore – keep stub - } - try { - const sq = await import("shell-quote"); - if (typeof sq.parse === "function") { - shellQuoteParse = sq.parse as typeof shellQuoteParse; - } - } catch { - // ignore – keep stub - } -} - -// Trigger the dynamic import in the background; callers that need the real -// implementation should await the returned promise (parsers currently does not -// require this for correctness during tests). -void loadLibs(); - export function parseToolCallOutput(toolCallOutput: string): { output: string; metadata: ExecOutputMetadata; @@ -175,10 +131,9 @@ function computeAutoApproval(cmd: Array): SafeCommandReason | null { cmd.length === 3 && cmd[0] === "bash" && cmd[1] === "-lc" && - typeof cmd[2] === "string" && - shellQuoteParse + typeof cmd[2] === "string" ) { - const parsed = shellQuoteParse(cmd[2], process.env ?? {}); + const parsed = parse(cmd[2], process.env ?? {}); if (parsed.length === 0) { return null; } diff --git a/codex-cli/tests/agent-cancel-early.test.ts b/codex-cli/tests/agent-cancel-early.test.ts index b7b5fdcae7..b235a6d6bc 100644 --- a/codex-cli/tests/agent-cancel-early.test.ts +++ b/codex-cli/tests/agent-cancel-early.test.ts @@ -64,13 +64,13 @@ vi.mock("openai", () => { }; }); -vi.mock("@lib/approvals.js", () => ({ +vi.mock("../src/approvals.js", () => ({ __esModule: true, alwaysApprovedCommands: new Set(), canAutoApprove: () => ({ type: "auto-approve", runInSandbox: false } as any), })); -vi.mock("@lib/format-command.js", () => ({ +vi.mock("../src/format-command.js", () => ({ __esModule: true, formatCommandForDisplay: (c: Array) => c.join(" "), })); diff --git a/codex-cli/tests/agent-cancel-prev-response.test.ts b/codex-cli/tests/agent-cancel-prev-response.test.ts index 4047f88452..fe73c338cc 100644 --- a/codex-cli/tests/agent-cancel-prev-response.test.ts +++ b/codex-cli/tests/agent-cancel-prev-response.test.ts @@ -71,13 +71,13 @@ vi.mock("openai", () => { }); // Stub helpers not relevant for this test. -vi.mock("@lib/approvals.js", () => ({ +vi.mock("../src/approvals.js", () => ({ __esModule: true, alwaysApprovedCommands: new Set(), canAutoApprove: () => ({ type: "auto-approve", runInSandbox: false } as any), })); -vi.mock("@lib/format-command.js", () => ({ +vi.mock("../src/format-command.js", () => ({ __esModule: true, formatCommandForDisplay: (c: Array) => c.join(" "), })); diff --git a/codex-cli/tests/agent-cancel-race.test.ts b/codex-cli/tests/agent-cancel-race.test.ts index c9c1845d8e..89e7cca744 100644 --- a/codex-cli/tests/agent-cancel-race.test.ts +++ b/codex-cli/tests/agent-cancel-race.test.ts @@ -67,11 +67,11 @@ vi.mock("openai", () => { }); // Stubs for external helpers referenced indirectly. -vi.mock("@lib/approvals.js", () => ({ +vi.mock("../src/approvals.js", () => ({ __esModule: true, isSafeCommand: () => null, })); -vi.mock("@lib/format-command.js", () => ({ +vi.mock("../src/format-command.js", () => ({ __esModule: true, formatCommandForDisplay: (c: Array) => c.join(" "), })); diff --git a/codex-cli/tests/agent-cancel.test.ts b/codex-cli/tests/agent-cancel.test.ts index 93ff5ba317..69c17f7f7e 100644 --- a/codex-cli/tests/agent-cancel.test.ts +++ b/codex-cli/tests/agent-cancel.test.ts @@ -47,7 +47,7 @@ vi.mock("openai", () => { }); // Mock the approvals and formatCommand helpers referenced by handle‑exec‑command. -vi.mock("@lib/approvals.js", () => { +vi.mock("../src/approvals.js", () => { return { __esModule: true, alwaysApprovedCommands: new Set(), @@ -57,7 +57,7 @@ vi.mock("@lib/approvals.js", () => { }; }); -vi.mock("@lib/format-command.js", () => { +vi.mock("../src/format-command.js", () => { return { __esModule: true, formatCommandForDisplay: (cmd: Array) => cmd.join(" "), diff --git a/codex-cli/tests/agent-function-call-id.test.ts b/codex-cli/tests/agent-function-call-id.test.ts index 7d0a55d122..d50c08eea4 100644 --- a/codex-cli/tests/agent-function-call-id.test.ts +++ b/codex-cli/tests/agent-function-call-id.test.ts @@ -88,14 +88,14 @@ vi.mock("openai", () => { }); // Stub approvals & command formatting – not relevant for this test. -vi.mock("@lib/approvals.js", () => ({ +vi.mock("../src/approvals.js", () => ({ __esModule: true, alwaysApprovedCommands: new Set(), canAutoApprove: () => ({ type: "auto-approve", runInSandbox: false } as any), isSafeCommand: () => null, })); -vi.mock("@lib/format-command.js", () => ({ +vi.mock("../src/format-command.js", () => ({ __esModule: true, formatCommandForDisplay: (c: Array) => c.join(" "), })); diff --git a/codex-cli/tests/agent-generic-network-error.test.ts b/codex-cli/tests/agent-generic-network-error.test.ts index 8c636fbda3..942adff668 100644 --- a/codex-cli/tests/agent-generic-network-error.test.ts +++ b/codex-cli/tests/agent-generic-network-error.test.ts @@ -23,14 +23,14 @@ vi.mock("openai", () => { }); // Stub approvals / formatting helpers – unrelated to network handling. -vi.mock("@lib/approvals.js", () => ({ +vi.mock("../src/approvals.js", () => ({ __esModule: true, alwaysApprovedCommands: new Set(), canAutoApprove: () => ({ type: "auto-approve", runInSandbox: false } as any), isSafeCommand: () => null, })); -vi.mock("@lib/format-command.js", () => ({ +vi.mock("../src/format-command.js", () => ({ __esModule: true, formatCommandForDisplay: (c: Array) => c.join(" "), })); diff --git a/codex-cli/tests/agent-invalid-request-error.test.ts b/codex-cli/tests/agent-invalid-request-error.test.ts index 631d451944..090d0b52d9 100644 --- a/codex-cli/tests/agent-invalid-request-error.test.ts +++ b/codex-cli/tests/agent-invalid-request-error.test.ts @@ -22,14 +22,14 @@ vi.mock("openai", () => { }; }); -vi.mock("@lib/approvals.js", () => ({ +vi.mock("../src/approvals.js", () => ({ __esModule: true, alwaysApprovedCommands: new Set(), canAutoApprove: () => ({ type: "auto-approve", runInSandbox: false } as any), isSafeCommand: () => null, })); -vi.mock("@lib/format-command.js", () => ({ +vi.mock("../src/format-command.js", () => ({ __esModule: true, formatCommandForDisplay: (c: Array) => c.join(" "), })); diff --git a/codex-cli/tests/agent-max-tokens-error.test.ts b/codex-cli/tests/agent-max-tokens-error.test.ts index 5e01229454..de4fd17026 100644 --- a/codex-cli/tests/agent-max-tokens-error.test.ts +++ b/codex-cli/tests/agent-max-tokens-error.test.ts @@ -22,14 +22,14 @@ vi.mock("openai", () => { }; }); -vi.mock("@lib/approvals.js", () => ({ +vi.mock("../src/approvals.js", () => ({ __esModule: true, alwaysApprovedCommands: new Set(), canAutoApprove: () => ({ type: "auto-approve", runInSandbox: false } as any), isSafeCommand: () => null, })); -vi.mock("@lib/format-command.js", () => ({ +vi.mock("../src/format-command.js", () => ({ __esModule: true, formatCommandForDisplay: (c: Array) => c.join(" "), })); diff --git a/codex-cli/tests/agent-network-errors.test.ts b/codex-cli/tests/agent-network-errors.test.ts index e01b08e918..f98ea5bf19 100644 --- a/codex-cli/tests/agent-network-errors.test.ts +++ b/codex-cli/tests/agent-network-errors.test.ts @@ -42,14 +42,14 @@ vi.mock("openai", () => { }); // Stub approvals / formatting helpers – not relevant here. -vi.mock("@lib/approvals.js", () => ({ +vi.mock("../src/approvals.js", () => ({ __esModule: true, alwaysApprovedCommands: new Set(), canAutoApprove: () => ({ type: "auto-approve", runInSandbox: false } as any), isSafeCommand: () => null, })); -vi.mock("@lib/format-command.js", () => ({ +vi.mock("../src/format-command.js", () => ({ __esModule: true, formatCommandForDisplay: (c: Array) => c.join(" "), })); diff --git a/codex-cli/tests/agent-project-doc.test.ts b/codex-cli/tests/agent-project-doc.test.ts index 97ad9c8c34..d3050f3953 100644 --- a/codex-cli/tests/agent-project-doc.test.ts +++ b/codex-cli/tests/agent-project-doc.test.ts @@ -51,7 +51,7 @@ vi.mock("openai", () => { // The AgentLoop pulls these helpers in order to decide whether a command can // be auto‑approved. None of that matters for this test, so we stub the module // with minimal no‑op implementations. -vi.mock("@lib/approvals.js", () => { +vi.mock("../src/approvals.js", () => { return { __esModule: true, alwaysApprovedCommands: new Set(), @@ -61,7 +61,7 @@ vi.mock("@lib/approvals.js", () => { }; }); -vi.mock("@lib/format-command.js", () => { +vi.mock("../src/format-command.js", () => { return { __esModule: true, formatCommandForDisplay: (cmd: Array) => cmd.join(" "), diff --git a/codex-cli/tests/agent-rate-limit-error.test.ts b/codex-cli/tests/agent-rate-limit-error.test.ts index 18779450aa..9782744679 100644 --- a/codex-cli/tests/agent-rate-limit-error.test.ts +++ b/codex-cli/tests/agent-rate-limit-error.test.ts @@ -34,14 +34,14 @@ vi.mock("openai", () => { // Stub helpers that the agent indirectly imports so it does not attempt any // file‑system access or real approvals logic during the test. -vi.mock("@lib/approvals.js", () => ({ +vi.mock("../src/approvals.js", () => ({ __esModule: true, alwaysApprovedCommands: new Set(), canAutoApprove: () => ({ type: "auto-approve", runInSandbox: false } as any), isSafeCommand: () => null, })); -vi.mock("@lib/format-command.js", () => ({ +vi.mock("../src/format-command.js", () => ({ __esModule: true, formatCommandForDisplay: (c: Array) => c.join(" "), })); diff --git a/codex-cli/tests/agent-server-retry.test.ts b/codex-cli/tests/agent-server-retry.test.ts index 9ec4eb5aac..09278f2ceb 100644 --- a/codex-cli/tests/agent-server-retry.test.ts +++ b/codex-cli/tests/agent-server-retry.test.ts @@ -32,14 +32,14 @@ vi.mock("openai", () => { }; }); -vi.mock("@lib/approvals.js", () => ({ +vi.mock("../src/approvals.js", () => ({ __esModule: true, alwaysApprovedCommands: new Set(), canAutoApprove: () => ({ type: "auto-approve", runInSandbox: false } as any), isSafeCommand: () => null, })); -vi.mock("@lib/format-command.js", () => ({ +vi.mock("../src/format-command.js", () => ({ __esModule: true, formatCommandForDisplay: (c: Array) => c.join(" "), })); diff --git a/codex-cli/tests/agent-terminate.test.ts b/codex-cli/tests/agent-terminate.test.ts index 7a60456518..bce77437af 100644 --- a/codex-cli/tests/agent-terminate.test.ts +++ b/codex-cli/tests/agent-terminate.test.ts @@ -49,7 +49,7 @@ vi.mock("openai", () => { // --- Helpers referenced by handle‑exec‑command ----------------------------- -vi.mock("@lib/approvals.js", () => { +vi.mock("../src/approvals.js", () => { return { __esModule: true, alwaysApprovedCommands: new Set(), @@ -59,7 +59,7 @@ vi.mock("@lib/approvals.js", () => { }; }); -vi.mock("@lib/format-command.js", () => { +vi.mock("../src/format-command.js", () => { return { __esModule: true, formatCommandForDisplay: (cmd: Array) => cmd.join(" "), diff --git a/codex-cli/tests/agent-thinking-time.test.ts b/codex-cli/tests/agent-thinking-time.test.ts index c94d8a5e5e..7132070084 100644 --- a/codex-cli/tests/agent-thinking-time.test.ts +++ b/codex-cli/tests/agent-thinking-time.test.ts @@ -74,12 +74,12 @@ vi.mock("openai", () => { }); // Stub helpers referenced indirectly so we do not pull in real FS/network -vi.mock("@lib/approvals.js", () => ({ +vi.mock("../src/approvals.js", () => ({ __esModule: true, isSafeCommand: () => null, })); -vi.mock("@lib/format-command.js", () => ({ +vi.mock("../src/format-command.js", () => ({ __esModule: true, formatCommandForDisplay: (c: Array) => c.join(" "), })); diff --git a/codex-cli/tests/approvals.test.ts b/codex-cli/tests/approvals.test.ts index e06db94092..7cb0bd3d3e 100644 --- a/codex-cli/tests/approvals.test.ts +++ b/codex-cli/tests/approvals.test.ts @@ -1,6 +1,6 @@ -import type { SafetyAssessment } from "../src/lib/approvals"; +import type { SafetyAssessment } from "../src/approvals"; -import { canAutoApprove } from "../src/lib/approvals"; +import { canAutoApprove } from "../src/approvals"; import { describe, test, expect } from "vitest"; describe("canAutoApprove()", () => { diff --git a/codex-cli/tests/external-editor.test.ts b/codex-cli/tests/external-editor.test.ts index d530be5ecc..77041c2870 100644 --- a/codex-cli/tests/external-editor.test.ts +++ b/codex-cli/tests/external-editor.test.ts @@ -1,4 +1,4 @@ -import TextBuffer from "../src/lib/text-buffer"; +import TextBuffer from "../src/text-buffer"; import { describe, it, expect, vi } from "vitest"; /* ------------------------------------------------------------------------- diff --git a/codex-cli/tests/format-command.test.ts b/codex-cli/tests/format-command.test.ts index 3981d9b106..7de417308a 100644 --- a/codex-cli/tests/format-command.test.ts +++ b/codex-cli/tests/format-command.test.ts @@ -1,4 +1,4 @@ -import { formatCommandForDisplay } from "../src/lib/format-command"; +import { formatCommandForDisplay } from "../src/format-command"; import { describe, test, expect } from "vitest"; describe("formatCommandForDisplay()", () => { diff --git a/codex-cli/tests/invalid-command-handling.test.ts b/codex-cli/tests/invalid-command-handling.test.ts index 6619b4f240..a3f87a7251 100644 --- a/codex-cli/tests/invalid-command-handling.test.ts +++ b/codex-cli/tests/invalid-command-handling.test.ts @@ -22,7 +22,7 @@ describe("rawExec – invalid command handling", () => { // --------------------------------------------------------------------------- // Mock approvals and logging helpers so the test focuses on execution flow. -vi.mock("@lib/approvals.js", () => { +vi.mock("../src/approvals.js", () => { return { __esModule: true, canAutoApprove: () => @@ -31,7 +31,7 @@ vi.mock("@lib/approvals.js", () => { }; }); -vi.mock("@lib/format-command.js", () => { +vi.mock("../src/format-command.js", () => { return { __esModule: true, formatCommandForDisplay: (cmd: Array) => cmd.join(" "), diff --git a/codex-cli/tests/multiline-external-editor-shortcut.test.tsx b/codex-cli/tests/multiline-external-editor-shortcut.test.tsx index 158a5e655d..9b2e2f25e5 100644 --- a/codex-cli/tests/multiline-external-editor-shortcut.test.tsx +++ b/codex-cli/tests/multiline-external-editor-shortcut.test.tsx @@ -1,6 +1,6 @@ import { renderTui } from "./ui-test-helpers.js"; import MultilineTextEditor from "../src/components/chat/multiline-editor.js"; -import TextBuffer from "../src/lib/text-buffer.js"; +import TextBuffer from "../src/text-buffer.js"; import * as React from "react"; import { describe, it, expect, vi } from "vitest"; diff --git a/codex-cli/tests/multiline-history-behavior.test.tsx b/codex-cli/tests/multiline-history-behavior.test.tsx index 5c906837ee..cada52ddab 100644 --- a/codex-cli/tests/multiline-history-behavior.test.tsx +++ b/codex-cli/tests/multiline-history-behavior.test.tsx @@ -34,12 +34,12 @@ vi.mock("../src/utils/input-utils.js", () => ({ })), })); -// Mock the optional @lib/* dependencies so the dynamic import in parsers.ts +// Mock the optional ../src/* dependencies so the dynamic import in parsers.ts // does not fail during the test environment where the alias isn't configured. -vi.mock("@lib/format-command.js", () => ({ +vi.mock("../src/format-command.js", () => ({ formatCommandForDisplay: (cmd: Array) => cmd.join(" "), })); -vi.mock("@lib/approvals.js", () => ({ +vi.mock("../src/approvals.js", () => ({ isSafeCommand: (_cmd: Array) => null, })); diff --git a/codex-cli/tests/parse-apply-patch.test.ts b/codex-cli/tests/parse-apply-patch.test.ts index 0195542e56..53aa119093 100644 --- a/codex-cli/tests/parse-apply-patch.test.ts +++ b/codex-cli/tests/parse-apply-patch.test.ts @@ -1,4 +1,4 @@ -import { parseApplyPatch } from "../src/lib/parse-apply-patch"; +import { parseApplyPatch } from "../src/parse-apply-patch"; import { expect, test, describe } from "vitest"; // Helper function to unwrap a non‑null result in tests that expect success. diff --git a/codex-cli/tests/text-buffer-copy-paste.test.ts b/codex-cli/tests/text-buffer-copy-paste.test.ts index 311b2b9aa6..cc1fd119e5 100644 --- a/codex-cli/tests/text-buffer-copy-paste.test.ts +++ b/codex-cli/tests/text-buffer-copy-paste.test.ts @@ -1,4 +1,4 @@ -import TextBuffer from "../src/lib/text-buffer.js"; +import TextBuffer from "../src/text-buffer.js"; import { describe, it, expect } from "vitest"; // These tests ensure that the TextBuffer copy‑&‑paste logic keeps parity with diff --git a/codex-cli/tests/text-buffer-crlf.test.ts b/codex-cli/tests/text-buffer-crlf.test.ts index 4b33b498b5..736c22a27d 100644 --- a/codex-cli/tests/text-buffer-crlf.test.ts +++ b/codex-cli/tests/text-buffer-crlf.test.ts @@ -1,4 +1,4 @@ -import TextBuffer from "../src/lib/text-buffer.js"; +import TextBuffer from "../src/text-buffer.js"; import { describe, it, expect } from "vitest"; describe("TextBuffer – newline normalisation", () => { diff --git a/codex-cli/tests/text-buffer-gaps.test.ts b/codex-cli/tests/text-buffer-gaps.test.ts index 986ad37eed..046d468e8a 100644 --- a/codex-cli/tests/text-buffer-gaps.test.ts +++ b/codex-cli/tests/text-buffer-gaps.test.ts @@ -1,4 +1,4 @@ -import TextBuffer from "../src/lib/text-buffer"; +import TextBuffer from "../src/text-buffer"; import { describe, it, expect } from "vitest"; // The purpose of this test‑suite is NOT to make the implementation green today diff --git a/codex-cli/tests/text-buffer-word.test.ts b/codex-cli/tests/text-buffer-word.test.ts index 009786a28c..4ea7679450 100644 --- a/codex-cli/tests/text-buffer-word.test.ts +++ b/codex-cli/tests/text-buffer-word.test.ts @@ -1,4 +1,4 @@ -import TextBuffer from "../src/lib/text-buffer.js"; +import TextBuffer from "../src/text-buffer.js"; import { describe, test, expect } from "vitest"; describe("TextBuffer – word‑wise navigation & deletion", () => { diff --git a/codex-cli/tests/text-buffer.test.ts b/codex-cli/tests/text-buffer.test.ts index ae78f29409..c3f33d0fa1 100644 --- a/codex-cli/tests/text-buffer.test.ts +++ b/codex-cli/tests/text-buffer.test.ts @@ -1,4 +1,4 @@ -import TextBuffer from "../src/lib/text-buffer"; +import TextBuffer from "../src/text-buffer"; import { describe, it, expect } from "vitest"; describe("TextBuffer – basic editing parity with Rust suite", () => { diff --git a/codex-cli/tsconfig.json b/codex-cli/tsconfig.json index 626fc5dbf4..d1dacc9149 100644 --- a/codex-cli/tsconfig.json +++ b/codex-cli/tsconfig.json @@ -11,9 +11,6 @@ ], "types": ["node"], "baseUrl": "./", - "paths": { - "@lib/*": ["./src/lib/*"] - }, "resolveJsonModule": false, // ESM doesn't yet support JSON modules. "jsx": "react", "declaration": true, From 2cafcf2ec061552d195a762123025d21f0f00097 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Wed, 16 Apr 2025 14:15:00 -0700 Subject: [PATCH 03/41] Back out @lib indirection in tsconfig.json --- codex-cli/src/app.tsx | 2 +- codex-cli/src/{lib => }/approvals.ts | 2 +- codex-cli/src/cli.tsx | 4 +- .../src/components/chat/multiline-editor.tsx | 2 +- .../chat/terminal-chat-tool-call-item.tsx | 2 +- .../src/components/chat/terminal-chat.tsx | 4 +- codex-cli/src/{lib => }/format-command.ts | 0 codex-cli/src/{lib => }/parse-apply-patch.ts | 0 codex-cli/src/{lib => }/text-buffer.ts | 0 codex-cli/src/utils/agent/agent-loop.ts | 2 +- codex-cli/src/utils/agent/exec.ts | 2 +- .../src/utils/agent/handle-exec-command.ts | 6 +- codex-cli/src/utils/agent/review.ts | 2 +- codex-cli/src/utils/parsers.ts | 55 ++----------------- codex-cli/tests/agent-cancel-early.test.ts | 4 +- .../tests/agent-cancel-prev-response.test.ts | 4 +- codex-cli/tests/agent-cancel-race.test.ts | 4 +- codex-cli/tests/agent-cancel.test.ts | 4 +- .../tests/agent-function-call-id.test.ts | 4 +- .../tests/agent-generic-network-error.test.ts | 4 +- .../tests/agent-invalid-request-error.test.ts | 4 +- .../tests/agent-max-tokens-error.test.ts | 4 +- codex-cli/tests/agent-network-errors.test.ts | 4 +- codex-cli/tests/agent-project-doc.test.ts | 4 +- .../tests/agent-rate-limit-error.test.ts | 4 +- codex-cli/tests/agent-server-retry.test.ts | 4 +- codex-cli/tests/agent-terminate.test.ts | 4 +- codex-cli/tests/agent-thinking-time.test.ts | 4 +- codex-cli/tests/approvals.test.ts | 4 +- codex-cli/tests/external-editor.test.ts | 2 +- codex-cli/tests/format-command.test.ts | 2 +- .../tests/invalid-command-handling.test.ts | 4 +- ...ultiline-external-editor-shortcut.test.tsx | 2 +- .../tests/multiline-history-behavior.test.tsx | 6 +- codex-cli/tests/parse-apply-patch.test.ts | 2 +- .../tests/text-buffer-copy-paste.test.ts | 2 +- codex-cli/tests/text-buffer-crlf.test.ts | 2 +- codex-cli/tests/text-buffer-gaps.test.ts | 2 +- codex-cli/tests/text-buffer-word.test.ts | 2 +- codex-cli/tests/text-buffer.test.ts | 2 +- codex-cli/tsconfig.json | 3 - 41 files changed, 63 insertions(+), 111 deletions(-) rename codex-cli/src/{lib => }/approvals.ts (99%) rename codex-cli/src/{lib => }/format-command.ts (100%) rename codex-cli/src/{lib => }/parse-apply-patch.ts (100%) rename codex-cli/src/{lib => }/text-buffer.ts (100%) diff --git a/codex-cli/src/app.tsx b/codex-cli/src/app.tsx index dbb0cdedc8..c0b8c6f4e3 100644 --- a/codex-cli/src/app.tsx +++ b/codex-cli/src/app.tsx @@ -1,5 +1,5 @@ +import type { ApprovalPolicy } from "./approvals"; import type { AppConfig } from "./utils/config"; -import type { ApprovalPolicy } from "@lib/approvals"; import type { ResponseItem } from "openai/resources/responses/responses"; import TerminalChat from "./components/chat/terminal-chat"; diff --git a/codex-cli/src/lib/approvals.ts b/codex-cli/src/approvals.ts similarity index 99% rename from codex-cli/src/lib/approvals.ts rename to codex-cli/src/approvals.ts index 8985939a43..0cf3703b54 100644 --- a/codex-cli/src/lib/approvals.ts +++ b/codex-cli/src/approvals.ts @@ -3,7 +3,7 @@ import type { ParseEntry, ControlOperator } from "shell-quote"; import { identify_files_added, identify_files_needed, -} from "../utils/agent/apply-patch"; +} from "./utils/agent/apply-patch"; import * as path from "path"; import { parse } from "shell-quote"; diff --git a/codex-cli/src/cli.tsx b/codex-cli/src/cli.tsx index 5e43dff1f3..0af421a431 100644 --- a/codex-cli/src/cli.tsx +++ b/codex-cli/src/cli.tsx @@ -1,9 +1,9 @@ #!/usr/bin/env node import type { AppRollout } from "./app"; +import type { ApprovalPolicy } from "./approvals"; import type { CommandConfirmation } from "./utils/agent/agent-loop"; import type { AppConfig } from "./utils/config"; -import type { ApprovalPolicy } from "@lib/approvals"; import type { ResponseItem } from "openai/resources/responses/responses"; import App from "./app"; @@ -124,7 +124,7 @@ const cli = meow( fullContext: { type: "boolean", aliases: ["f"], - description: `Run in full-context editing approach. The model is given the whole code + description: `Run in full-context editing approach. The model is given the whole code directory as context and performs changes in one go without acting.`, }, }, diff --git a/codex-cli/src/components/chat/multiline-editor.tsx b/codex-cli/src/components/chat/multiline-editor.tsx index c18555447e..c99961bbcd 100644 --- a/codex-cli/src/components/chat/multiline-editor.tsx +++ b/codex-cli/src/components/chat/multiline-editor.tsx @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { useTerminalSize } from "../../hooks/use-terminal-size"; -import TextBuffer from "../../lib/text-buffer.js"; +import TextBuffer from "../../text-buffer.js"; import chalk from "chalk"; import { Box, Text, useInput, useStdin } from "ink"; import { EventEmitter } from "node:events"; diff --git a/codex-cli/src/components/chat/terminal-chat-tool-call-item.tsx b/codex-cli/src/components/chat/terminal-chat-tool-call-item.tsx index 1aeb7d7e98..5853460884 100644 --- a/codex-cli/src/components/chat/terminal-chat-tool-call-item.tsx +++ b/codex-cli/src/components/chat/terminal-chat-tool-call-item.tsx @@ -1,5 +1,5 @@ +import { parseApplyPatch } from "../../parse-apply-patch"; import { shortenPath } from "../../utils/short-path"; -import { parseApplyPatch } from "@lib/parse-apply-patch"; import chalk from "chalk"; import { Text } from "ink"; import React from "react"; diff --git a/codex-cli/src/components/chat/terminal-chat.tsx b/codex-cli/src/components/chat/terminal-chat.tsx index bbc9bb052a..35fecec564 100644 --- a/codex-cli/src/components/chat/terminal-chat.tsx +++ b/codex-cli/src/components/chat/terminal-chat.tsx @@ -1,6 +1,6 @@ +import type { ApplyPatchCommand, ApprovalPolicy } from "../../approvals.js"; import type { CommandConfirmation } from "../../utils/agent/agent-loop.js"; import type { AppConfig } from "../../utils/config.js"; -import type { ApplyPatchCommand, ApprovalPolicy } from "@lib/approvals.js"; import type { ColorName } from "chalk"; import type { ResponseItem } from "openai/resources/responses/responses.mjs"; import type { ReviewDecision } from "src/utils/agent/review.ts"; @@ -12,6 +12,7 @@ import { uniqueById, } from "./terminal-chat-utils.js"; import TerminalMessageHistory from "./terminal-message-history.js"; +import { formatCommandForDisplay } from "../../format-command.js"; import { useConfirmation } from "../../hooks/use-confirmation.js"; import { useTerminalSize } from "../../hooks/use-terminal-size.js"; import { AgentLoop } from "../../utils/agent/agent-loop.js"; @@ -25,7 +26,6 @@ import ApprovalModeOverlay from "../approval-mode-overlay.js"; import HelpOverlay from "../help-overlay.js"; import HistoryOverlay from "../history-overlay.js"; import ModelOverlay from "../model-overlay.js"; -import { formatCommandForDisplay } from "@lib/format-command.js"; import { Box, Text } from "ink"; import React, { useEffect, useMemo, useState } from "react"; import { inspect } from "util"; diff --git a/codex-cli/src/lib/format-command.ts b/codex-cli/src/format-command.ts similarity index 100% rename from codex-cli/src/lib/format-command.ts rename to codex-cli/src/format-command.ts diff --git a/codex-cli/src/lib/parse-apply-patch.ts b/codex-cli/src/parse-apply-patch.ts similarity index 100% rename from codex-cli/src/lib/parse-apply-patch.ts rename to codex-cli/src/parse-apply-patch.ts diff --git a/codex-cli/src/lib/text-buffer.ts b/codex-cli/src/text-buffer.ts similarity index 100% rename from codex-cli/src/lib/text-buffer.ts rename to codex-cli/src/text-buffer.ts diff --git a/codex-cli/src/utils/agent/agent-loop.ts b/codex-cli/src/utils/agent/agent-loop.ts index e2b465733d..d88604c471 100644 --- a/codex-cli/src/utils/agent/agent-loop.ts +++ b/codex-cli/src/utils/agent/agent-loop.ts @@ -1,6 +1,6 @@ import type { ReviewDecision } from "./review.js"; +import type { ApplyPatchCommand, ApprovalPolicy } from "../../approvals.js"; import type { AppConfig } from "../config.js"; -import type { ApplyPatchCommand, ApprovalPolicy } from "@lib/approvals.js"; import type { ResponseFunctionToolCall, ResponseInputItem, diff --git a/codex-cli/src/utils/agent/exec.ts b/codex-cli/src/utils/agent/exec.ts index aade68a860..a441f192f8 100644 --- a/codex-cli/src/utils/agent/exec.ts +++ b/codex-cli/src/utils/agent/exec.ts @@ -5,7 +5,7 @@ import { process_patch } from "./apply-patch.js"; import { SandboxType } from "./sandbox/interface.js"; import { execWithSeatbelt } from "./sandbox/macos-seatbelt.js"; import { exec as rawExec } from "./sandbox/raw-exec.js"; -import { formatCommandForDisplay } from "@lib/format-command.js"; +import { formatCommandForDisplay } from "../../format-command.js"; import fs from "fs"; import os from "os"; diff --git a/codex-cli/src/utils/agent/handle-exec-command.ts b/codex-cli/src/utils/agent/handle-exec-command.ts index 5eaa11d660..41b7abbcd4 100644 --- a/codex-cli/src/utils/agent/handle-exec-command.ts +++ b/codex-cli/src/utils/agent/handle-exec-command.ts @@ -1,7 +1,7 @@ import type { CommandConfirmation } from "./agent-loop.js"; import type { AppConfig } from "../config.js"; import type { ExecInput } from "./sandbox/interface.js"; -import type { ApplyPatchCommand, ApprovalPolicy } from "@lib/approvals.js"; +import type { ApplyPatchCommand, ApprovalPolicy } from "../../approvals.js"; import type { ResponseInputItem } from "openai/resources/responses/responses.mjs"; import { exec, execApplyPatch } from "./exec.js"; @@ -9,8 +9,8 @@ import { isLoggingEnabled, log } from "./log.js"; import { ReviewDecision } from "./review.js"; import { FullAutoErrorMode } from "../auto-approval-mode.js"; import { SandboxType } from "./sandbox/interface.js"; -import { canAutoApprove } from "@lib/approvals.js"; -import { formatCommandForDisplay } from "@lib/format-command.js"; +import { canAutoApprove } from "../../approvals.js"; +import { formatCommandForDisplay } from "../../format-command.js"; import { access } from "fs/promises"; // --------------------------------------------------------------------------- diff --git a/codex-cli/src/utils/agent/review.ts b/codex-cli/src/utils/agent/review.ts index ed2af0ce17..a370388569 100644 --- a/codex-cli/src/utils/agent/review.ts +++ b/codex-cli/src/utils/agent/review.ts @@ -1,4 +1,4 @@ -import type { SafeCommandReason } from "@lib/approvals"; +import type { SafeCommandReason } from "../../approvals"; export type CommandReviewDetails = { cmd: Array; diff --git a/codex-cli/src/utils/parsers.ts b/codex-cli/src/utils/parsers.ts index cb477c1d7d..815e7b2f00 100644 --- a/codex-cli/src/utils/parsers.ts +++ b/codex-cli/src/utils/parsers.ts @@ -3,11 +3,13 @@ import type { ExecInput, ExecOutputMetadata, } from "./agent/sandbox/interface.js"; -import type { SafeCommandReason } from "@lib/approvals.js"; import type { ResponseFunctionToolCall } from "openai/resources/responses/responses.mjs"; +import { isSafeCommand, type SafeCommandReason } from "../approvals.js"; import { log } from "node:console"; import process from "process"; +import { parse } from "shell-quote"; +import { formatCommandForDisplay } from "src/format-command.js"; // The console utility import is intentionally explicit to avoid bundlers from // including the entire `console` module when only the `log` function is @@ -23,52 +25,6 @@ const SAFE_SHELL_OPERATORS: ReadonlySet = new Set([ ";", ]); -// Lazily resolve heavy dependencies at runtime to avoid test environments -// (which might not have the @lib alias configured) from failing at import -// time. If the modules cannot be loaded we fall back to permissive stub -// implementations so that basic functionality – like unit‑testing small UI -// helpers – continues to work without the full codex‑lib dependency tree. - -let isSafeCommand: (cmd: Array) => SafeCommandReason | null = () => - null; -let shellQuoteParse: - | ((cmd: string, env?: Record) => Array) - | undefined; -let formatCommandForDisplay: (cmd: Array) => string = (cmd) => - cmd.join(" "); - -async function loadLibs(): Promise { - try { - const approvals = await import("@lib/approvals.js"); - if (typeof approvals.isSafeCommand === "function") { - isSafeCommand = approvals.isSafeCommand; - } - } catch { - // ignore – keep stub - } - try { - const fmt = await import("@lib/format-command.js"); - if (typeof fmt.formatCommandForDisplay === "function") { - formatCommandForDisplay = fmt.formatCommandForDisplay; - } - } catch { - // ignore – keep stub - } - try { - const sq = await import("shell-quote"); - if (typeof sq.parse === "function") { - shellQuoteParse = sq.parse as typeof shellQuoteParse; - } - } catch { - // ignore – keep stub - } -} - -// Trigger the dynamic import in the background; callers that need the real -// implementation should await the returned promise (parsers currently does not -// require this for correctness during tests). -void loadLibs(); - export function parseToolCallOutput(toolCallOutput: string): { output: string; metadata: ExecOutputMetadata; @@ -175,10 +131,9 @@ function computeAutoApproval(cmd: Array): SafeCommandReason | null { cmd.length === 3 && cmd[0] === "bash" && cmd[1] === "-lc" && - typeof cmd[2] === "string" && - shellQuoteParse + typeof cmd[2] === "string" ) { - const parsed = shellQuoteParse(cmd[2], process.env ?? {}); + const parsed = parse(cmd[2], process.env ?? {}); if (parsed.length === 0) { return null; } diff --git a/codex-cli/tests/agent-cancel-early.test.ts b/codex-cli/tests/agent-cancel-early.test.ts index b7b5fdcae7..b235a6d6bc 100644 --- a/codex-cli/tests/agent-cancel-early.test.ts +++ b/codex-cli/tests/agent-cancel-early.test.ts @@ -64,13 +64,13 @@ vi.mock("openai", () => { }; }); -vi.mock("@lib/approvals.js", () => ({ +vi.mock("../src/approvals.js", () => ({ __esModule: true, alwaysApprovedCommands: new Set(), canAutoApprove: () => ({ type: "auto-approve", runInSandbox: false } as any), })); -vi.mock("@lib/format-command.js", () => ({ +vi.mock("../src/format-command.js", () => ({ __esModule: true, formatCommandForDisplay: (c: Array) => c.join(" "), })); diff --git a/codex-cli/tests/agent-cancel-prev-response.test.ts b/codex-cli/tests/agent-cancel-prev-response.test.ts index 4047f88452..fe73c338cc 100644 --- a/codex-cli/tests/agent-cancel-prev-response.test.ts +++ b/codex-cli/tests/agent-cancel-prev-response.test.ts @@ -71,13 +71,13 @@ vi.mock("openai", () => { }); // Stub helpers not relevant for this test. -vi.mock("@lib/approvals.js", () => ({ +vi.mock("../src/approvals.js", () => ({ __esModule: true, alwaysApprovedCommands: new Set(), canAutoApprove: () => ({ type: "auto-approve", runInSandbox: false } as any), })); -vi.mock("@lib/format-command.js", () => ({ +vi.mock("../src/format-command.js", () => ({ __esModule: true, formatCommandForDisplay: (c: Array) => c.join(" "), })); diff --git a/codex-cli/tests/agent-cancel-race.test.ts b/codex-cli/tests/agent-cancel-race.test.ts index c9c1845d8e..89e7cca744 100644 --- a/codex-cli/tests/agent-cancel-race.test.ts +++ b/codex-cli/tests/agent-cancel-race.test.ts @@ -67,11 +67,11 @@ vi.mock("openai", () => { }); // Stubs for external helpers referenced indirectly. -vi.mock("@lib/approvals.js", () => ({ +vi.mock("../src/approvals.js", () => ({ __esModule: true, isSafeCommand: () => null, })); -vi.mock("@lib/format-command.js", () => ({ +vi.mock("../src/format-command.js", () => ({ __esModule: true, formatCommandForDisplay: (c: Array) => c.join(" "), })); diff --git a/codex-cli/tests/agent-cancel.test.ts b/codex-cli/tests/agent-cancel.test.ts index 93ff5ba317..69c17f7f7e 100644 --- a/codex-cli/tests/agent-cancel.test.ts +++ b/codex-cli/tests/agent-cancel.test.ts @@ -47,7 +47,7 @@ vi.mock("openai", () => { }); // Mock the approvals and formatCommand helpers referenced by handle‑exec‑command. -vi.mock("@lib/approvals.js", () => { +vi.mock("../src/approvals.js", () => { return { __esModule: true, alwaysApprovedCommands: new Set(), @@ -57,7 +57,7 @@ vi.mock("@lib/approvals.js", () => { }; }); -vi.mock("@lib/format-command.js", () => { +vi.mock("../src/format-command.js", () => { return { __esModule: true, formatCommandForDisplay: (cmd: Array) => cmd.join(" "), diff --git a/codex-cli/tests/agent-function-call-id.test.ts b/codex-cli/tests/agent-function-call-id.test.ts index 7d0a55d122..d50c08eea4 100644 --- a/codex-cli/tests/agent-function-call-id.test.ts +++ b/codex-cli/tests/agent-function-call-id.test.ts @@ -88,14 +88,14 @@ vi.mock("openai", () => { }); // Stub approvals & command formatting – not relevant for this test. -vi.mock("@lib/approvals.js", () => ({ +vi.mock("../src/approvals.js", () => ({ __esModule: true, alwaysApprovedCommands: new Set(), canAutoApprove: () => ({ type: "auto-approve", runInSandbox: false } as any), isSafeCommand: () => null, })); -vi.mock("@lib/format-command.js", () => ({ +vi.mock("../src/format-command.js", () => ({ __esModule: true, formatCommandForDisplay: (c: Array) => c.join(" "), })); diff --git a/codex-cli/tests/agent-generic-network-error.test.ts b/codex-cli/tests/agent-generic-network-error.test.ts index 8c636fbda3..942adff668 100644 --- a/codex-cli/tests/agent-generic-network-error.test.ts +++ b/codex-cli/tests/agent-generic-network-error.test.ts @@ -23,14 +23,14 @@ vi.mock("openai", () => { }); // Stub approvals / formatting helpers – unrelated to network handling. -vi.mock("@lib/approvals.js", () => ({ +vi.mock("../src/approvals.js", () => ({ __esModule: true, alwaysApprovedCommands: new Set(), canAutoApprove: () => ({ type: "auto-approve", runInSandbox: false } as any), isSafeCommand: () => null, })); -vi.mock("@lib/format-command.js", () => ({ +vi.mock("../src/format-command.js", () => ({ __esModule: true, formatCommandForDisplay: (c: Array) => c.join(" "), })); diff --git a/codex-cli/tests/agent-invalid-request-error.test.ts b/codex-cli/tests/agent-invalid-request-error.test.ts index 631d451944..090d0b52d9 100644 --- a/codex-cli/tests/agent-invalid-request-error.test.ts +++ b/codex-cli/tests/agent-invalid-request-error.test.ts @@ -22,14 +22,14 @@ vi.mock("openai", () => { }; }); -vi.mock("@lib/approvals.js", () => ({ +vi.mock("../src/approvals.js", () => ({ __esModule: true, alwaysApprovedCommands: new Set(), canAutoApprove: () => ({ type: "auto-approve", runInSandbox: false } as any), isSafeCommand: () => null, })); -vi.mock("@lib/format-command.js", () => ({ +vi.mock("../src/format-command.js", () => ({ __esModule: true, formatCommandForDisplay: (c: Array) => c.join(" "), })); diff --git a/codex-cli/tests/agent-max-tokens-error.test.ts b/codex-cli/tests/agent-max-tokens-error.test.ts index 5e01229454..de4fd17026 100644 --- a/codex-cli/tests/agent-max-tokens-error.test.ts +++ b/codex-cli/tests/agent-max-tokens-error.test.ts @@ -22,14 +22,14 @@ vi.mock("openai", () => { }; }); -vi.mock("@lib/approvals.js", () => ({ +vi.mock("../src/approvals.js", () => ({ __esModule: true, alwaysApprovedCommands: new Set(), canAutoApprove: () => ({ type: "auto-approve", runInSandbox: false } as any), isSafeCommand: () => null, })); -vi.mock("@lib/format-command.js", () => ({ +vi.mock("../src/format-command.js", () => ({ __esModule: true, formatCommandForDisplay: (c: Array) => c.join(" "), })); diff --git a/codex-cli/tests/agent-network-errors.test.ts b/codex-cli/tests/agent-network-errors.test.ts index e01b08e918..f98ea5bf19 100644 --- a/codex-cli/tests/agent-network-errors.test.ts +++ b/codex-cli/tests/agent-network-errors.test.ts @@ -42,14 +42,14 @@ vi.mock("openai", () => { }); // Stub approvals / formatting helpers – not relevant here. -vi.mock("@lib/approvals.js", () => ({ +vi.mock("../src/approvals.js", () => ({ __esModule: true, alwaysApprovedCommands: new Set(), canAutoApprove: () => ({ type: "auto-approve", runInSandbox: false } as any), isSafeCommand: () => null, })); -vi.mock("@lib/format-command.js", () => ({ +vi.mock("../src/format-command.js", () => ({ __esModule: true, formatCommandForDisplay: (c: Array) => c.join(" "), })); diff --git a/codex-cli/tests/agent-project-doc.test.ts b/codex-cli/tests/agent-project-doc.test.ts index 97ad9c8c34..d3050f3953 100644 --- a/codex-cli/tests/agent-project-doc.test.ts +++ b/codex-cli/tests/agent-project-doc.test.ts @@ -51,7 +51,7 @@ vi.mock("openai", () => { // The AgentLoop pulls these helpers in order to decide whether a command can // be auto‑approved. None of that matters for this test, so we stub the module // with minimal no‑op implementations. -vi.mock("@lib/approvals.js", () => { +vi.mock("../src/approvals.js", () => { return { __esModule: true, alwaysApprovedCommands: new Set(), @@ -61,7 +61,7 @@ vi.mock("@lib/approvals.js", () => { }; }); -vi.mock("@lib/format-command.js", () => { +vi.mock("../src/format-command.js", () => { return { __esModule: true, formatCommandForDisplay: (cmd: Array) => cmd.join(" "), diff --git a/codex-cli/tests/agent-rate-limit-error.test.ts b/codex-cli/tests/agent-rate-limit-error.test.ts index 18779450aa..9782744679 100644 --- a/codex-cli/tests/agent-rate-limit-error.test.ts +++ b/codex-cli/tests/agent-rate-limit-error.test.ts @@ -34,14 +34,14 @@ vi.mock("openai", () => { // Stub helpers that the agent indirectly imports so it does not attempt any // file‑system access or real approvals logic during the test. -vi.mock("@lib/approvals.js", () => ({ +vi.mock("../src/approvals.js", () => ({ __esModule: true, alwaysApprovedCommands: new Set(), canAutoApprove: () => ({ type: "auto-approve", runInSandbox: false } as any), isSafeCommand: () => null, })); -vi.mock("@lib/format-command.js", () => ({ +vi.mock("../src/format-command.js", () => ({ __esModule: true, formatCommandForDisplay: (c: Array) => c.join(" "), })); diff --git a/codex-cli/tests/agent-server-retry.test.ts b/codex-cli/tests/agent-server-retry.test.ts index 9ec4eb5aac..09278f2ceb 100644 --- a/codex-cli/tests/agent-server-retry.test.ts +++ b/codex-cli/tests/agent-server-retry.test.ts @@ -32,14 +32,14 @@ vi.mock("openai", () => { }; }); -vi.mock("@lib/approvals.js", () => ({ +vi.mock("../src/approvals.js", () => ({ __esModule: true, alwaysApprovedCommands: new Set(), canAutoApprove: () => ({ type: "auto-approve", runInSandbox: false } as any), isSafeCommand: () => null, })); -vi.mock("@lib/format-command.js", () => ({ +vi.mock("../src/format-command.js", () => ({ __esModule: true, formatCommandForDisplay: (c: Array) => c.join(" "), })); diff --git a/codex-cli/tests/agent-terminate.test.ts b/codex-cli/tests/agent-terminate.test.ts index 7a60456518..bce77437af 100644 --- a/codex-cli/tests/agent-terminate.test.ts +++ b/codex-cli/tests/agent-terminate.test.ts @@ -49,7 +49,7 @@ vi.mock("openai", () => { // --- Helpers referenced by handle‑exec‑command ----------------------------- -vi.mock("@lib/approvals.js", () => { +vi.mock("../src/approvals.js", () => { return { __esModule: true, alwaysApprovedCommands: new Set(), @@ -59,7 +59,7 @@ vi.mock("@lib/approvals.js", () => { }; }); -vi.mock("@lib/format-command.js", () => { +vi.mock("../src/format-command.js", () => { return { __esModule: true, formatCommandForDisplay: (cmd: Array) => cmd.join(" "), diff --git a/codex-cli/tests/agent-thinking-time.test.ts b/codex-cli/tests/agent-thinking-time.test.ts index c94d8a5e5e..7132070084 100644 --- a/codex-cli/tests/agent-thinking-time.test.ts +++ b/codex-cli/tests/agent-thinking-time.test.ts @@ -74,12 +74,12 @@ vi.mock("openai", () => { }); // Stub helpers referenced indirectly so we do not pull in real FS/network -vi.mock("@lib/approvals.js", () => ({ +vi.mock("../src/approvals.js", () => ({ __esModule: true, isSafeCommand: () => null, })); -vi.mock("@lib/format-command.js", () => ({ +vi.mock("../src/format-command.js", () => ({ __esModule: true, formatCommandForDisplay: (c: Array) => c.join(" "), })); diff --git a/codex-cli/tests/approvals.test.ts b/codex-cli/tests/approvals.test.ts index e06db94092..7cb0bd3d3e 100644 --- a/codex-cli/tests/approvals.test.ts +++ b/codex-cli/tests/approvals.test.ts @@ -1,6 +1,6 @@ -import type { SafetyAssessment } from "../src/lib/approvals"; +import type { SafetyAssessment } from "../src/approvals"; -import { canAutoApprove } from "../src/lib/approvals"; +import { canAutoApprove } from "../src/approvals"; import { describe, test, expect } from "vitest"; describe("canAutoApprove()", () => { diff --git a/codex-cli/tests/external-editor.test.ts b/codex-cli/tests/external-editor.test.ts index d530be5ecc..77041c2870 100644 --- a/codex-cli/tests/external-editor.test.ts +++ b/codex-cli/tests/external-editor.test.ts @@ -1,4 +1,4 @@ -import TextBuffer from "../src/lib/text-buffer"; +import TextBuffer from "../src/text-buffer"; import { describe, it, expect, vi } from "vitest"; /* ------------------------------------------------------------------------- diff --git a/codex-cli/tests/format-command.test.ts b/codex-cli/tests/format-command.test.ts index 3981d9b106..7de417308a 100644 --- a/codex-cli/tests/format-command.test.ts +++ b/codex-cli/tests/format-command.test.ts @@ -1,4 +1,4 @@ -import { formatCommandForDisplay } from "../src/lib/format-command"; +import { formatCommandForDisplay } from "../src/format-command"; import { describe, test, expect } from "vitest"; describe("formatCommandForDisplay()", () => { diff --git a/codex-cli/tests/invalid-command-handling.test.ts b/codex-cli/tests/invalid-command-handling.test.ts index 6619b4f240..a3f87a7251 100644 --- a/codex-cli/tests/invalid-command-handling.test.ts +++ b/codex-cli/tests/invalid-command-handling.test.ts @@ -22,7 +22,7 @@ describe("rawExec – invalid command handling", () => { // --------------------------------------------------------------------------- // Mock approvals and logging helpers so the test focuses on execution flow. -vi.mock("@lib/approvals.js", () => { +vi.mock("../src/approvals.js", () => { return { __esModule: true, canAutoApprove: () => @@ -31,7 +31,7 @@ vi.mock("@lib/approvals.js", () => { }; }); -vi.mock("@lib/format-command.js", () => { +vi.mock("../src/format-command.js", () => { return { __esModule: true, formatCommandForDisplay: (cmd: Array) => cmd.join(" "), diff --git a/codex-cli/tests/multiline-external-editor-shortcut.test.tsx b/codex-cli/tests/multiline-external-editor-shortcut.test.tsx index 158a5e655d..9b2e2f25e5 100644 --- a/codex-cli/tests/multiline-external-editor-shortcut.test.tsx +++ b/codex-cli/tests/multiline-external-editor-shortcut.test.tsx @@ -1,6 +1,6 @@ import { renderTui } from "./ui-test-helpers.js"; import MultilineTextEditor from "../src/components/chat/multiline-editor.js"; -import TextBuffer from "../src/lib/text-buffer.js"; +import TextBuffer from "../src/text-buffer.js"; import * as React from "react"; import { describe, it, expect, vi } from "vitest"; diff --git a/codex-cli/tests/multiline-history-behavior.test.tsx b/codex-cli/tests/multiline-history-behavior.test.tsx index 5c906837ee..cada52ddab 100644 --- a/codex-cli/tests/multiline-history-behavior.test.tsx +++ b/codex-cli/tests/multiline-history-behavior.test.tsx @@ -34,12 +34,12 @@ vi.mock("../src/utils/input-utils.js", () => ({ })), })); -// Mock the optional @lib/* dependencies so the dynamic import in parsers.ts +// Mock the optional ../src/* dependencies so the dynamic import in parsers.ts // does not fail during the test environment where the alias isn't configured. -vi.mock("@lib/format-command.js", () => ({ +vi.mock("../src/format-command.js", () => ({ formatCommandForDisplay: (cmd: Array) => cmd.join(" "), })); -vi.mock("@lib/approvals.js", () => ({ +vi.mock("../src/approvals.js", () => ({ isSafeCommand: (_cmd: Array) => null, })); diff --git a/codex-cli/tests/parse-apply-patch.test.ts b/codex-cli/tests/parse-apply-patch.test.ts index 0195542e56..53aa119093 100644 --- a/codex-cli/tests/parse-apply-patch.test.ts +++ b/codex-cli/tests/parse-apply-patch.test.ts @@ -1,4 +1,4 @@ -import { parseApplyPatch } from "../src/lib/parse-apply-patch"; +import { parseApplyPatch } from "../src/parse-apply-patch"; import { expect, test, describe } from "vitest"; // Helper function to unwrap a non‑null result in tests that expect success. diff --git a/codex-cli/tests/text-buffer-copy-paste.test.ts b/codex-cli/tests/text-buffer-copy-paste.test.ts index 311b2b9aa6..cc1fd119e5 100644 --- a/codex-cli/tests/text-buffer-copy-paste.test.ts +++ b/codex-cli/tests/text-buffer-copy-paste.test.ts @@ -1,4 +1,4 @@ -import TextBuffer from "../src/lib/text-buffer.js"; +import TextBuffer from "../src/text-buffer.js"; import { describe, it, expect } from "vitest"; // These tests ensure that the TextBuffer copy‑&‑paste logic keeps parity with diff --git a/codex-cli/tests/text-buffer-crlf.test.ts b/codex-cli/tests/text-buffer-crlf.test.ts index 4b33b498b5..736c22a27d 100644 --- a/codex-cli/tests/text-buffer-crlf.test.ts +++ b/codex-cli/tests/text-buffer-crlf.test.ts @@ -1,4 +1,4 @@ -import TextBuffer from "../src/lib/text-buffer.js"; +import TextBuffer from "../src/text-buffer.js"; import { describe, it, expect } from "vitest"; describe("TextBuffer – newline normalisation", () => { diff --git a/codex-cli/tests/text-buffer-gaps.test.ts b/codex-cli/tests/text-buffer-gaps.test.ts index 986ad37eed..046d468e8a 100644 --- a/codex-cli/tests/text-buffer-gaps.test.ts +++ b/codex-cli/tests/text-buffer-gaps.test.ts @@ -1,4 +1,4 @@ -import TextBuffer from "../src/lib/text-buffer"; +import TextBuffer from "../src/text-buffer"; import { describe, it, expect } from "vitest"; // The purpose of this test‑suite is NOT to make the implementation green today diff --git a/codex-cli/tests/text-buffer-word.test.ts b/codex-cli/tests/text-buffer-word.test.ts index 009786a28c..4ea7679450 100644 --- a/codex-cli/tests/text-buffer-word.test.ts +++ b/codex-cli/tests/text-buffer-word.test.ts @@ -1,4 +1,4 @@ -import TextBuffer from "../src/lib/text-buffer.js"; +import TextBuffer from "../src/text-buffer.js"; import { describe, test, expect } from "vitest"; describe("TextBuffer – word‑wise navigation & deletion", () => { diff --git a/codex-cli/tests/text-buffer.test.ts b/codex-cli/tests/text-buffer.test.ts index ae78f29409..c3f33d0fa1 100644 --- a/codex-cli/tests/text-buffer.test.ts +++ b/codex-cli/tests/text-buffer.test.ts @@ -1,4 +1,4 @@ -import TextBuffer from "../src/lib/text-buffer"; +import TextBuffer from "../src/text-buffer"; import { describe, it, expect } from "vitest"; describe("TextBuffer – basic editing parity with Rust suite", () => { diff --git a/codex-cli/tsconfig.json b/codex-cli/tsconfig.json index 626fc5dbf4..d1dacc9149 100644 --- a/codex-cli/tsconfig.json +++ b/codex-cli/tsconfig.json @@ -11,9 +11,6 @@ ], "types": ["node"], "baseUrl": "./", - "paths": { - "@lib/*": ["./src/lib/*"] - }, "resolveJsonModule": false, // ESM doesn't yet support JSON modules. "jsx": "react", "declaration": true, From 54ba90c4b80cc820ca3267e93a2dd8dbf681c52f Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Wed, 16 Apr 2025 15:09:12 -0700 Subject: [PATCH 04/41] Removes computeAutoApproval() and tightens up canAutoApprove() as the source of truth --- codex-cli/src/approvals.ts | 127 ++++++++++-------- .../components/chat/use-message-grouping.ts | 72 ---------- codex-cli/src/utils/agent/review.ts | 8 -- codex-cli/src/utils/parsers.ts | 111 ++------------- 4 files changed, 80 insertions(+), 238 deletions(-) diff --git a/codex-cli/src/approvals.ts b/codex-cli/src/approvals.ts index 0cf3703b54..8a670b01ca 100644 --- a/codex-cli/src/approvals.ts +++ b/codex-cli/src/approvals.ts @@ -75,19 +75,72 @@ export function canAutoApprove( writableRoots: ReadonlyArray, env: NodeJS.ProcessEnv = process.env, ): SafetyAssessment { - try { - if (command[0] === "apply_patch") { - return command.length === 2 && typeof command[1] === "string" - ? canAutoApproveApplyPatch(command[1], writableRoots, policy) - : { - type: "reject", - reason: "Invalid apply_patch command", - }; + if (command[0] === "apply_patch") { + return command.length === 2 && typeof command[1] === "string" + ? canAutoApproveApplyPatch(command[1], writableRoots, policy) + : { + type: "reject", + reason: "Invalid apply_patch command", + }; + } + + const isSafe = isSafeCommand(command); + if (isSafe != null) { + const { reason, group } = isSafe; + return { + type: "auto-approve", + reason, + group, + runInSandbox: false, + }; + } + + if ( + command[0] === "bash" && + command[1] === "-lc" && + typeof command[2] === "string" && + command.length === 3 + ) { + const applyPatchArg = tryParseApplyPatch(command[2]); + if (applyPatchArg != null) { + return canAutoApproveApplyPatch(applyPatchArg, writableRoots, policy); } - const isSafe = isSafeCommand(command); - if (isSafe != null) { - const { reason, group } = isSafe; + let bashCmd; + try { + bashCmd = parse(command[2], env); + } catch (e) { + // In practice, there seem to be syntactically valid shell commands that + // shell-quote cannot parse, so we should not reject, but ask the user. + switch (policy) { + case "full-auto": + // In full-auto, we still run the command automatically, but must + // restrict it to the sandbox. + return { + type: "auto-approve", + reason: "Full auto mode", + group: "Running commands", + runInSandbox: true, + }; + case "suggest": + case "auto-edit": + // In all other modes, since we cannot reason about the command, we + // should ask the user. + return { + type: "ask-user", + }; + } + } + + // bashCmd could be a mix of strings and operators, e.g.: + // "ls || (true && pwd)" => [ 'ls', { op: '||' }, '(', 'true', { op: '&&' }, 'pwd', ')' ] + // We try to ensure that *every* command segment is deemed safe and that + // all operators belong to an allow‑list. If so, the entire expression is + // considered auto‑approvable. + + const shellSafe = isEntireShellExpressionSafe(bashCmd); + if (shellSafe != null) { + const { reason, group } = shellSafe; return { type: "auto-approve", reason, @@ -95,58 +148,16 @@ export function canAutoApprove( runInSandbox: false, }; } + } - if ( - command[0] === "bash" && - command[1] === "-lc" && - typeof command[2] === "string" && - command.length === 3 - ) { - const applyPatchArg = tryParseApplyPatch(command[2]); - if (applyPatchArg != null) { - return canAutoApproveApplyPatch(applyPatchArg, writableRoots, policy); - } - - const bashCmd = parse(command[2], env); - - // bashCmd could be a mix of strings and operators, e.g.: - // "ls || (true && pwd)" => [ 'ls', { op: '||' }, '(', 'true', { op: '&&' }, 'pwd', ')' ] - // We try to ensure that *every* command segment is deemed safe and that - // all operators belong to an allow‑list. If so, the entire expression is - // considered auto‑approvable. - - const shellSafe = isEntireShellExpressionSafe(bashCmd); - if (shellSafe != null) { - const { reason, group } = shellSafe; - return { - type: "auto-approve", - reason, - group, - runInSandbox: false, - }; - } - } - - return policy === "full-auto" - ? { - type: "auto-approve", - reason: "Full auto mode", - group: "Running commands", - runInSandbox: true, - } - : { type: "ask-user" }; - } catch (err) { - if (policy === "full-auto") { - return { + return policy === "full-auto" + ? { type: "auto-approve", reason: "Full auto mode", group: "Running commands", runInSandbox: true, - }; - } else { - return { type: "ask-user" }; - } - } + } + : { type: "ask-user" }; } function canAutoApproveApplyPatch( diff --git a/codex-cli/src/components/chat/use-message-grouping.ts b/codex-cli/src/components/chat/use-message-grouping.ts index 75e51ac682..1e526821d0 100644 --- a/codex-cli/src/components/chat/use-message-grouping.ts +++ b/codex-cli/src/components/chat/use-message-grouping.ts @@ -1,8 +1,5 @@ import type { ResponseItem } from "openai/resources/responses/responses.mjs"; -import { parseToolCall } from "../../utils/parsers.js"; -import { useMemo } from "react"; - /** * Represents a grouped sequence of response items (e.g., function call batches). */ @@ -10,72 +7,3 @@ export type GroupedResponseItem = { label: string; items: Array; }; - -/** - * Custom hook to group recent response items for display batching. - * Returns counts of auto-approved tool call groups, the latest batch, - * and the count of user messages in the visible window. - */ -export function useMessageGrouping(visibleItems: Array): { - groupCounts: Record; - batch: Array<{ item?: ResponseItem; group?: GroupedResponseItem }>; - userMsgCount: number; -} { - return useMemo(() => { - // The grouping logic only depends on the subset of messages that are - // currently rendered (visibleItems). Using that as the sole dependency - // keeps recomputations to a minimum and avoids unnecessary work when the - // full list of `items` changes outside of the visible window. - let userMsgCount = 0; - const groupCounts: Record = {}; - visibleItems.forEach((m) => { - if (m.type === "function_call") { - const toolCall = parseToolCall(m); - if (toolCall?.autoApproval) { - const group = toolCall.autoApproval.group; - groupCounts[group] = (groupCounts[group] || 0) + 1; - } - } - if (m.type === "message" && m.role === "user") { - userMsgCount++; - } - }); - const lastFew = visibleItems.slice(-3); - const batch: Array<{ item?: ResponseItem; group?: GroupedResponseItem }> = - []; - if (lastFew[0]?.type === "function_call") { - const toolCall = parseToolCall(lastFew[0]); - batch.push({ - group: { - label: toolCall?.autoApproval?.group || "Running command", - items: lastFew, - }, - }); - if (lastFew[2]?.type === "message") { - batch.push({ item: lastFew[2] }); - } - } else if (lastFew[1]?.type === "function_call") { - const toolCall = parseToolCall(lastFew[1]); - batch.push({ - group: { - label: toolCall?.autoApproval?.group || "Running command", - items: lastFew.slice(1), - }, - }); - } else if (lastFew[2]?.type === "function_call") { - const toolCall = parseToolCall(lastFew[2]); - batch.push({ - group: { - label: toolCall?.autoApproval?.group || "Running command", - items: [lastFew[2]], - }, - }); - } else { - lastFew.forEach((item) => batch.push({ item })); - } - return { groupCounts, batch, userMsgCount }; - // `items` is stable across renders while `visibleItems` changes based on - // the scroll window. Including only `visibleItems` avoids unnecessary - // recomputations while still producing correct results. - }, [visibleItems]); -} diff --git a/codex-cli/src/utils/agent/review.ts b/codex-cli/src/utils/agent/review.ts index a370388569..9a5e66a4ff 100644 --- a/codex-cli/src/utils/agent/review.ts +++ b/codex-cli/src/utils/agent/review.ts @@ -1,11 +1,3 @@ -import type { SafeCommandReason } from "../../approvals"; - -export type CommandReviewDetails = { - cmd: Array; - cmdReadableText: string; - autoApproval: SafeCommandReason | null; -}; - export enum ReviewDecision { YES = "yes", NO_CONTINUE = "no-continue", diff --git a/codex-cli/src/utils/parsers.ts b/codex-cli/src/utils/parsers.ts index 815e7b2f00..e46db3b34d 100644 --- a/codex-cli/src/utils/parsers.ts +++ b/codex-cli/src/utils/parsers.ts @@ -5,26 +5,13 @@ import type { } from "./agent/sandbox/interface.js"; import type { ResponseFunctionToolCall } from "openai/resources/responses/responses.mjs"; -import { isSafeCommand, type SafeCommandReason } from "../approvals.js"; import { log } from "node:console"; -import process from "process"; -import { parse } from "shell-quote"; import { formatCommandForDisplay } from "src/format-command.js"; // The console utility import is intentionally explicit to avoid bundlers from // including the entire `console` module when only the `log` function is // required. -// Allowed shell operators that we consider "safe" as they do not introduce -// side‑effects on their own (unlike redirections). Parentheses and braces for -// grouping are excluded for simplicity. -const SAFE_SHELL_OPERATORS: ReadonlySet = new Set([ - "&&", - "||", - "|", - ";", -]); - export function parseToolCallOutput(toolCallOutput: string): { output: string; metadata: ExecOutputMetadata; @@ -46,6 +33,17 @@ export function parseToolCallOutput(toolCallOutput: string): { } } +export type CommandReviewDetails = { + cmd: Array; + cmdReadableText: string; +}; + +/** + * Tries to parse a tool call and, if successful, returns an object that has + * both: + * - an array of strings to use with `ExecInput` and `canAutoApprove()` + * - a human-readable string to display to the user + */ export function parseToolCall( toolCall: ResponseFunctionToolCall, ): CommandReviewDetails | undefined { @@ -57,12 +55,9 @@ export function parseToolCall( const { cmd } = toolCallArgs; const cmdReadableText = formatCommandForDisplay(cmd); - const autoApproval = computeAutoApproval(cmd); - return { cmd, cmdReadableText, - autoApproval, }; } @@ -109,87 +104,3 @@ function toStringArray(obj: unknown): Array | undefined { return undefined; } } - -// ---------------- safe‑command helpers ---------------- - -/** - * Attempts to determine whether `cmd` is composed exclusively of safe - * sub‑commands combined using only operators from the SAFE_SHELL_OPERATORS - * allow‑list. Returns the `SafeCommandReason` (taken from the first sub‑command) - * if the whole expression is safe; otherwise returns `null`. - */ -function computeAutoApproval(cmd: Array): SafeCommandReason | null { - // Fast path: a simple command with no shell processing. - const direct = isSafeCommand(cmd); - if (direct != null) { - return direct; - } - - // For expressions like ["bash", "-lc", "ls && pwd"] break down the inner - // string and verify each segment. - if ( - cmd.length === 3 && - cmd[0] === "bash" && - cmd[1] === "-lc" && - typeof cmd[2] === "string" - ) { - const parsed = parse(cmd[2], process.env ?? {}); - if (parsed.length === 0) { - return null; - } - - let current: Array = []; - let first: SafeCommandReason | null = null; - - const flush = (): boolean => { - if (current.length === 0) { - return true; - } - const safe = isSafeCommand(current); - if (safe == null) { - return false; - } - if (!first) { - first = safe; - } - current = []; - return true; - }; - - for (const part of parsed) { - if (typeof part === "string") { - // Simple word/argument token. - if (part === "(" || part === ")" || part === "{" || part === "}") { - // We treat explicit grouping tokens as unsafe because their - // semantics depend on the shell evaluation environment. - return null; - } - current.push(part); - } else if (part && typeof part === "object") { - const opToken = part as { op?: string }; - if (typeof opToken.op === "string") { - if (!flush()) { - return null; - } - if (!SAFE_SHELL_OPERATORS.has(opToken.op)) { - return null; - } - } else { - // Unknown object token kind (e.g. redirection) – treat as unsafe. - return null; - } - } else { - // Token types such as numbers / booleans are unexpected – treat as unsafe. - return null; - } - } - - if (!flush()) { - return null; - } - - return first; - } - - return null; -} From c56f84c5f59ddd36cb92a46bf184773f1bc5c318 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Wed, 16 Apr 2025 15:09:12 -0700 Subject: [PATCH 05/41] Removes computeAutoApproval() and tightens up canAutoApprove() as the source of truth --- codex-cli/src/approvals.ts | 127 ++++++++++-------- .../components/chat/use-message-grouping.ts | 72 ---------- codex-cli/src/utils/agent/review.ts | 8 -- codex-cli/src/utils/parsers.ts | 112 ++------------- 4 files changed, 80 insertions(+), 239 deletions(-) diff --git a/codex-cli/src/approvals.ts b/codex-cli/src/approvals.ts index 0cf3703b54..8a670b01ca 100644 --- a/codex-cli/src/approvals.ts +++ b/codex-cli/src/approvals.ts @@ -75,19 +75,72 @@ export function canAutoApprove( writableRoots: ReadonlyArray, env: NodeJS.ProcessEnv = process.env, ): SafetyAssessment { - try { - if (command[0] === "apply_patch") { - return command.length === 2 && typeof command[1] === "string" - ? canAutoApproveApplyPatch(command[1], writableRoots, policy) - : { - type: "reject", - reason: "Invalid apply_patch command", - }; + if (command[0] === "apply_patch") { + return command.length === 2 && typeof command[1] === "string" + ? canAutoApproveApplyPatch(command[1], writableRoots, policy) + : { + type: "reject", + reason: "Invalid apply_patch command", + }; + } + + const isSafe = isSafeCommand(command); + if (isSafe != null) { + const { reason, group } = isSafe; + return { + type: "auto-approve", + reason, + group, + runInSandbox: false, + }; + } + + if ( + command[0] === "bash" && + command[1] === "-lc" && + typeof command[2] === "string" && + command.length === 3 + ) { + const applyPatchArg = tryParseApplyPatch(command[2]); + if (applyPatchArg != null) { + return canAutoApproveApplyPatch(applyPatchArg, writableRoots, policy); } - const isSafe = isSafeCommand(command); - if (isSafe != null) { - const { reason, group } = isSafe; + let bashCmd; + try { + bashCmd = parse(command[2], env); + } catch (e) { + // In practice, there seem to be syntactically valid shell commands that + // shell-quote cannot parse, so we should not reject, but ask the user. + switch (policy) { + case "full-auto": + // In full-auto, we still run the command automatically, but must + // restrict it to the sandbox. + return { + type: "auto-approve", + reason: "Full auto mode", + group: "Running commands", + runInSandbox: true, + }; + case "suggest": + case "auto-edit": + // In all other modes, since we cannot reason about the command, we + // should ask the user. + return { + type: "ask-user", + }; + } + } + + // bashCmd could be a mix of strings and operators, e.g.: + // "ls || (true && pwd)" => [ 'ls', { op: '||' }, '(', 'true', { op: '&&' }, 'pwd', ')' ] + // We try to ensure that *every* command segment is deemed safe and that + // all operators belong to an allow‑list. If so, the entire expression is + // considered auto‑approvable. + + const shellSafe = isEntireShellExpressionSafe(bashCmd); + if (shellSafe != null) { + const { reason, group } = shellSafe; return { type: "auto-approve", reason, @@ -95,58 +148,16 @@ export function canAutoApprove( runInSandbox: false, }; } + } - if ( - command[0] === "bash" && - command[1] === "-lc" && - typeof command[2] === "string" && - command.length === 3 - ) { - const applyPatchArg = tryParseApplyPatch(command[2]); - if (applyPatchArg != null) { - return canAutoApproveApplyPatch(applyPatchArg, writableRoots, policy); - } - - const bashCmd = parse(command[2], env); - - // bashCmd could be a mix of strings and operators, e.g.: - // "ls || (true && pwd)" => [ 'ls', { op: '||' }, '(', 'true', { op: '&&' }, 'pwd', ')' ] - // We try to ensure that *every* command segment is deemed safe and that - // all operators belong to an allow‑list. If so, the entire expression is - // considered auto‑approvable. - - const shellSafe = isEntireShellExpressionSafe(bashCmd); - if (shellSafe != null) { - const { reason, group } = shellSafe; - return { - type: "auto-approve", - reason, - group, - runInSandbox: false, - }; - } - } - - return policy === "full-auto" - ? { - type: "auto-approve", - reason: "Full auto mode", - group: "Running commands", - runInSandbox: true, - } - : { type: "ask-user" }; - } catch (err) { - if (policy === "full-auto") { - return { + return policy === "full-auto" + ? { type: "auto-approve", reason: "Full auto mode", group: "Running commands", runInSandbox: true, - }; - } else { - return { type: "ask-user" }; - } - } + } + : { type: "ask-user" }; } function canAutoApproveApplyPatch( diff --git a/codex-cli/src/components/chat/use-message-grouping.ts b/codex-cli/src/components/chat/use-message-grouping.ts index 75e51ac682..1e526821d0 100644 --- a/codex-cli/src/components/chat/use-message-grouping.ts +++ b/codex-cli/src/components/chat/use-message-grouping.ts @@ -1,8 +1,5 @@ import type { ResponseItem } from "openai/resources/responses/responses.mjs"; -import { parseToolCall } from "../../utils/parsers.js"; -import { useMemo } from "react"; - /** * Represents a grouped sequence of response items (e.g., function call batches). */ @@ -10,72 +7,3 @@ export type GroupedResponseItem = { label: string; items: Array; }; - -/** - * Custom hook to group recent response items for display batching. - * Returns counts of auto-approved tool call groups, the latest batch, - * and the count of user messages in the visible window. - */ -export function useMessageGrouping(visibleItems: Array): { - groupCounts: Record; - batch: Array<{ item?: ResponseItem; group?: GroupedResponseItem }>; - userMsgCount: number; -} { - return useMemo(() => { - // The grouping logic only depends on the subset of messages that are - // currently rendered (visibleItems). Using that as the sole dependency - // keeps recomputations to a minimum and avoids unnecessary work when the - // full list of `items` changes outside of the visible window. - let userMsgCount = 0; - const groupCounts: Record = {}; - visibleItems.forEach((m) => { - if (m.type === "function_call") { - const toolCall = parseToolCall(m); - if (toolCall?.autoApproval) { - const group = toolCall.autoApproval.group; - groupCounts[group] = (groupCounts[group] || 0) + 1; - } - } - if (m.type === "message" && m.role === "user") { - userMsgCount++; - } - }); - const lastFew = visibleItems.slice(-3); - const batch: Array<{ item?: ResponseItem; group?: GroupedResponseItem }> = - []; - if (lastFew[0]?.type === "function_call") { - const toolCall = parseToolCall(lastFew[0]); - batch.push({ - group: { - label: toolCall?.autoApproval?.group || "Running command", - items: lastFew, - }, - }); - if (lastFew[2]?.type === "message") { - batch.push({ item: lastFew[2] }); - } - } else if (lastFew[1]?.type === "function_call") { - const toolCall = parseToolCall(lastFew[1]); - batch.push({ - group: { - label: toolCall?.autoApproval?.group || "Running command", - items: lastFew.slice(1), - }, - }); - } else if (lastFew[2]?.type === "function_call") { - const toolCall = parseToolCall(lastFew[2]); - batch.push({ - group: { - label: toolCall?.autoApproval?.group || "Running command", - items: [lastFew[2]], - }, - }); - } else { - lastFew.forEach((item) => batch.push({ item })); - } - return { groupCounts, batch, userMsgCount }; - // `items` is stable across renders while `visibleItems` changes based on - // the scroll window. Including only `visibleItems` avoids unnecessary - // recomputations while still producing correct results. - }, [visibleItems]); -} diff --git a/codex-cli/src/utils/agent/review.ts b/codex-cli/src/utils/agent/review.ts index a370388569..9a5e66a4ff 100644 --- a/codex-cli/src/utils/agent/review.ts +++ b/codex-cli/src/utils/agent/review.ts @@ -1,11 +1,3 @@ -import type { SafeCommandReason } from "../../approvals"; - -export type CommandReviewDetails = { - cmd: Array; - cmdReadableText: string; - autoApproval: SafeCommandReason | null; -}; - export enum ReviewDecision { YES = "yes", NO_CONTINUE = "no-continue", diff --git a/codex-cli/src/utils/parsers.ts b/codex-cli/src/utils/parsers.ts index 815e7b2f00..4461379c02 100644 --- a/codex-cli/src/utils/parsers.ts +++ b/codex-cli/src/utils/parsers.ts @@ -1,30 +1,16 @@ -import type { CommandReviewDetails } from "./agent/review.js"; import type { ExecInput, ExecOutputMetadata, } from "./agent/sandbox/interface.js"; import type { ResponseFunctionToolCall } from "openai/resources/responses/responses.mjs"; -import { isSafeCommand, type SafeCommandReason } from "../approvals.js"; import { log } from "node:console"; -import process from "process"; -import { parse } from "shell-quote"; import { formatCommandForDisplay } from "src/format-command.js"; // The console utility import is intentionally explicit to avoid bundlers from // including the entire `console` module when only the `log` function is // required. -// Allowed shell operators that we consider "safe" as they do not introduce -// side‑effects on their own (unlike redirections). Parentheses and braces for -// grouping are excluded for simplicity. -const SAFE_SHELL_OPERATORS: ReadonlySet = new Set([ - "&&", - "||", - "|", - ";", -]); - export function parseToolCallOutput(toolCallOutput: string): { output: string; metadata: ExecOutputMetadata; @@ -46,6 +32,17 @@ export function parseToolCallOutput(toolCallOutput: string): { } } +export type CommandReviewDetails = { + cmd: Array; + cmdReadableText: string; +}; + +/** + * Tries to parse a tool call and, if successful, returns an object that has + * both: + * - an array of strings to use with `ExecInput` and `canAutoApprove()` + * - a human-readable string to display to the user + */ export function parseToolCall( toolCall: ResponseFunctionToolCall, ): CommandReviewDetails | undefined { @@ -57,12 +54,9 @@ export function parseToolCall( const { cmd } = toolCallArgs; const cmdReadableText = formatCommandForDisplay(cmd); - const autoApproval = computeAutoApproval(cmd); - return { cmd, cmdReadableText, - autoApproval, }; } @@ -109,87 +103,3 @@ function toStringArray(obj: unknown): Array | undefined { return undefined; } } - -// ---------------- safe‑command helpers ---------------- - -/** - * Attempts to determine whether `cmd` is composed exclusively of safe - * sub‑commands combined using only operators from the SAFE_SHELL_OPERATORS - * allow‑list. Returns the `SafeCommandReason` (taken from the first sub‑command) - * if the whole expression is safe; otherwise returns `null`. - */ -function computeAutoApproval(cmd: Array): SafeCommandReason | null { - // Fast path: a simple command with no shell processing. - const direct = isSafeCommand(cmd); - if (direct != null) { - return direct; - } - - // For expressions like ["bash", "-lc", "ls && pwd"] break down the inner - // string and verify each segment. - if ( - cmd.length === 3 && - cmd[0] === "bash" && - cmd[1] === "-lc" && - typeof cmd[2] === "string" - ) { - const parsed = parse(cmd[2], process.env ?? {}); - if (parsed.length === 0) { - return null; - } - - let current: Array = []; - let first: SafeCommandReason | null = null; - - const flush = (): boolean => { - if (current.length === 0) { - return true; - } - const safe = isSafeCommand(current); - if (safe == null) { - return false; - } - if (!first) { - first = safe; - } - current = []; - return true; - }; - - for (const part of parsed) { - if (typeof part === "string") { - // Simple word/argument token. - if (part === "(" || part === ")" || part === "{" || part === "}") { - // We treat explicit grouping tokens as unsafe because their - // semantics depend on the shell evaluation environment. - return null; - } - current.push(part); - } else if (part && typeof part === "object") { - const opToken = part as { op?: string }; - if (typeof opToken.op === "string") { - if (!flush()) { - return null; - } - if (!SAFE_SHELL_OPERATORS.has(opToken.op)) { - return null; - } - } else { - // Unknown object token kind (e.g. redirection) – treat as unsafe. - return null; - } - } else { - // Token types such as numbers / booleans are unexpected – treat as unsafe. - return null; - } - } - - if (!flush()) { - return null; - } - - return first; - } - - return null; -} From db9c62b85d40fd1fc44cadc0b3337da2c3e12c17 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Wed, 16 Apr 2025 15:14:02 -0700 Subject: [PATCH 06/41] Removes computeAutoApproval() and tightens up canAutoApprove() as the source of truth --- codex-cli/src/approvals.ts | 127 ++++++++++-------- .../components/chat/use-message-grouping.ts | 72 ---------- codex-cli/src/utils/agent/review.ts | 8 -- codex-cli/src/utils/parsers.ts | 112 ++------------- 4 files changed, 80 insertions(+), 239 deletions(-) diff --git a/codex-cli/src/approvals.ts b/codex-cli/src/approvals.ts index 0cf3703b54..8a670b01ca 100644 --- a/codex-cli/src/approvals.ts +++ b/codex-cli/src/approvals.ts @@ -75,19 +75,72 @@ export function canAutoApprove( writableRoots: ReadonlyArray, env: NodeJS.ProcessEnv = process.env, ): SafetyAssessment { - try { - if (command[0] === "apply_patch") { - return command.length === 2 && typeof command[1] === "string" - ? canAutoApproveApplyPatch(command[1], writableRoots, policy) - : { - type: "reject", - reason: "Invalid apply_patch command", - }; + if (command[0] === "apply_patch") { + return command.length === 2 && typeof command[1] === "string" + ? canAutoApproveApplyPatch(command[1], writableRoots, policy) + : { + type: "reject", + reason: "Invalid apply_patch command", + }; + } + + const isSafe = isSafeCommand(command); + if (isSafe != null) { + const { reason, group } = isSafe; + return { + type: "auto-approve", + reason, + group, + runInSandbox: false, + }; + } + + if ( + command[0] === "bash" && + command[1] === "-lc" && + typeof command[2] === "string" && + command.length === 3 + ) { + const applyPatchArg = tryParseApplyPatch(command[2]); + if (applyPatchArg != null) { + return canAutoApproveApplyPatch(applyPatchArg, writableRoots, policy); } - const isSafe = isSafeCommand(command); - if (isSafe != null) { - const { reason, group } = isSafe; + let bashCmd; + try { + bashCmd = parse(command[2], env); + } catch (e) { + // In practice, there seem to be syntactically valid shell commands that + // shell-quote cannot parse, so we should not reject, but ask the user. + switch (policy) { + case "full-auto": + // In full-auto, we still run the command automatically, but must + // restrict it to the sandbox. + return { + type: "auto-approve", + reason: "Full auto mode", + group: "Running commands", + runInSandbox: true, + }; + case "suggest": + case "auto-edit": + // In all other modes, since we cannot reason about the command, we + // should ask the user. + return { + type: "ask-user", + }; + } + } + + // bashCmd could be a mix of strings and operators, e.g.: + // "ls || (true && pwd)" => [ 'ls', { op: '||' }, '(', 'true', { op: '&&' }, 'pwd', ')' ] + // We try to ensure that *every* command segment is deemed safe and that + // all operators belong to an allow‑list. If so, the entire expression is + // considered auto‑approvable. + + const shellSafe = isEntireShellExpressionSafe(bashCmd); + if (shellSafe != null) { + const { reason, group } = shellSafe; return { type: "auto-approve", reason, @@ -95,58 +148,16 @@ export function canAutoApprove( runInSandbox: false, }; } + } - if ( - command[0] === "bash" && - command[1] === "-lc" && - typeof command[2] === "string" && - command.length === 3 - ) { - const applyPatchArg = tryParseApplyPatch(command[2]); - if (applyPatchArg != null) { - return canAutoApproveApplyPatch(applyPatchArg, writableRoots, policy); - } - - const bashCmd = parse(command[2], env); - - // bashCmd could be a mix of strings and operators, e.g.: - // "ls || (true && pwd)" => [ 'ls', { op: '||' }, '(', 'true', { op: '&&' }, 'pwd', ')' ] - // We try to ensure that *every* command segment is deemed safe and that - // all operators belong to an allow‑list. If so, the entire expression is - // considered auto‑approvable. - - const shellSafe = isEntireShellExpressionSafe(bashCmd); - if (shellSafe != null) { - const { reason, group } = shellSafe; - return { - type: "auto-approve", - reason, - group, - runInSandbox: false, - }; - } - } - - return policy === "full-auto" - ? { - type: "auto-approve", - reason: "Full auto mode", - group: "Running commands", - runInSandbox: true, - } - : { type: "ask-user" }; - } catch (err) { - if (policy === "full-auto") { - return { + return policy === "full-auto" + ? { type: "auto-approve", reason: "Full auto mode", group: "Running commands", runInSandbox: true, - }; - } else { - return { type: "ask-user" }; - } - } + } + : { type: "ask-user" }; } function canAutoApproveApplyPatch( diff --git a/codex-cli/src/components/chat/use-message-grouping.ts b/codex-cli/src/components/chat/use-message-grouping.ts index 75e51ac682..1e526821d0 100644 --- a/codex-cli/src/components/chat/use-message-grouping.ts +++ b/codex-cli/src/components/chat/use-message-grouping.ts @@ -1,8 +1,5 @@ import type { ResponseItem } from "openai/resources/responses/responses.mjs"; -import { parseToolCall } from "../../utils/parsers.js"; -import { useMemo } from "react"; - /** * Represents a grouped sequence of response items (e.g., function call batches). */ @@ -10,72 +7,3 @@ export type GroupedResponseItem = { label: string; items: Array; }; - -/** - * Custom hook to group recent response items for display batching. - * Returns counts of auto-approved tool call groups, the latest batch, - * and the count of user messages in the visible window. - */ -export function useMessageGrouping(visibleItems: Array): { - groupCounts: Record; - batch: Array<{ item?: ResponseItem; group?: GroupedResponseItem }>; - userMsgCount: number; -} { - return useMemo(() => { - // The grouping logic only depends on the subset of messages that are - // currently rendered (visibleItems). Using that as the sole dependency - // keeps recomputations to a minimum and avoids unnecessary work when the - // full list of `items` changes outside of the visible window. - let userMsgCount = 0; - const groupCounts: Record = {}; - visibleItems.forEach((m) => { - if (m.type === "function_call") { - const toolCall = parseToolCall(m); - if (toolCall?.autoApproval) { - const group = toolCall.autoApproval.group; - groupCounts[group] = (groupCounts[group] || 0) + 1; - } - } - if (m.type === "message" && m.role === "user") { - userMsgCount++; - } - }); - const lastFew = visibleItems.slice(-3); - const batch: Array<{ item?: ResponseItem; group?: GroupedResponseItem }> = - []; - if (lastFew[0]?.type === "function_call") { - const toolCall = parseToolCall(lastFew[0]); - batch.push({ - group: { - label: toolCall?.autoApproval?.group || "Running command", - items: lastFew, - }, - }); - if (lastFew[2]?.type === "message") { - batch.push({ item: lastFew[2] }); - } - } else if (lastFew[1]?.type === "function_call") { - const toolCall = parseToolCall(lastFew[1]); - batch.push({ - group: { - label: toolCall?.autoApproval?.group || "Running command", - items: lastFew.slice(1), - }, - }); - } else if (lastFew[2]?.type === "function_call") { - const toolCall = parseToolCall(lastFew[2]); - batch.push({ - group: { - label: toolCall?.autoApproval?.group || "Running command", - items: [lastFew[2]], - }, - }); - } else { - lastFew.forEach((item) => batch.push({ item })); - } - return { groupCounts, batch, userMsgCount }; - // `items` is stable across renders while `visibleItems` changes based on - // the scroll window. Including only `visibleItems` avoids unnecessary - // recomputations while still producing correct results. - }, [visibleItems]); -} diff --git a/codex-cli/src/utils/agent/review.ts b/codex-cli/src/utils/agent/review.ts index a370388569..9a5e66a4ff 100644 --- a/codex-cli/src/utils/agent/review.ts +++ b/codex-cli/src/utils/agent/review.ts @@ -1,11 +1,3 @@ -import type { SafeCommandReason } from "../../approvals"; - -export type CommandReviewDetails = { - cmd: Array; - cmdReadableText: string; - autoApproval: SafeCommandReason | null; -}; - export enum ReviewDecision { YES = "yes", NO_CONTINUE = "no-continue", diff --git a/codex-cli/src/utils/parsers.ts b/codex-cli/src/utils/parsers.ts index 815e7b2f00..4461379c02 100644 --- a/codex-cli/src/utils/parsers.ts +++ b/codex-cli/src/utils/parsers.ts @@ -1,30 +1,16 @@ -import type { CommandReviewDetails } from "./agent/review.js"; import type { ExecInput, ExecOutputMetadata, } from "./agent/sandbox/interface.js"; import type { ResponseFunctionToolCall } from "openai/resources/responses/responses.mjs"; -import { isSafeCommand, type SafeCommandReason } from "../approvals.js"; import { log } from "node:console"; -import process from "process"; -import { parse } from "shell-quote"; import { formatCommandForDisplay } from "src/format-command.js"; // The console utility import is intentionally explicit to avoid bundlers from // including the entire `console` module when only the `log` function is // required. -// Allowed shell operators that we consider "safe" as they do not introduce -// side‑effects on their own (unlike redirections). Parentheses and braces for -// grouping are excluded for simplicity. -const SAFE_SHELL_OPERATORS: ReadonlySet = new Set([ - "&&", - "||", - "|", - ";", -]); - export function parseToolCallOutput(toolCallOutput: string): { output: string; metadata: ExecOutputMetadata; @@ -46,6 +32,17 @@ export function parseToolCallOutput(toolCallOutput: string): { } } +export type CommandReviewDetails = { + cmd: Array; + cmdReadableText: string; +}; + +/** + * Tries to parse a tool call and, if successful, returns an object that has + * both: + * - an array of strings to use with `ExecInput` and `canAutoApprove()` + * - a human-readable string to display to the user + */ export function parseToolCall( toolCall: ResponseFunctionToolCall, ): CommandReviewDetails | undefined { @@ -57,12 +54,9 @@ export function parseToolCall( const { cmd } = toolCallArgs; const cmdReadableText = formatCommandForDisplay(cmd); - const autoApproval = computeAutoApproval(cmd); - return { cmd, cmdReadableText, - autoApproval, }; } @@ -109,87 +103,3 @@ function toStringArray(obj: unknown): Array | undefined { return undefined; } } - -// ---------------- safe‑command helpers ---------------- - -/** - * Attempts to determine whether `cmd` is composed exclusively of safe - * sub‑commands combined using only operators from the SAFE_SHELL_OPERATORS - * allow‑list. Returns the `SafeCommandReason` (taken from the first sub‑command) - * if the whole expression is safe; otherwise returns `null`. - */ -function computeAutoApproval(cmd: Array): SafeCommandReason | null { - // Fast path: a simple command with no shell processing. - const direct = isSafeCommand(cmd); - if (direct != null) { - return direct; - } - - // For expressions like ["bash", "-lc", "ls && pwd"] break down the inner - // string and verify each segment. - if ( - cmd.length === 3 && - cmd[0] === "bash" && - cmd[1] === "-lc" && - typeof cmd[2] === "string" - ) { - const parsed = parse(cmd[2], process.env ?? {}); - if (parsed.length === 0) { - return null; - } - - let current: Array = []; - let first: SafeCommandReason | null = null; - - const flush = (): boolean => { - if (current.length === 0) { - return true; - } - const safe = isSafeCommand(current); - if (safe == null) { - return false; - } - if (!first) { - first = safe; - } - current = []; - return true; - }; - - for (const part of parsed) { - if (typeof part === "string") { - // Simple word/argument token. - if (part === "(" || part === ")" || part === "{" || part === "}") { - // We treat explicit grouping tokens as unsafe because their - // semantics depend on the shell evaluation environment. - return null; - } - current.push(part); - } else if (part && typeof part === "object") { - const opToken = part as { op?: string }; - if (typeof opToken.op === "string") { - if (!flush()) { - return null; - } - if (!SAFE_SHELL_OPERATORS.has(opToken.op)) { - return null; - } - } else { - // Unknown object token kind (e.g. redirection) – treat as unsafe. - return null; - } - } else { - // Token types such as numbers / booleans are unexpected – treat as unsafe. - return null; - } - } - - if (!flush()) { - return null; - } - - return first; - } - - return null; -} From 7199511a34c1e11006263b5fca9b9cfb8a4fc172 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Wed, 16 Apr 2025 15:16:51 -0700 Subject: [PATCH 07/41] Removes computeAutoApproval() and tightens up canAutoApprove() as the source of truth Signed-off-by: Michael Bolin --- codex-cli/src/approvals.ts | 127 ++++++++++-------- .../components/chat/use-message-grouping.ts | 72 ---------- codex-cli/src/utils/agent/review.ts | 8 -- codex-cli/src/utils/parsers.ts | 112 ++------------- 4 files changed, 80 insertions(+), 239 deletions(-) diff --git a/codex-cli/src/approvals.ts b/codex-cli/src/approvals.ts index 0cf3703b54..8a670b01ca 100644 --- a/codex-cli/src/approvals.ts +++ b/codex-cli/src/approvals.ts @@ -75,19 +75,72 @@ export function canAutoApprove( writableRoots: ReadonlyArray, env: NodeJS.ProcessEnv = process.env, ): SafetyAssessment { - try { - if (command[0] === "apply_patch") { - return command.length === 2 && typeof command[1] === "string" - ? canAutoApproveApplyPatch(command[1], writableRoots, policy) - : { - type: "reject", - reason: "Invalid apply_patch command", - }; + if (command[0] === "apply_patch") { + return command.length === 2 && typeof command[1] === "string" + ? canAutoApproveApplyPatch(command[1], writableRoots, policy) + : { + type: "reject", + reason: "Invalid apply_patch command", + }; + } + + const isSafe = isSafeCommand(command); + if (isSafe != null) { + const { reason, group } = isSafe; + return { + type: "auto-approve", + reason, + group, + runInSandbox: false, + }; + } + + if ( + command[0] === "bash" && + command[1] === "-lc" && + typeof command[2] === "string" && + command.length === 3 + ) { + const applyPatchArg = tryParseApplyPatch(command[2]); + if (applyPatchArg != null) { + return canAutoApproveApplyPatch(applyPatchArg, writableRoots, policy); } - const isSafe = isSafeCommand(command); - if (isSafe != null) { - const { reason, group } = isSafe; + let bashCmd; + try { + bashCmd = parse(command[2], env); + } catch (e) { + // In practice, there seem to be syntactically valid shell commands that + // shell-quote cannot parse, so we should not reject, but ask the user. + switch (policy) { + case "full-auto": + // In full-auto, we still run the command automatically, but must + // restrict it to the sandbox. + return { + type: "auto-approve", + reason: "Full auto mode", + group: "Running commands", + runInSandbox: true, + }; + case "suggest": + case "auto-edit": + // In all other modes, since we cannot reason about the command, we + // should ask the user. + return { + type: "ask-user", + }; + } + } + + // bashCmd could be a mix of strings and operators, e.g.: + // "ls || (true && pwd)" => [ 'ls', { op: '||' }, '(', 'true', { op: '&&' }, 'pwd', ')' ] + // We try to ensure that *every* command segment is deemed safe and that + // all operators belong to an allow‑list. If so, the entire expression is + // considered auto‑approvable. + + const shellSafe = isEntireShellExpressionSafe(bashCmd); + if (shellSafe != null) { + const { reason, group } = shellSafe; return { type: "auto-approve", reason, @@ -95,58 +148,16 @@ export function canAutoApprove( runInSandbox: false, }; } + } - if ( - command[0] === "bash" && - command[1] === "-lc" && - typeof command[2] === "string" && - command.length === 3 - ) { - const applyPatchArg = tryParseApplyPatch(command[2]); - if (applyPatchArg != null) { - return canAutoApproveApplyPatch(applyPatchArg, writableRoots, policy); - } - - const bashCmd = parse(command[2], env); - - // bashCmd could be a mix of strings and operators, e.g.: - // "ls || (true && pwd)" => [ 'ls', { op: '||' }, '(', 'true', { op: '&&' }, 'pwd', ')' ] - // We try to ensure that *every* command segment is deemed safe and that - // all operators belong to an allow‑list. If so, the entire expression is - // considered auto‑approvable. - - const shellSafe = isEntireShellExpressionSafe(bashCmd); - if (shellSafe != null) { - const { reason, group } = shellSafe; - return { - type: "auto-approve", - reason, - group, - runInSandbox: false, - }; - } - } - - return policy === "full-auto" - ? { - type: "auto-approve", - reason: "Full auto mode", - group: "Running commands", - runInSandbox: true, - } - : { type: "ask-user" }; - } catch (err) { - if (policy === "full-auto") { - return { + return policy === "full-auto" + ? { type: "auto-approve", reason: "Full auto mode", group: "Running commands", runInSandbox: true, - }; - } else { - return { type: "ask-user" }; - } - } + } + : { type: "ask-user" }; } function canAutoApproveApplyPatch( diff --git a/codex-cli/src/components/chat/use-message-grouping.ts b/codex-cli/src/components/chat/use-message-grouping.ts index 75e51ac682..1e526821d0 100644 --- a/codex-cli/src/components/chat/use-message-grouping.ts +++ b/codex-cli/src/components/chat/use-message-grouping.ts @@ -1,8 +1,5 @@ import type { ResponseItem } from "openai/resources/responses/responses.mjs"; -import { parseToolCall } from "../../utils/parsers.js"; -import { useMemo } from "react"; - /** * Represents a grouped sequence of response items (e.g., function call batches). */ @@ -10,72 +7,3 @@ export type GroupedResponseItem = { label: string; items: Array; }; - -/** - * Custom hook to group recent response items for display batching. - * Returns counts of auto-approved tool call groups, the latest batch, - * and the count of user messages in the visible window. - */ -export function useMessageGrouping(visibleItems: Array): { - groupCounts: Record; - batch: Array<{ item?: ResponseItem; group?: GroupedResponseItem }>; - userMsgCount: number; -} { - return useMemo(() => { - // The grouping logic only depends on the subset of messages that are - // currently rendered (visibleItems). Using that as the sole dependency - // keeps recomputations to a minimum and avoids unnecessary work when the - // full list of `items` changes outside of the visible window. - let userMsgCount = 0; - const groupCounts: Record = {}; - visibleItems.forEach((m) => { - if (m.type === "function_call") { - const toolCall = parseToolCall(m); - if (toolCall?.autoApproval) { - const group = toolCall.autoApproval.group; - groupCounts[group] = (groupCounts[group] || 0) + 1; - } - } - if (m.type === "message" && m.role === "user") { - userMsgCount++; - } - }); - const lastFew = visibleItems.slice(-3); - const batch: Array<{ item?: ResponseItem; group?: GroupedResponseItem }> = - []; - if (lastFew[0]?.type === "function_call") { - const toolCall = parseToolCall(lastFew[0]); - batch.push({ - group: { - label: toolCall?.autoApproval?.group || "Running command", - items: lastFew, - }, - }); - if (lastFew[2]?.type === "message") { - batch.push({ item: lastFew[2] }); - } - } else if (lastFew[1]?.type === "function_call") { - const toolCall = parseToolCall(lastFew[1]); - batch.push({ - group: { - label: toolCall?.autoApproval?.group || "Running command", - items: lastFew.slice(1), - }, - }); - } else if (lastFew[2]?.type === "function_call") { - const toolCall = parseToolCall(lastFew[2]); - batch.push({ - group: { - label: toolCall?.autoApproval?.group || "Running command", - items: [lastFew[2]], - }, - }); - } else { - lastFew.forEach((item) => batch.push({ item })); - } - return { groupCounts, batch, userMsgCount }; - // `items` is stable across renders while `visibleItems` changes based on - // the scroll window. Including only `visibleItems` avoids unnecessary - // recomputations while still producing correct results. - }, [visibleItems]); -} diff --git a/codex-cli/src/utils/agent/review.ts b/codex-cli/src/utils/agent/review.ts index a370388569..9a5e66a4ff 100644 --- a/codex-cli/src/utils/agent/review.ts +++ b/codex-cli/src/utils/agent/review.ts @@ -1,11 +1,3 @@ -import type { SafeCommandReason } from "../../approvals"; - -export type CommandReviewDetails = { - cmd: Array; - cmdReadableText: string; - autoApproval: SafeCommandReason | null; -}; - export enum ReviewDecision { YES = "yes", NO_CONTINUE = "no-continue", diff --git a/codex-cli/src/utils/parsers.ts b/codex-cli/src/utils/parsers.ts index 815e7b2f00..4461379c02 100644 --- a/codex-cli/src/utils/parsers.ts +++ b/codex-cli/src/utils/parsers.ts @@ -1,30 +1,16 @@ -import type { CommandReviewDetails } from "./agent/review.js"; import type { ExecInput, ExecOutputMetadata, } from "./agent/sandbox/interface.js"; import type { ResponseFunctionToolCall } from "openai/resources/responses/responses.mjs"; -import { isSafeCommand, type SafeCommandReason } from "../approvals.js"; import { log } from "node:console"; -import process from "process"; -import { parse } from "shell-quote"; import { formatCommandForDisplay } from "src/format-command.js"; // The console utility import is intentionally explicit to avoid bundlers from // including the entire `console` module when only the `log` function is // required. -// Allowed shell operators that we consider "safe" as they do not introduce -// side‑effects on their own (unlike redirections). Parentheses and braces for -// grouping are excluded for simplicity. -const SAFE_SHELL_OPERATORS: ReadonlySet = new Set([ - "&&", - "||", - "|", - ";", -]); - export function parseToolCallOutput(toolCallOutput: string): { output: string; metadata: ExecOutputMetadata; @@ -46,6 +32,17 @@ export function parseToolCallOutput(toolCallOutput: string): { } } +export type CommandReviewDetails = { + cmd: Array; + cmdReadableText: string; +}; + +/** + * Tries to parse a tool call and, if successful, returns an object that has + * both: + * - an array of strings to use with `ExecInput` and `canAutoApprove()` + * - a human-readable string to display to the user + */ export function parseToolCall( toolCall: ResponseFunctionToolCall, ): CommandReviewDetails | undefined { @@ -57,12 +54,9 @@ export function parseToolCall( const { cmd } = toolCallArgs; const cmdReadableText = formatCommandForDisplay(cmd); - const autoApproval = computeAutoApproval(cmd); - return { cmd, cmdReadableText, - autoApproval, }; } @@ -109,87 +103,3 @@ function toStringArray(obj: unknown): Array | undefined { return undefined; } } - -// ---------------- safe‑command helpers ---------------- - -/** - * Attempts to determine whether `cmd` is composed exclusively of safe - * sub‑commands combined using only operators from the SAFE_SHELL_OPERATORS - * allow‑list. Returns the `SafeCommandReason` (taken from the first sub‑command) - * if the whole expression is safe; otherwise returns `null`. - */ -function computeAutoApproval(cmd: Array): SafeCommandReason | null { - // Fast path: a simple command with no shell processing. - const direct = isSafeCommand(cmd); - if (direct != null) { - return direct; - } - - // For expressions like ["bash", "-lc", "ls && pwd"] break down the inner - // string and verify each segment. - if ( - cmd.length === 3 && - cmd[0] === "bash" && - cmd[1] === "-lc" && - typeof cmd[2] === "string" - ) { - const parsed = parse(cmd[2], process.env ?? {}); - if (parsed.length === 0) { - return null; - } - - let current: Array = []; - let first: SafeCommandReason | null = null; - - const flush = (): boolean => { - if (current.length === 0) { - return true; - } - const safe = isSafeCommand(current); - if (safe == null) { - return false; - } - if (!first) { - first = safe; - } - current = []; - return true; - }; - - for (const part of parsed) { - if (typeof part === "string") { - // Simple word/argument token. - if (part === "(" || part === ")" || part === "{" || part === "}") { - // We treat explicit grouping tokens as unsafe because their - // semantics depend on the shell evaluation environment. - return null; - } - current.push(part); - } else if (part && typeof part === "object") { - const opToken = part as { op?: string }; - if (typeof opToken.op === "string") { - if (!flush()) { - return null; - } - if (!SAFE_SHELL_OPERATORS.has(opToken.op)) { - return null; - } - } else { - // Unknown object token kind (e.g. redirection) – treat as unsafe. - return null; - } - } else { - // Token types such as numbers / booleans are unexpected – treat as unsafe. - return null; - } - } - - if (!flush()) { - return null; - } - - return first; - } - - return null; -} From 08d209f8fa49242bfa7979b3861ff6302550b6f3 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Wed, 16 Apr 2025 15:36:25 -0700 Subject: [PATCH 08/41] Removes computeAutoApproval() and tightens up canAutoApprove() as the source of truth Signed-off-by: Michael Bolin --- codex-cli/src/approvals.ts | 127 ++++++++++-------- .../components/chat/use-message-grouping.ts | 72 ---------- codex-cli/src/utils/agent/review.ts | 8 -- codex-cli/src/utils/parsers.ts | 112 ++------------- 4 files changed, 80 insertions(+), 239 deletions(-) diff --git a/codex-cli/src/approvals.ts b/codex-cli/src/approvals.ts index 0cf3703b54..8a670b01ca 100644 --- a/codex-cli/src/approvals.ts +++ b/codex-cli/src/approvals.ts @@ -75,19 +75,72 @@ export function canAutoApprove( writableRoots: ReadonlyArray, env: NodeJS.ProcessEnv = process.env, ): SafetyAssessment { - try { - if (command[0] === "apply_patch") { - return command.length === 2 && typeof command[1] === "string" - ? canAutoApproveApplyPatch(command[1], writableRoots, policy) - : { - type: "reject", - reason: "Invalid apply_patch command", - }; + if (command[0] === "apply_patch") { + return command.length === 2 && typeof command[1] === "string" + ? canAutoApproveApplyPatch(command[1], writableRoots, policy) + : { + type: "reject", + reason: "Invalid apply_patch command", + }; + } + + const isSafe = isSafeCommand(command); + if (isSafe != null) { + const { reason, group } = isSafe; + return { + type: "auto-approve", + reason, + group, + runInSandbox: false, + }; + } + + if ( + command[0] === "bash" && + command[1] === "-lc" && + typeof command[2] === "string" && + command.length === 3 + ) { + const applyPatchArg = tryParseApplyPatch(command[2]); + if (applyPatchArg != null) { + return canAutoApproveApplyPatch(applyPatchArg, writableRoots, policy); } - const isSafe = isSafeCommand(command); - if (isSafe != null) { - const { reason, group } = isSafe; + let bashCmd; + try { + bashCmd = parse(command[2], env); + } catch (e) { + // In practice, there seem to be syntactically valid shell commands that + // shell-quote cannot parse, so we should not reject, but ask the user. + switch (policy) { + case "full-auto": + // In full-auto, we still run the command automatically, but must + // restrict it to the sandbox. + return { + type: "auto-approve", + reason: "Full auto mode", + group: "Running commands", + runInSandbox: true, + }; + case "suggest": + case "auto-edit": + // In all other modes, since we cannot reason about the command, we + // should ask the user. + return { + type: "ask-user", + }; + } + } + + // bashCmd could be a mix of strings and operators, e.g.: + // "ls || (true && pwd)" => [ 'ls', { op: '||' }, '(', 'true', { op: '&&' }, 'pwd', ')' ] + // We try to ensure that *every* command segment is deemed safe and that + // all operators belong to an allow‑list. If so, the entire expression is + // considered auto‑approvable. + + const shellSafe = isEntireShellExpressionSafe(bashCmd); + if (shellSafe != null) { + const { reason, group } = shellSafe; return { type: "auto-approve", reason, @@ -95,58 +148,16 @@ export function canAutoApprove( runInSandbox: false, }; } + } - if ( - command[0] === "bash" && - command[1] === "-lc" && - typeof command[2] === "string" && - command.length === 3 - ) { - const applyPatchArg = tryParseApplyPatch(command[2]); - if (applyPatchArg != null) { - return canAutoApproveApplyPatch(applyPatchArg, writableRoots, policy); - } - - const bashCmd = parse(command[2], env); - - // bashCmd could be a mix of strings and operators, e.g.: - // "ls || (true && pwd)" => [ 'ls', { op: '||' }, '(', 'true', { op: '&&' }, 'pwd', ')' ] - // We try to ensure that *every* command segment is deemed safe and that - // all operators belong to an allow‑list. If so, the entire expression is - // considered auto‑approvable. - - const shellSafe = isEntireShellExpressionSafe(bashCmd); - if (shellSafe != null) { - const { reason, group } = shellSafe; - return { - type: "auto-approve", - reason, - group, - runInSandbox: false, - }; - } - } - - return policy === "full-auto" - ? { - type: "auto-approve", - reason: "Full auto mode", - group: "Running commands", - runInSandbox: true, - } - : { type: "ask-user" }; - } catch (err) { - if (policy === "full-auto") { - return { + return policy === "full-auto" + ? { type: "auto-approve", reason: "Full auto mode", group: "Running commands", runInSandbox: true, - }; - } else { - return { type: "ask-user" }; - } - } + } + : { type: "ask-user" }; } function canAutoApproveApplyPatch( diff --git a/codex-cli/src/components/chat/use-message-grouping.ts b/codex-cli/src/components/chat/use-message-grouping.ts index 75e51ac682..1e526821d0 100644 --- a/codex-cli/src/components/chat/use-message-grouping.ts +++ b/codex-cli/src/components/chat/use-message-grouping.ts @@ -1,8 +1,5 @@ import type { ResponseItem } from "openai/resources/responses/responses.mjs"; -import { parseToolCall } from "../../utils/parsers.js"; -import { useMemo } from "react"; - /** * Represents a grouped sequence of response items (e.g., function call batches). */ @@ -10,72 +7,3 @@ export type GroupedResponseItem = { label: string; items: Array; }; - -/** - * Custom hook to group recent response items for display batching. - * Returns counts of auto-approved tool call groups, the latest batch, - * and the count of user messages in the visible window. - */ -export function useMessageGrouping(visibleItems: Array): { - groupCounts: Record; - batch: Array<{ item?: ResponseItem; group?: GroupedResponseItem }>; - userMsgCount: number; -} { - return useMemo(() => { - // The grouping logic only depends on the subset of messages that are - // currently rendered (visibleItems). Using that as the sole dependency - // keeps recomputations to a minimum and avoids unnecessary work when the - // full list of `items` changes outside of the visible window. - let userMsgCount = 0; - const groupCounts: Record = {}; - visibleItems.forEach((m) => { - if (m.type === "function_call") { - const toolCall = parseToolCall(m); - if (toolCall?.autoApproval) { - const group = toolCall.autoApproval.group; - groupCounts[group] = (groupCounts[group] || 0) + 1; - } - } - if (m.type === "message" && m.role === "user") { - userMsgCount++; - } - }); - const lastFew = visibleItems.slice(-3); - const batch: Array<{ item?: ResponseItem; group?: GroupedResponseItem }> = - []; - if (lastFew[0]?.type === "function_call") { - const toolCall = parseToolCall(lastFew[0]); - batch.push({ - group: { - label: toolCall?.autoApproval?.group || "Running command", - items: lastFew, - }, - }); - if (lastFew[2]?.type === "message") { - batch.push({ item: lastFew[2] }); - } - } else if (lastFew[1]?.type === "function_call") { - const toolCall = parseToolCall(lastFew[1]); - batch.push({ - group: { - label: toolCall?.autoApproval?.group || "Running command", - items: lastFew.slice(1), - }, - }); - } else if (lastFew[2]?.type === "function_call") { - const toolCall = parseToolCall(lastFew[2]); - batch.push({ - group: { - label: toolCall?.autoApproval?.group || "Running command", - items: [lastFew[2]], - }, - }); - } else { - lastFew.forEach((item) => batch.push({ item })); - } - return { groupCounts, batch, userMsgCount }; - // `items` is stable across renders while `visibleItems` changes based on - // the scroll window. Including only `visibleItems` avoids unnecessary - // recomputations while still producing correct results. - }, [visibleItems]); -} diff --git a/codex-cli/src/utils/agent/review.ts b/codex-cli/src/utils/agent/review.ts index a370388569..9a5e66a4ff 100644 --- a/codex-cli/src/utils/agent/review.ts +++ b/codex-cli/src/utils/agent/review.ts @@ -1,11 +1,3 @@ -import type { SafeCommandReason } from "../../approvals"; - -export type CommandReviewDetails = { - cmd: Array; - cmdReadableText: string; - autoApproval: SafeCommandReason | null; -}; - export enum ReviewDecision { YES = "yes", NO_CONTINUE = "no-continue", diff --git a/codex-cli/src/utils/parsers.ts b/codex-cli/src/utils/parsers.ts index 815e7b2f00..4461379c02 100644 --- a/codex-cli/src/utils/parsers.ts +++ b/codex-cli/src/utils/parsers.ts @@ -1,30 +1,16 @@ -import type { CommandReviewDetails } from "./agent/review.js"; import type { ExecInput, ExecOutputMetadata, } from "./agent/sandbox/interface.js"; import type { ResponseFunctionToolCall } from "openai/resources/responses/responses.mjs"; -import { isSafeCommand, type SafeCommandReason } from "../approvals.js"; import { log } from "node:console"; -import process from "process"; -import { parse } from "shell-quote"; import { formatCommandForDisplay } from "src/format-command.js"; // The console utility import is intentionally explicit to avoid bundlers from // including the entire `console` module when only the `log` function is // required. -// Allowed shell operators that we consider "safe" as they do not introduce -// side‑effects on their own (unlike redirections). Parentheses and braces for -// grouping are excluded for simplicity. -const SAFE_SHELL_OPERATORS: ReadonlySet = new Set([ - "&&", - "||", - "|", - ";", -]); - export function parseToolCallOutput(toolCallOutput: string): { output: string; metadata: ExecOutputMetadata; @@ -46,6 +32,17 @@ export function parseToolCallOutput(toolCallOutput: string): { } } +export type CommandReviewDetails = { + cmd: Array; + cmdReadableText: string; +}; + +/** + * Tries to parse a tool call and, if successful, returns an object that has + * both: + * - an array of strings to use with `ExecInput` and `canAutoApprove()` + * - a human-readable string to display to the user + */ export function parseToolCall( toolCall: ResponseFunctionToolCall, ): CommandReviewDetails | undefined { @@ -57,12 +54,9 @@ export function parseToolCall( const { cmd } = toolCallArgs; const cmdReadableText = formatCommandForDisplay(cmd); - const autoApproval = computeAutoApproval(cmd); - return { cmd, cmdReadableText, - autoApproval, }; } @@ -109,87 +103,3 @@ function toStringArray(obj: unknown): Array | undefined { return undefined; } } - -// ---------------- safe‑command helpers ---------------- - -/** - * Attempts to determine whether `cmd` is composed exclusively of safe - * sub‑commands combined using only operators from the SAFE_SHELL_OPERATORS - * allow‑list. Returns the `SafeCommandReason` (taken from the first sub‑command) - * if the whole expression is safe; otherwise returns `null`. - */ -function computeAutoApproval(cmd: Array): SafeCommandReason | null { - // Fast path: a simple command with no shell processing. - const direct = isSafeCommand(cmd); - if (direct != null) { - return direct; - } - - // For expressions like ["bash", "-lc", "ls && pwd"] break down the inner - // string and verify each segment. - if ( - cmd.length === 3 && - cmd[0] === "bash" && - cmd[1] === "-lc" && - typeof cmd[2] === "string" - ) { - const parsed = parse(cmd[2], process.env ?? {}); - if (parsed.length === 0) { - return null; - } - - let current: Array = []; - let first: SafeCommandReason | null = null; - - const flush = (): boolean => { - if (current.length === 0) { - return true; - } - const safe = isSafeCommand(current); - if (safe == null) { - return false; - } - if (!first) { - first = safe; - } - current = []; - return true; - }; - - for (const part of parsed) { - if (typeof part === "string") { - // Simple word/argument token. - if (part === "(" || part === ")" || part === "{" || part === "}") { - // We treat explicit grouping tokens as unsafe because their - // semantics depend on the shell evaluation environment. - return null; - } - current.push(part); - } else if (part && typeof part === "object") { - const opToken = part as { op?: string }; - if (typeof opToken.op === "string") { - if (!flush()) { - return null; - } - if (!SAFE_SHELL_OPERATORS.has(opToken.op)) { - return null; - } - } else { - // Unknown object token kind (e.g. redirection) – treat as unsafe. - return null; - } - } else { - // Token types such as numbers / booleans are unexpected – treat as unsafe. - return null; - } - } - - if (!flush()) { - return null; - } - - return first; - } - - return null; -} From 832b98a5577d08106f242668faea4fd9c3192f5e Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Sat, 19 Apr 2025 09:52:49 -0700 Subject: [PATCH 09/41] use spawn instead of exec to avoid injection vulnerability --- .../src/components/chat/terminal-chat.tsx | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/codex-cli/src/components/chat/terminal-chat.tsx b/codex-cli/src/components/chat/terminal-chat.tsx index 298d9208e2..f228ed19c4 100644 --- a/codex-cli/src/components/chat/terminal-chat.tsx +++ b/codex-cli/src/components/chat/terminal-chat.tsx @@ -29,7 +29,7 @@ import HelpOverlay from "../help-overlay.js"; import HistoryOverlay from "../history-overlay.js"; import ModelOverlay from "../model-overlay.js"; import { Box, Text } from "ink"; -import { exec } from "node:child_process"; +import { spawn } from "node:child_process"; import OpenAI from "openai"; import React, { useEffect, useMemo, useRef, useState } from "react"; import { inspect } from "util"; @@ -300,15 +300,10 @@ export default function TerminalChat({ agentRef.current = undefined; forceUpdate(); // re‑render after teardown too }; - // We intentionally omit 'approvalPolicy' and 'confirmationPrompt' from the deps - // so switching modes or showing confirmation dialogs doesn’t tear down the loop. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - model, - config, - requestConfirmation, - additionalWritableRoots, - ]); + // We intentionally omit 'approvalPolicy' and 'confirmationPrompt' from the deps + // so switching modes or showing confirmation dialogs doesn’t tear down the loop. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [model, config, requestConfirmation, additionalWritableRoots]); // whenever loading starts/stops, reset or start a timer — but pause the // timer while a confirmation overlay is displayed so we don't trigger a @@ -369,9 +364,10 @@ export default function TerminalChat({ const safePreview = preview.replace(/"/g, '\\"'); const title = "Codex CLI"; const cwd = PWD; - exec( - `osascript -e 'display notification "${safePreview}" with title "${title}" subtitle "${cwd}" sound name "Ping"'`, - ); + spawn("osascript", [ + "-e", + `display notification "${safePreview}" with title "${title}" subtitle "${cwd}" sound name "Ping"`, + ]); } } } @@ -597,7 +593,11 @@ export default function TerminalChat({ setApprovalPolicy(newMode as ApprovalPolicy); // update existing AgentLoop instance if (agentRef.current) { - (agentRef.current as unknown as { approvalPolicy: ApprovalPolicy }).approvalPolicy = newMode as ApprovalPolicy; + ( + agentRef.current as unknown as { + approvalPolicy: ApprovalPolicy; + } + ).approvalPolicy = newMode as ApprovalPolicy; } setItems((prev) => [ ...prev, From 66ae9fd234f3d5448ec74a8a381e67736f61af44 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Sat, 19 Apr 2025 10:01:18 -0700 Subject: [PATCH 10/41] re-enable Prettier check for codex-cli in CI --- .github/workflows/ci.yml | 6 +- .../chat/terminal-chat-command-review.tsx | 79 ++++++++++--------- codex-cli/tests/input-utils.test.ts | 10 ++- 3 files changed, 52 insertions(+), 43 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 695190c4c9..378aea50c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,7 +44,11 @@ jobs: # Run all tasks using workspace filters - - name: Check formatting + - name: Check TypeScript code formatting. + working-directory: codex-cli + run: pnpm run format + + - name: Check Markdown and config file formatting. run: pnpm run format - name: Run tests diff --git a/codex-cli/src/components/chat/terminal-chat-command-review.tsx b/codex-cli/src/components/chat/terminal-chat-command-review.tsx index eadb9071ae..912af97961 100644 --- a/codex-cli/src/components/chat/terminal-chat-command-review.tsx +++ b/codex-cli/src/components/chat/terminal-chat-command-review.tsx @@ -121,46 +121,47 @@ export function TerminalChatCommandReview({ useInput( (input, key) => { - if (mode === "select") { - if (input === "y") { - onReviewCommand(ReviewDecision.YES); - } else if (input === "x") { - onReviewCommand(ReviewDecision.EXPLAIN); - } else if (input === "e") { - setMode("input"); - } else if (input === "n") { - onReviewCommand( - ReviewDecision.NO_CONTINUE, - "Don't do that, keep going though", - ); - } else if (input === "a" && showAlwaysApprove) { - onReviewCommand(ReviewDecision.ALWAYS); - } else if (input === "s") { - // switch approval mode - onSwitchApprovalMode(); - } else if (key.escape) { - onReviewCommand(ReviewDecision.NO_EXIT); + if (mode === "select") { + if (input === "y") { + onReviewCommand(ReviewDecision.YES); + } else if (input === "x") { + onReviewCommand(ReviewDecision.EXPLAIN); + } else if (input === "e") { + setMode("input"); + } else if (input === "n") { + onReviewCommand( + ReviewDecision.NO_CONTINUE, + "Don't do that, keep going though", + ); + } else if (input === "a" && showAlwaysApprove) { + onReviewCommand(ReviewDecision.ALWAYS); + } else if (input === "s") { + // switch approval mode + onSwitchApprovalMode(); + } else if (key.escape) { + onReviewCommand(ReviewDecision.NO_EXIT); + } + } else if (mode === "explanation") { + // When in explanation mode, any key returns to select mode + if (key.return || key.escape || input === "x") { + setMode("select"); + } + } else { + // text entry mode + if (key.return) { + // if user hit enter on empty msg, fall back to DEFAULT_DENY_MESSAGE + const custom = msg.trim() === "" ? DEFAULT_DENY_MESSAGE : msg; + onReviewCommand(ReviewDecision.NO_CONTINUE, custom); + } else if (key.escape) { + // treat escape as denial with default message as well + onReviewCommand( + ReviewDecision.NO_CONTINUE, + msg.trim() === "" ? DEFAULT_DENY_MESSAGE : msg, + ); + } } - } else if (mode === "explanation") { - // When in explanation mode, any key returns to select mode - if (key.return || key.escape || input === "x") { - setMode("select"); - } - } else { - // text entry mode - if (key.return) { - // if user hit enter on empty msg, fall back to DEFAULT_DENY_MESSAGE - const custom = msg.trim() === "" ? DEFAULT_DENY_MESSAGE : msg; - onReviewCommand(ReviewDecision.NO_CONTINUE, custom); - } else if (key.escape) { - // treat escape as denial with default message as well - onReviewCommand( - ReviewDecision.NO_CONTINUE, - msg.trim() === "" ? DEFAULT_DENY_MESSAGE : msg, - ); - } - } - }, { isActive } + }, + { isActive }, ); return ( diff --git a/codex-cli/tests/input-utils.test.ts b/codex-cli/tests/input-utils.test.ts index 5290e55488..780a384240 100644 --- a/codex-cli/tests/input-utils.test.ts +++ b/codex-cli/tests/input-utils.test.ts @@ -14,9 +14,13 @@ describe("createInputItem", () => { it("includes image content for existing file", async () => { const fakeBuffer = Buffer.from("fake image content"); - const readSpy = vi.spyOn(fs, "readFile").mockResolvedValue(fakeBuffer as any); + const readSpy = vi + .spyOn(fs, "readFile") + .mockResolvedValue(fakeBuffer as any); const result = await createInputItem("hello", ["dummy-path"]); - const expectedUrl = `data:application/octet-stream;base64,${fakeBuffer.toString("base64")}`; + const expectedUrl = `data:application/octet-stream;base64,${fakeBuffer.toString( + "base64", + )}`; expect(result.role).toBe("user"); expect(result.type).toBe("message"); expect(result.content.length).toBe(2); @@ -40,4 +44,4 @@ describe("createInputItem", () => { text: "[missing image: does-not-exist.png]", }); }); -}); \ No newline at end of file +}); From ede10c12476349cf070b07aeecb239db7e81d646 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Sat, 19 Apr 2025 10:01:41 -0700 Subject: [PATCH 11/41] re-enable Prettier check for codex-cli in CI --- .github/workflows/ci.yml | 6 +- .../chat/terminal-chat-command-review.tsx | 79 ++++++++++--------- codex-cli/tests/input-utils.test.ts | 10 ++- 3 files changed, 52 insertions(+), 43 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 695190c4c9..378aea50c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,7 +44,11 @@ jobs: # Run all tasks using workspace filters - - name: Check formatting + - name: Check TypeScript code formatting. + working-directory: codex-cli + run: pnpm run format + + - name: Check Markdown and config file formatting. run: pnpm run format - name: Run tests diff --git a/codex-cli/src/components/chat/terminal-chat-command-review.tsx b/codex-cli/src/components/chat/terminal-chat-command-review.tsx index eadb9071ae..912af97961 100644 --- a/codex-cli/src/components/chat/terminal-chat-command-review.tsx +++ b/codex-cli/src/components/chat/terminal-chat-command-review.tsx @@ -121,46 +121,47 @@ export function TerminalChatCommandReview({ useInput( (input, key) => { - if (mode === "select") { - if (input === "y") { - onReviewCommand(ReviewDecision.YES); - } else if (input === "x") { - onReviewCommand(ReviewDecision.EXPLAIN); - } else if (input === "e") { - setMode("input"); - } else if (input === "n") { - onReviewCommand( - ReviewDecision.NO_CONTINUE, - "Don't do that, keep going though", - ); - } else if (input === "a" && showAlwaysApprove) { - onReviewCommand(ReviewDecision.ALWAYS); - } else if (input === "s") { - // switch approval mode - onSwitchApprovalMode(); - } else if (key.escape) { - onReviewCommand(ReviewDecision.NO_EXIT); + if (mode === "select") { + if (input === "y") { + onReviewCommand(ReviewDecision.YES); + } else if (input === "x") { + onReviewCommand(ReviewDecision.EXPLAIN); + } else if (input === "e") { + setMode("input"); + } else if (input === "n") { + onReviewCommand( + ReviewDecision.NO_CONTINUE, + "Don't do that, keep going though", + ); + } else if (input === "a" && showAlwaysApprove) { + onReviewCommand(ReviewDecision.ALWAYS); + } else if (input === "s") { + // switch approval mode + onSwitchApprovalMode(); + } else if (key.escape) { + onReviewCommand(ReviewDecision.NO_EXIT); + } + } else if (mode === "explanation") { + // When in explanation mode, any key returns to select mode + if (key.return || key.escape || input === "x") { + setMode("select"); + } + } else { + // text entry mode + if (key.return) { + // if user hit enter on empty msg, fall back to DEFAULT_DENY_MESSAGE + const custom = msg.trim() === "" ? DEFAULT_DENY_MESSAGE : msg; + onReviewCommand(ReviewDecision.NO_CONTINUE, custom); + } else if (key.escape) { + // treat escape as denial with default message as well + onReviewCommand( + ReviewDecision.NO_CONTINUE, + msg.trim() === "" ? DEFAULT_DENY_MESSAGE : msg, + ); + } } - } else if (mode === "explanation") { - // When in explanation mode, any key returns to select mode - if (key.return || key.escape || input === "x") { - setMode("select"); - } - } else { - // text entry mode - if (key.return) { - // if user hit enter on empty msg, fall back to DEFAULT_DENY_MESSAGE - const custom = msg.trim() === "" ? DEFAULT_DENY_MESSAGE : msg; - onReviewCommand(ReviewDecision.NO_CONTINUE, custom); - } else if (key.escape) { - // treat escape as denial with default message as well - onReviewCommand( - ReviewDecision.NO_CONTINUE, - msg.trim() === "" ? DEFAULT_DENY_MESSAGE : msg, - ); - } - } - }, { isActive } + }, + { isActive }, ); return ( diff --git a/codex-cli/tests/input-utils.test.ts b/codex-cli/tests/input-utils.test.ts index 5290e55488..780a384240 100644 --- a/codex-cli/tests/input-utils.test.ts +++ b/codex-cli/tests/input-utils.test.ts @@ -14,9 +14,13 @@ describe("createInputItem", () => { it("includes image content for existing file", async () => { const fakeBuffer = Buffer.from("fake image content"); - const readSpy = vi.spyOn(fs, "readFile").mockResolvedValue(fakeBuffer as any); + const readSpy = vi + .spyOn(fs, "readFile") + .mockResolvedValue(fakeBuffer as any); const result = await createInputItem("hello", ["dummy-path"]); - const expectedUrl = `data:application/octet-stream;base64,${fakeBuffer.toString("base64")}`; + const expectedUrl = `data:application/octet-stream;base64,${fakeBuffer.toString( + "base64", + )}`; expect(result.role).toBe("user"); expect(result.type).toBe("message"); expect(result.content.length).toBe(2); @@ -40,4 +44,4 @@ describe("createInputItem", () => { text: "[missing image: does-not-exist.png]", }); }); -}); \ No newline at end of file +}); From be9d216aabc0cb62e1d965b428996f50d81ca7db Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Sat, 19 Apr 2025 10:01:41 -0700 Subject: [PATCH 12/41] re-enable Prettier check for codex-cli in CI --- .github/workflows/ci.yml | 6 +- .../chat/terminal-chat-command-review.tsx | 79 ++++++++++--------- .../src/components/chat/terminal-chat.tsx | 19 +++-- codex-cli/tests/input-utils.test.ts | 10 ++- 4 files changed, 61 insertions(+), 53 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 695190c4c9..378aea50c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,7 +44,11 @@ jobs: # Run all tasks using workspace filters - - name: Check formatting + - name: Check TypeScript code formatting. + working-directory: codex-cli + run: pnpm run format + + - name: Check Markdown and config file formatting. run: pnpm run format - name: Run tests diff --git a/codex-cli/src/components/chat/terminal-chat-command-review.tsx b/codex-cli/src/components/chat/terminal-chat-command-review.tsx index eadb9071ae..912af97961 100644 --- a/codex-cli/src/components/chat/terminal-chat-command-review.tsx +++ b/codex-cli/src/components/chat/terminal-chat-command-review.tsx @@ -121,46 +121,47 @@ export function TerminalChatCommandReview({ useInput( (input, key) => { - if (mode === "select") { - if (input === "y") { - onReviewCommand(ReviewDecision.YES); - } else if (input === "x") { - onReviewCommand(ReviewDecision.EXPLAIN); - } else if (input === "e") { - setMode("input"); - } else if (input === "n") { - onReviewCommand( - ReviewDecision.NO_CONTINUE, - "Don't do that, keep going though", - ); - } else if (input === "a" && showAlwaysApprove) { - onReviewCommand(ReviewDecision.ALWAYS); - } else if (input === "s") { - // switch approval mode - onSwitchApprovalMode(); - } else if (key.escape) { - onReviewCommand(ReviewDecision.NO_EXIT); + if (mode === "select") { + if (input === "y") { + onReviewCommand(ReviewDecision.YES); + } else if (input === "x") { + onReviewCommand(ReviewDecision.EXPLAIN); + } else if (input === "e") { + setMode("input"); + } else if (input === "n") { + onReviewCommand( + ReviewDecision.NO_CONTINUE, + "Don't do that, keep going though", + ); + } else if (input === "a" && showAlwaysApprove) { + onReviewCommand(ReviewDecision.ALWAYS); + } else if (input === "s") { + // switch approval mode + onSwitchApprovalMode(); + } else if (key.escape) { + onReviewCommand(ReviewDecision.NO_EXIT); + } + } else if (mode === "explanation") { + // When in explanation mode, any key returns to select mode + if (key.return || key.escape || input === "x") { + setMode("select"); + } + } else { + // text entry mode + if (key.return) { + // if user hit enter on empty msg, fall back to DEFAULT_DENY_MESSAGE + const custom = msg.trim() === "" ? DEFAULT_DENY_MESSAGE : msg; + onReviewCommand(ReviewDecision.NO_CONTINUE, custom); + } else if (key.escape) { + // treat escape as denial with default message as well + onReviewCommand( + ReviewDecision.NO_CONTINUE, + msg.trim() === "" ? DEFAULT_DENY_MESSAGE : msg, + ); + } } - } else if (mode === "explanation") { - // When in explanation mode, any key returns to select mode - if (key.return || key.escape || input === "x") { - setMode("select"); - } - } else { - // text entry mode - if (key.return) { - // if user hit enter on empty msg, fall back to DEFAULT_DENY_MESSAGE - const custom = msg.trim() === "" ? DEFAULT_DENY_MESSAGE : msg; - onReviewCommand(ReviewDecision.NO_CONTINUE, custom); - } else if (key.escape) { - // treat escape as denial with default message as well - onReviewCommand( - ReviewDecision.NO_CONTINUE, - msg.trim() === "" ? DEFAULT_DENY_MESSAGE : msg, - ); - } - } - }, { isActive } + }, + { isActive }, ); return ( diff --git a/codex-cli/src/components/chat/terminal-chat.tsx b/codex-cli/src/components/chat/terminal-chat.tsx index 298d9208e2..e341cdfbbb 100644 --- a/codex-cli/src/components/chat/terminal-chat.tsx +++ b/codex-cli/src/components/chat/terminal-chat.tsx @@ -300,15 +300,10 @@ export default function TerminalChat({ agentRef.current = undefined; forceUpdate(); // re‑render after teardown too }; - // We intentionally omit 'approvalPolicy' and 'confirmationPrompt' from the deps - // so switching modes or showing confirmation dialogs doesn’t tear down the loop. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - model, - config, - requestConfirmation, - additionalWritableRoots, - ]); + // We intentionally omit 'approvalPolicy' and 'confirmationPrompt' from the deps + // so switching modes or showing confirmation dialogs doesn’t tear down the loop. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [model, config, requestConfirmation, additionalWritableRoots]); // whenever loading starts/stops, reset or start a timer — but pause the // timer while a confirmation overlay is displayed so we don't trigger a @@ -597,7 +592,11 @@ export default function TerminalChat({ setApprovalPolicy(newMode as ApprovalPolicy); // update existing AgentLoop instance if (agentRef.current) { - (agentRef.current as unknown as { approvalPolicy: ApprovalPolicy }).approvalPolicy = newMode as ApprovalPolicy; + ( + agentRef.current as unknown as { + approvalPolicy: ApprovalPolicy; + } + ).approvalPolicy = newMode as ApprovalPolicy; } setItems((prev) => [ ...prev, diff --git a/codex-cli/tests/input-utils.test.ts b/codex-cli/tests/input-utils.test.ts index 5290e55488..780a384240 100644 --- a/codex-cli/tests/input-utils.test.ts +++ b/codex-cli/tests/input-utils.test.ts @@ -14,9 +14,13 @@ describe("createInputItem", () => { it("includes image content for existing file", async () => { const fakeBuffer = Buffer.from("fake image content"); - const readSpy = vi.spyOn(fs, "readFile").mockResolvedValue(fakeBuffer as any); + const readSpy = vi + .spyOn(fs, "readFile") + .mockResolvedValue(fakeBuffer as any); const result = await createInputItem("hello", ["dummy-path"]); - const expectedUrl = `data:application/octet-stream;base64,${fakeBuffer.toString("base64")}`; + const expectedUrl = `data:application/octet-stream;base64,${fakeBuffer.toString( + "base64", + )}`; expect(result.role).toBe("user"); expect(result.type).toBe("message"); expect(result.content.length).toBe(2); @@ -40,4 +44,4 @@ describe("createInputItem", () => { text: "[missing image: does-not-exist.png]", }); }); -}); \ No newline at end of file +}); From 19a43ad15b679721d3c33aa01f4f04dd3bf2797c Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Sat, 19 Apr 2025 10:03:17 -0700 Subject: [PATCH 13/41] use spawn instead of exec to avoid injection vulnerability --- codex-cli/src/components/chat/terminal-chat.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/codex-cli/src/components/chat/terminal-chat.tsx b/codex-cli/src/components/chat/terminal-chat.tsx index e341cdfbbb..f228ed19c4 100644 --- a/codex-cli/src/components/chat/terminal-chat.tsx +++ b/codex-cli/src/components/chat/terminal-chat.tsx @@ -29,7 +29,7 @@ import HelpOverlay from "../help-overlay.js"; import HistoryOverlay from "../history-overlay.js"; import ModelOverlay from "../model-overlay.js"; import { Box, Text } from "ink"; -import { exec } from "node:child_process"; +import { spawn } from "node:child_process"; import OpenAI from "openai"; import React, { useEffect, useMemo, useRef, useState } from "react"; import { inspect } from "util"; @@ -364,9 +364,10 @@ export default function TerminalChat({ const safePreview = preview.replace(/"/g, '\\"'); const title = "Codex CLI"; const cwd = PWD; - exec( - `osascript -e 'display notification "${safePreview}" with title "${title}" subtitle "${cwd}" sound name "Ping"'`, - ); + spawn("osascript", [ + "-e", + `display notification "${safePreview}" with title "${title}" subtitle "${cwd}" sound name "Ping"`, + ]); } } } From 3bbeba28ce0617a5b2d0e53eb96792fe93a5d3f6 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Sat, 19 Apr 2025 10:26:23 -0700 Subject: [PATCH 14/41] CONFIG_DIR should not be in the list of writable roots by default --- .../src/utils/agent/sandbox/macos-seatbelt.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/codex-cli/src/utils/agent/sandbox/macos-seatbelt.ts b/codex-cli/src/utils/agent/sandbox/macos-seatbelt.ts index 0317458261..760ba63d8c 100644 --- a/codex-cli/src/utils/agent/sandbox/macos-seatbelt.ts +++ b/codex-cli/src/utils/agent/sandbox/macos-seatbelt.ts @@ -3,11 +3,9 @@ import type { SpawnOptions } from "child_process"; import { exec } from "./raw-exec.js"; import { log } from "../log.js"; -import { CONFIG_DIR } from "src/utils/config.js"; function getCommonRoots() { return [ - CONFIG_DIR, // Without this root, it'll cause: // pyenv: cannot rehash: $HOME/.pyenv/shims isn't writable `${process.env["HOME"]}/.pyenv`, @@ -17,16 +15,17 @@ function getCommonRoots() { export function execWithSeatbelt( cmd: Array, opts: SpawnOptions, - writableRoots: Array, + writableRoots: ReadonlyArray, abortSignal?: AbortSignal, ): Promise { let scopedWritePolicy: string; let policyTemplateParams: Array; - if (writableRoots.length > 0) { - // Add `~/.codex` to the list of writable roots - // (if there's any already, not in read-only mode) - getCommonRoots().map((root) => writableRoots.push(root)); - const { policies, params } = writableRoots + + const fullWritableRoots = [...writableRoots, ...getCommonRoots()]; + // In practice, fullWritableRoots will be non-empty, but we check just in + // case the logic to build up fullWritableRoots changes. + if (fullWritableRoots.length > 0) { + const { policies, params } = fullWritableRoots .map((root, index) => ({ policy: `(subpath (param "WRITABLE_ROOT_${index}"))`, param: `-DWRITABLE_ROOT_${index}=${root}`, From 8a4968ae9fe5353076ed6d38244f4be16d032873 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Sat, 19 Apr 2025 10:01:41 -0700 Subject: [PATCH 15/41] re-enable Prettier check for codex-cli in CI --- .github/workflows/ci.yml | 6 +- .../chat/terminal-chat-command-review.tsx | 79 ++++++++++--------- .../src/components/chat/terminal-chat.tsx | 19 +++-- codex-cli/tests/input-utils.test.ts | 10 ++- 4 files changed, 61 insertions(+), 53 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 695190c4c9..e99c2daa04 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,7 +44,11 @@ jobs: # Run all tasks using workspace filters - - name: Check formatting + - name: Check TypeScript code formatting + working-directory: codex-cli + run: pnpm run format + + - name: Check Markdown and config file formatting run: pnpm run format - name: Run tests diff --git a/codex-cli/src/components/chat/terminal-chat-command-review.tsx b/codex-cli/src/components/chat/terminal-chat-command-review.tsx index eadb9071ae..912af97961 100644 --- a/codex-cli/src/components/chat/terminal-chat-command-review.tsx +++ b/codex-cli/src/components/chat/terminal-chat-command-review.tsx @@ -121,46 +121,47 @@ export function TerminalChatCommandReview({ useInput( (input, key) => { - if (mode === "select") { - if (input === "y") { - onReviewCommand(ReviewDecision.YES); - } else if (input === "x") { - onReviewCommand(ReviewDecision.EXPLAIN); - } else if (input === "e") { - setMode("input"); - } else if (input === "n") { - onReviewCommand( - ReviewDecision.NO_CONTINUE, - "Don't do that, keep going though", - ); - } else if (input === "a" && showAlwaysApprove) { - onReviewCommand(ReviewDecision.ALWAYS); - } else if (input === "s") { - // switch approval mode - onSwitchApprovalMode(); - } else if (key.escape) { - onReviewCommand(ReviewDecision.NO_EXIT); + if (mode === "select") { + if (input === "y") { + onReviewCommand(ReviewDecision.YES); + } else if (input === "x") { + onReviewCommand(ReviewDecision.EXPLAIN); + } else if (input === "e") { + setMode("input"); + } else if (input === "n") { + onReviewCommand( + ReviewDecision.NO_CONTINUE, + "Don't do that, keep going though", + ); + } else if (input === "a" && showAlwaysApprove) { + onReviewCommand(ReviewDecision.ALWAYS); + } else if (input === "s") { + // switch approval mode + onSwitchApprovalMode(); + } else if (key.escape) { + onReviewCommand(ReviewDecision.NO_EXIT); + } + } else if (mode === "explanation") { + // When in explanation mode, any key returns to select mode + if (key.return || key.escape || input === "x") { + setMode("select"); + } + } else { + // text entry mode + if (key.return) { + // if user hit enter on empty msg, fall back to DEFAULT_DENY_MESSAGE + const custom = msg.trim() === "" ? DEFAULT_DENY_MESSAGE : msg; + onReviewCommand(ReviewDecision.NO_CONTINUE, custom); + } else if (key.escape) { + // treat escape as denial with default message as well + onReviewCommand( + ReviewDecision.NO_CONTINUE, + msg.trim() === "" ? DEFAULT_DENY_MESSAGE : msg, + ); + } } - } else if (mode === "explanation") { - // When in explanation mode, any key returns to select mode - if (key.return || key.escape || input === "x") { - setMode("select"); - } - } else { - // text entry mode - if (key.return) { - // if user hit enter on empty msg, fall back to DEFAULT_DENY_MESSAGE - const custom = msg.trim() === "" ? DEFAULT_DENY_MESSAGE : msg; - onReviewCommand(ReviewDecision.NO_CONTINUE, custom); - } else if (key.escape) { - // treat escape as denial with default message as well - onReviewCommand( - ReviewDecision.NO_CONTINUE, - msg.trim() === "" ? DEFAULT_DENY_MESSAGE : msg, - ); - } - } - }, { isActive } + }, + { isActive }, ); return ( diff --git a/codex-cli/src/components/chat/terminal-chat.tsx b/codex-cli/src/components/chat/terminal-chat.tsx index 298d9208e2..e341cdfbbb 100644 --- a/codex-cli/src/components/chat/terminal-chat.tsx +++ b/codex-cli/src/components/chat/terminal-chat.tsx @@ -300,15 +300,10 @@ export default function TerminalChat({ agentRef.current = undefined; forceUpdate(); // re‑render after teardown too }; - // We intentionally omit 'approvalPolicy' and 'confirmationPrompt' from the deps - // so switching modes or showing confirmation dialogs doesn’t tear down the loop. - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - model, - config, - requestConfirmation, - additionalWritableRoots, - ]); + // We intentionally omit 'approvalPolicy' and 'confirmationPrompt' from the deps + // so switching modes or showing confirmation dialogs doesn’t tear down the loop. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [model, config, requestConfirmation, additionalWritableRoots]); // whenever loading starts/stops, reset or start a timer — but pause the // timer while a confirmation overlay is displayed so we don't trigger a @@ -597,7 +592,11 @@ export default function TerminalChat({ setApprovalPolicy(newMode as ApprovalPolicy); // update existing AgentLoop instance if (agentRef.current) { - (agentRef.current as unknown as { approvalPolicy: ApprovalPolicy }).approvalPolicy = newMode as ApprovalPolicy; + ( + agentRef.current as unknown as { + approvalPolicy: ApprovalPolicy; + } + ).approvalPolicy = newMode as ApprovalPolicy; } setItems((prev) => [ ...prev, diff --git a/codex-cli/tests/input-utils.test.ts b/codex-cli/tests/input-utils.test.ts index 5290e55488..780a384240 100644 --- a/codex-cli/tests/input-utils.test.ts +++ b/codex-cli/tests/input-utils.test.ts @@ -14,9 +14,13 @@ describe("createInputItem", () => { it("includes image content for existing file", async () => { const fakeBuffer = Buffer.from("fake image content"); - const readSpy = vi.spyOn(fs, "readFile").mockResolvedValue(fakeBuffer as any); + const readSpy = vi + .spyOn(fs, "readFile") + .mockResolvedValue(fakeBuffer as any); const result = await createInputItem("hello", ["dummy-path"]); - const expectedUrl = `data:application/octet-stream;base64,${fakeBuffer.toString("base64")}`; + const expectedUrl = `data:application/octet-stream;base64,${fakeBuffer.toString( + "base64", + )}`; expect(result.role).toBe("user"); expect(result.type).toBe("message"); expect(result.content.length).toBe(2); @@ -40,4 +44,4 @@ describe("createInputItem", () => { text: "[missing image: does-not-exist.png]", }); }); -}); \ No newline at end of file +}); From 0430ab327b761c6f05bd1daf33e40a2bd86a7b2f Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Sat, 19 Apr 2025 10:29:18 -0700 Subject: [PATCH 16/41] use spawn instead of exec to avoid injection vulnerability --- codex-cli/src/components/chat/terminal-chat.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/codex-cli/src/components/chat/terminal-chat.tsx b/codex-cli/src/components/chat/terminal-chat.tsx index e341cdfbbb..f228ed19c4 100644 --- a/codex-cli/src/components/chat/terminal-chat.tsx +++ b/codex-cli/src/components/chat/terminal-chat.tsx @@ -29,7 +29,7 @@ import HelpOverlay from "../help-overlay.js"; import HistoryOverlay from "../history-overlay.js"; import ModelOverlay from "../model-overlay.js"; import { Box, Text } from "ink"; -import { exec } from "node:child_process"; +import { spawn } from "node:child_process"; import OpenAI from "openai"; import React, { useEffect, useMemo, useRef, useState } from "react"; import { inspect } from "util"; @@ -364,9 +364,10 @@ export default function TerminalChat({ const safePreview = preview.replace(/"/g, '\\"'); const title = "Codex CLI"; const cwd = PWD; - exec( - `osascript -e 'display notification "${safePreview}" with title "${title}" subtitle "${cwd}" sound name "Ping"'`, - ); + spawn("osascript", [ + "-e", + `display notification "${safePreview}" with title "${title}" subtitle "${cwd}" sound name "Ping"`, + ]); } } } From 5333ea726f68d181f518c4dcdc649c16d0c42678 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Sat, 19 Apr 2025 10:29:18 -0700 Subject: [PATCH 17/41] CONFIG_DIR should not be in the list of writable roots by default --- .../src/utils/agent/sandbox/macos-seatbelt.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/codex-cli/src/utils/agent/sandbox/macos-seatbelt.ts b/codex-cli/src/utils/agent/sandbox/macos-seatbelt.ts index 0317458261..760ba63d8c 100644 --- a/codex-cli/src/utils/agent/sandbox/macos-seatbelt.ts +++ b/codex-cli/src/utils/agent/sandbox/macos-seatbelt.ts @@ -3,11 +3,9 @@ import type { SpawnOptions } from "child_process"; import { exec } from "./raw-exec.js"; import { log } from "../log.js"; -import { CONFIG_DIR } from "src/utils/config.js"; function getCommonRoots() { return [ - CONFIG_DIR, // Without this root, it'll cause: // pyenv: cannot rehash: $HOME/.pyenv/shims isn't writable `${process.env["HOME"]}/.pyenv`, @@ -17,16 +15,17 @@ function getCommonRoots() { export function execWithSeatbelt( cmd: Array, opts: SpawnOptions, - writableRoots: Array, + writableRoots: ReadonlyArray, abortSignal?: AbortSignal, ): Promise { let scopedWritePolicy: string; let policyTemplateParams: Array; - if (writableRoots.length > 0) { - // Add `~/.codex` to the list of writable roots - // (if there's any already, not in read-only mode) - getCommonRoots().map((root) => writableRoots.push(root)); - const { policies, params } = writableRoots + + const fullWritableRoots = [...writableRoots, ...getCommonRoots()]; + // In practice, fullWritableRoots will be non-empty, but we check just in + // case the logic to build up fullWritableRoots changes. + if (fullWritableRoots.length > 0) { + const { policies, params } = fullWritableRoots .map((root, index) => ({ policy: `(subpath (param "WRITABLE_ROOT_${index}"))`, param: `-DWRITABLE_ROOT_${index}=${root}`, From 6ac0fd5fc10b5b02a6befdf3d364b103a45ba4ef Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Sat, 19 Apr 2025 11:07:35 -0700 Subject: [PATCH 18/41] remove unnecessary isLoggingEnabled() checks --- .../chat/terminal-chat-input-thinking.tsx | 16 ++--- .../components/chat/terminal-chat-input.tsx | 16 ++--- .../chat/terminal-chat-new-input.tsx | 16 ++--- .../src/components/chat/terminal-chat.tsx | 62 +++++++------------ codex-cli/src/utils/agent/agent-loop.ts | 58 +++++++---------- .../src/utils/agent/handle-exec-command.ts | 33 +++++----- codex-cli/src/utils/agent/log.ts | 8 +++ .../src/utils/agent/platform-commands.ts | 10 +-- codex-cli/src/utils/agent/sandbox/raw-exec.ts | 19 ++---- codex-cli/src/utils/config.ts | 14 ++--- 10 files changed, 94 insertions(+), 158 deletions(-) diff --git a/codex-cli/src/components/chat/terminal-chat-input-thinking.tsx b/codex-cli/src/components/chat/terminal-chat-input-thinking.tsx index 213dd8c9af..fdaf0c0823 100644 --- a/codex-cli/src/components/chat/terminal-chat-input-thinking.tsx +++ b/codex-cli/src/components/chat/terminal-chat-input-thinking.tsx @@ -1,4 +1,4 @@ -import { log, isLoggingEnabled } from "../../utils/agent/log.js"; +import { log } from "../../utils/agent/log.js"; // removed unused isLoggingEnabled import import { Box, Text, useInput, useStdin } from "ink"; import React, { useState } from "react"; import { useInterval } from "use-interval"; @@ -40,11 +40,7 @@ export default function TerminalChatInputThinking({ const str = Buffer.isBuffer(data) ? data.toString("utf8") : data; if (str === "\x1b\x1b") { - if (isLoggingEnabled()) { - log( - "raw stdin: received collapsed ESC ESC – starting confirmation timer", - ); - } + log("raw stdin: received collapsed ESC ESC – starting confirmation timer"); setAwaitingConfirm(true); setTimeout(() => setAwaitingConfirm(false), 1500); } @@ -65,15 +61,11 @@ export default function TerminalChatInputThinking({ } if (awaitingConfirm) { - if (isLoggingEnabled()) { - log("useInput: second ESC detected – triggering onInterrupt()"); - } + log("useInput: second ESC detected – triggering onInterrupt()"); onInterrupt(); setAwaitingConfirm(false); } else { - if (isLoggingEnabled()) { - log("useInput: first ESC detected – waiting for confirmation"); - } + log("useInput: first ESC detected – waiting for confirmation"); setAwaitingConfirm(true); setTimeout(() => setAwaitingConfirm(false), 1500); } diff --git a/codex-cli/src/components/chat/terminal-chat-input.tsx b/codex-cli/src/components/chat/terminal-chat-input.tsx index 59265221d4..9a0fdbdab9 100644 --- a/codex-cli/src/components/chat/terminal-chat-input.tsx +++ b/codex-cli/src/components/chat/terminal-chat-input.tsx @@ -6,7 +6,7 @@ import type { } from "openai/resources/responses/responses.mjs"; import { TerminalChatCommandReview } from "./terminal-chat-command-review.js"; -import { log, isLoggingEnabled } from "../../utils/agent/log.js"; +import { log } from "../../utils/agent/log.js"; import { loadConfig } from "../../utils/config.js"; import { createInputItem } from "../../utils/input-utils.js"; import { setSessionId } from "../../utils/session.js"; @@ -683,11 +683,7 @@ function TerminalChatInputThinking({ const str = Buffer.isBuffer(data) ? data.toString("utf8") : data; if (str === "\x1b\x1b") { // Treat as the first Escape press – prompt the user for confirmation. - if (isLoggingEnabled()) { - log( - "raw stdin: received collapsed ESC ESC – starting confirmation timer", - ); - } + log("raw stdin: received collapsed ESC ESC – starting confirmation timer"); setAwaitingConfirm(true); setTimeout(() => setAwaitingConfirm(false), 1500); } @@ -712,15 +708,11 @@ function TerminalChatInputThinking({ } if (awaitingConfirm) { - if (isLoggingEnabled()) { - log("useInput: second ESC detected – triggering onInterrupt()"); - } + log("useInput: second ESC detected – triggering onInterrupt()"); onInterrupt(); setAwaitingConfirm(false); } else { - if (isLoggingEnabled()) { - log("useInput: first ESC detected – waiting for confirmation"); - } + log("useInput: first ESC detected – waiting for confirmation"); setAwaitingConfirm(true); setTimeout(() => setAwaitingConfirm(false), 1500); } diff --git a/codex-cli/src/components/chat/terminal-chat-new-input.tsx b/codex-cli/src/components/chat/terminal-chat-new-input.tsx index 9ceb4bbccc..5197083be8 100644 --- a/codex-cli/src/components/chat/terminal-chat-new-input.tsx +++ b/codex-cli/src/components/chat/terminal-chat-new-input.tsx @@ -8,7 +8,7 @@ import type { import MultilineTextEditor from "./multiline-editor"; import { TerminalChatCommandReview } from "./terminal-chat-command-review.js"; -import { log, isLoggingEnabled } from "../../utils/agent/log.js"; +import { log } from "../../utils/agent/log.js"; import { loadConfig } from "../../utils/config.js"; import { createInputItem } from "../../utils/input-utils.js"; import { setSessionId } from "../../utils/session.js"; @@ -496,11 +496,7 @@ function TerminalChatInputThinking({ const str = Buffer.isBuffer(data) ? data.toString("utf8") : data; if (str === "\x1b\x1b") { // Treat as the first Escape press – prompt the user for confirmation. - if (isLoggingEnabled()) { - log( - "raw stdin: received collapsed ESC ESC – starting confirmation timer", - ); - } + log("raw stdin: received collapsed ESC ESC – starting confirmation timer"); setAwaitingConfirm(true); setTimeout(() => setAwaitingConfirm(false), 1500); } @@ -522,15 +518,11 @@ function TerminalChatInputThinking({ } if (awaitingConfirm) { - if (isLoggingEnabled()) { - log("useInput: second ESC detected – triggering onInterrupt()"); - } + log("useInput: second ESC detected – triggering onInterrupt()"); onInterrupt(); setAwaitingConfirm(false); } else { - if (isLoggingEnabled()) { - log("useInput: first ESC detected – waiting for confirmation"); - } + log("useInput: first ESC detected – waiting for confirmation"); setAwaitingConfirm(true); setTimeout(() => setAwaitingConfirm(false), 1500); } diff --git a/codex-cli/src/components/chat/terminal-chat.tsx b/codex-cli/src/components/chat/terminal-chat.tsx index f228ed19c4..538a3b3561 100644 --- a/codex-cli/src/components/chat/terminal-chat.tsx +++ b/codex-cli/src/components/chat/terminal-chat.tsx @@ -15,7 +15,7 @@ import { formatCommandForDisplay } from "../../format-command.js"; import { useConfirmation } from "../../hooks/use-confirmation.js"; import { useTerminalSize } from "../../hooks/use-terminal-size.js"; import { AgentLoop } from "../../utils/agent/agent-loop.js"; -import { isLoggingEnabled, log } from "../../utils/agent/log.js"; +import { log } from "../../utils/agent/log.js"; import { ReviewDecision } from "../../utils/agent/review.js"; import { generateCompactSummary } from "../../utils/compact-summary.js"; import { OPENAI_BASE_URL } from "../../utils/config.js"; @@ -197,30 +197,24 @@ export default function TerminalChat({ // ──────────────────────────────────────────────────────────────── // DEBUG: log every render w/ key bits of state // ──────────────────────────────────────────────────────────────── - if (isLoggingEnabled()) { - log( - `render – agent? ${Boolean(agentRef.current)} loading=${loading} items=${ - items.length - }`, - ); - } + log( + `render – agent? ${Boolean(agentRef.current)} loading=${loading} items=${ + items.length + }`, + ); useEffect(() => { // Skip recreating the agent if awaiting a decision on a pending confirmation if (confirmationPrompt != null) { - if (isLoggingEnabled()) { - log("skip AgentLoop recreation due to pending confirmationPrompt"); - } + log("skip AgentLoop recreation due to pending confirmationPrompt"); return; } - if (isLoggingEnabled()) { - log("creating NEW AgentLoop"); - log( - `model=${model} instructions=${Boolean( - config.instructions, - )} approvalPolicy=${approvalPolicy}`, - ); - } + log("creating NEW AgentLoop"); + log( + `model=${model} instructions=${Boolean( + config.instructions, + )} approvalPolicy=${approvalPolicy}`, + ); // Tear down any existing loop before creating a new one agentRef.current?.terminate(); @@ -288,14 +282,10 @@ export default function TerminalChat({ // force a render so JSX below can "see" the freshly created agent forceUpdate(); - if (isLoggingEnabled()) { - log(`AgentLoop created: ${inspect(agentRef.current, { depth: 1 })}`); - } + log(`AgentLoop created: ${inspect(agentRef.current, { depth: 1 })}`); return () => { - if (isLoggingEnabled()) { - log("terminating AgentLoop"); - } + log("terminating AgentLoop"); agentRef.current?.terminate(); agentRef.current = undefined; forceUpdate(); // re‑render after teardown too @@ -377,9 +367,7 @@ export default function TerminalChat({ // Let's also track whenever the ref becomes available const agent = agentRef.current; useEffect(() => { - if (isLoggingEnabled()) { - log(`agentRef.current is now ${Boolean(agent)}`); - } + log(`agentRef.current is now ${Boolean(agent)}`); }, [agent]); // --------------------------------------------------------------------- @@ -504,11 +492,7 @@ export default function TerminalChat({ if (!agent) { return; } - if (isLoggingEnabled()) { - log( - "TerminalChat: interruptAgent invoked – calling agent.cancel()", - ); - } + log("TerminalChat: interruptAgent invoked – calling agent.cancel()"); agent.cancel(); setLoading(false); @@ -544,13 +528,11 @@ export default function TerminalChat({ currentModel={model} hasLastResponse={Boolean(lastResponseId)} onSelect={(newModel) => { - if (isLoggingEnabled()) { - log( - "TerminalChat: interruptAgent invoked – calling agent.cancel()", - ); - if (!agent) { - log("TerminalChat: agent is not ready yet"); - } + log( + "TerminalChat: interruptAgent invoked – calling agent.cancel()", + ); + if (!agent) { + log("TerminalChat: agent is not ready yet"); } agent?.cancel(); setLoading(false); diff --git a/codex-cli/src/utils/agent/agent-loop.ts b/codex-cli/src/utils/agent/agent-loop.ts index cbe3c56960..15c37475d4 100644 --- a/codex-cli/src/utils/agent/agent-loop.ts +++ b/codex-cli/src/utils/agent/agent-loop.ts @@ -8,7 +8,7 @@ import type { } from "openai/resources/responses/responses.mjs"; import type { Reasoning } from "openai/resources.mjs"; -import { log, isLoggingEnabled } from "./log.js"; +import { log } from "./log.js"; import { OPENAI_BASE_URL, OPENAI_TIMEOUT_MS } from "../config.js"; import { parseToolCallArguments } from "../parsers.js"; import { @@ -116,15 +116,13 @@ export class AgentLoop { // Reset the current stream to allow new requests this.currentStream = null; - if (isLoggingEnabled()) { - log( - `AgentLoop.cancel() invoked – currentStream=${Boolean( - this.currentStream, - )} execAbortController=${Boolean( - this.execAbortController, - )} generation=${this.generation}`, - ); - } + log( + `AgentLoop.cancel() invoked – currentStream=${Boolean( + this.currentStream, + )} execAbortController=${Boolean( + this.execAbortController, + )} generation=${this.generation}`, + ); ( this.currentStream as { controller?: { abort?: () => void } } | null )?.controller?.abort?.(); @@ -136,9 +134,7 @@ export class AgentLoop { // Create a new abort controller for future tool calls this.execAbortController = new AbortController(); - if (isLoggingEnabled()) { - log("AgentLoop.cancel(): execAbortController.abort() called"); - } + log("AgentLoop.cancel(): execAbortController.abort() called"); // NOTE: We intentionally do *not* clear `lastResponseId` here. If the // stream produced a `function_call` before the user cancelled, OpenAI now @@ -174,9 +170,7 @@ export class AgentLoop { // this.onItem(cancelNotice); this.generation += 1; - if (isLoggingEnabled()) { - log(`AgentLoop.cancel(): generation bumped to ${this.generation}`); - } + log(`AgentLoop.cancel(): generation bumped to ${this.generation}`); } /** @@ -315,13 +309,11 @@ export class AgentLoop { const callId: string = (item as any).call_id ?? (item as any).id; const args = parseToolCallArguments(rawArguments ?? "{}"); - if (isLoggingEnabled()) { - log( - `handleFunctionCall(): name=${ - name ?? "undefined" - } callId=${callId} args=${rawArguments}`, - ); - } + log( + `handleFunctionCall(): name=${ + name ?? "undefined" + } callId=${callId} args=${rawArguments}`, + ); if (args == null) { const outputItem: ResponseInputItem.FunctionCallOutput = { @@ -407,11 +399,9 @@ export class AgentLoop { // Create a fresh AbortController for this run so that tool calls from a // previous run do not accidentally get signalled. this.execAbortController = new AbortController(); - if (isLoggingEnabled()) { - log( - `AgentLoop.run(): new execAbortController created (${this.execAbortController.signal}) for generation ${this.generation}`, - ); - } + log( + `AgentLoop.run(): new execAbortController created (${this.execAbortController.signal}) for generation ${this.generation}`, + ); // NOTE: We no longer (re‑)attach an `abort` listener to `hardAbort` here. // A single listener that forwards the `abort` to the current // `execAbortController` is installed once in the constructor. Re‑adding a @@ -502,11 +492,9 @@ export class AgentLoop { const mergedInstructions = [prefix, this.instructions] .filter(Boolean) .join("\n"); - if (isLoggingEnabled()) { - log( - `instructions (length ${mergedInstructions.length}): ${mergedInstructions}`, - ); - } + log( + `instructions (length ${mergedInstructions.length}): ${mergedInstructions}`, + ); // eslint-disable-next-line no-await-in-loop stream = await this.oai.responses.create({ model: this.model, @@ -733,9 +721,7 @@ export class AgentLoop { try { // eslint-disable-next-line no-await-in-loop for await (const event of stream) { - if (isLoggingEnabled()) { - log(`AgentLoop.run(): response event ${event.type}`); - } + log(`AgentLoop.run(): response event ${event.type}`); // process and surface each item (no‑op until we can depend on streaming events) if (event.type === "response.output_item.done") { diff --git a/codex-cli/src/utils/agent/handle-exec-command.ts b/codex-cli/src/utils/agent/handle-exec-command.ts index 1af390e226..97b78c0817 100644 --- a/codex-cli/src/utils/agent/handle-exec-command.ts +++ b/codex-cli/src/utils/agent/handle-exec-command.ts @@ -223,23 +223,22 @@ async function execCommand( workdir = process.cwd(); } } - if (isLoggingEnabled()) { - if (applyPatchCommand != null) { - log("EXEC running apply_patch command"); - } else { - const { cmd, timeoutInMillis } = execInput; - // Seconds are a bit easier to read in log messages and most timeouts - // are specified as multiples of 1000, anyway. - const timeout = - timeoutInMillis != null - ? Math.round(timeoutInMillis / 1000).toString() - : "undefined"; - log( - `EXEC running \`${formatCommandForDisplay( - cmd, - )}\` in workdir=${workdir} with timeout=${timeout}s`, - ); - } + + if (applyPatchCommand != null) { + log("EXEC running apply_patch command"); + } else if (isLoggingEnabled()) { + const { cmd, timeoutInMillis } = execInput; + // Seconds are a bit easier to read in log messages and most timeouts + // are specified as multiples of 1000, anyway. + const timeout = + timeoutInMillis != null + ? Math.round(timeoutInMillis / 1000).toString() + : "undefined"; + log( + `EXEC running \`${formatCommandForDisplay( + cmd, + )}\` in workdir=${workdir} with timeout=${timeout}s`, + ); } // Note execApplyPatch() and exec() are coded defensively and should not diff --git a/codex-cli/src/utils/agent/log.ts b/codex-cli/src/utils/agent/log.ts index e804386566..e4aca06c79 100644 --- a/codex-cli/src/utils/agent/log.ts +++ b/codex-cli/src/utils/agent/log.ts @@ -124,6 +124,14 @@ export function log(message: string): void { (logger ?? initLogger()).log(message); } +/** + * USE SPARINGLY! This function should only be used to guard a call to log() if + * the log message is large and you want to avoid constructing it if logging is + * disabled. + * + * `log()` is already a no-op if DEBUG is not set, so an extra + * `isLoggingEnabled()` check is unnecessary. + */ export function isLoggingEnabled(): boolean { return (logger ?? initLogger()).isLoggingEnabled(); } diff --git a/codex-cli/src/utils/agent/platform-commands.ts b/codex-cli/src/utils/agent/platform-commands.ts index 7be02c7ac5..085c575d44 100644 --- a/codex-cli/src/utils/agent/platform-commands.ts +++ b/codex-cli/src/utils/agent/platform-commands.ts @@ -2,7 +2,7 @@ * Utility functions for handling platform-specific commands */ -import { log, isLoggingEnabled } from "./log.js"; +import { log } from "./log.js"; /** * Map of Unix commands to their Windows equivalents @@ -59,9 +59,7 @@ export function adaptCommandForPlatform(command: Array): Array { return command; } - if (isLoggingEnabled()) { - log(`Adapting command '${cmd}' for Windows platform`); - } + log(`Adapting command '${cmd}' for Windows platform`); // Create a new command array with the adapted command const adaptedCommand = [...command]; @@ -78,9 +76,7 @@ export function adaptCommandForPlatform(command: Array): Array { } } - if (isLoggingEnabled()) { - log(`Adapted command: ${adaptedCommand.join(" ")}`); - } + log(`Adapted command: ${adaptedCommand.join(" ")}`); return adaptedCommand; } diff --git a/codex-cli/src/utils/agent/sandbox/raw-exec.ts b/codex-cli/src/utils/agent/sandbox/raw-exec.ts index 6cfb304731..f719cafdb1 100644 --- a/codex-cli/src/utils/agent/sandbox/raw-exec.ts +++ b/codex-cli/src/utils/agent/sandbox/raw-exec.ts @@ -7,7 +7,7 @@ import type { StdioPipe, } from "child_process"; -import { log, isLoggingEnabled } from "../log.js"; +import { log } from "../log.js"; import { adaptCommandForPlatform } from "../platform-commands.js"; import { spawn } from "child_process"; import * as os from "os"; @@ -28,13 +28,10 @@ export function exec( const adaptedCommand = adaptCommandForPlatform(command); if ( - isLoggingEnabled() && JSON.stringify(adaptedCommand) !== JSON.stringify(command) ) { log( - `Command adapted for platform: ${command.join( - " ", - )} -> ${adaptedCommand.join(" ")}`, + `Command adapted for platform: ${command.join(" ")} -> ${adaptedCommand.join(" ")}`, ); } @@ -95,9 +92,7 @@ export function exec( // timely fashion. if (abortSignal) { const abortHandler = () => { - if (isLoggingEnabled()) { - log(`raw-exec: abort signal received – killing child ${child.pid}`); - } + log(`raw-exec: abort signal received – killing child ${child.pid}`); const killTarget = (signal: NodeJS.Signals) => { if (!child.pid) { return; @@ -194,11 +189,9 @@ export function exec( exitCode = 1; } - if (isLoggingEnabled()) { - log( - `raw-exec: child ${child.pid} exited code=${exitCode} signal=${signal}`, - ); - } + log( + `raw-exec: child ${child.pid} exited code=${exitCode} signal=${signal}`, + ); resolve({ stdout, stderr, diff --git a/codex-cli/src/utils/config.ts b/codex-cli/src/utils/config.ts index 8467653580..3be77059e0 100644 --- a/codex-cli/src/utils/config.ts +++ b/codex-cli/src/utils/config.ts @@ -8,7 +8,7 @@ import type { FullAutoErrorMode } from "./auto-approval-mode.js"; -import { log, isLoggingEnabled } from "./agent/log.js"; +import { log } from "./agent/log.js"; import { AutoApprovalMode } from "./auto-approval-mode.js"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; import { load as loadYaml, dump as dumpYaml } from "js-yaml"; @@ -245,15 +245,11 @@ export const loadConfig = ( ? resolvePath(cwd, options.projectDocPath) : discoverProjectDocPath(cwd); if (projectDocPath) { - if (isLoggingEnabled()) { - log( - `[codex] Loaded project doc from ${projectDocPath} (${projectDoc.length} bytes)`, - ); - } + log( + `[codex] Loaded project doc from ${projectDocPath} (${projectDoc.length} bytes)`, + ); } else { - if (isLoggingEnabled()) { - log(`[codex] No project doc found in ${cwd}`); - } + log(`[codex] No project doc found in ${cwd}`); } } From f77625ea881bcf5d51bee68409a62c01db18c6b7 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Sat, 19 Apr 2025 11:07:35 -0700 Subject: [PATCH 19/41] remove unnecessary isLoggingEnabled() checks --- .../chat/terminal-chat-input-thinking.tsx | 18 ++---- .../components/chat/terminal-chat-input.tsx | 18 ++---- .../chat/terminal-chat-new-input.tsx | 18 ++---- .../src/components/chat/terminal-chat.tsx | 64 +++++++------------ codex-cli/src/utils/agent/agent-loop.ts | 58 +++++++---------- .../src/utils/agent/handle-exec-command.ts | 33 +++++----- codex-cli/src/utils/agent/log.ts | 8 +++ .../src/utils/agent/platform-commands.ts | 10 +-- codex-cli/src/utils/agent/sandbox/raw-exec.ts | 19 ++---- codex-cli/src/utils/config.ts | 14 ++-- 10 files changed, 102 insertions(+), 158 deletions(-) diff --git a/codex-cli/src/components/chat/terminal-chat-input-thinking.tsx b/codex-cli/src/components/chat/terminal-chat-input-thinking.tsx index 213dd8c9af..c0b305ef14 100644 --- a/codex-cli/src/components/chat/terminal-chat-input-thinking.tsx +++ b/codex-cli/src/components/chat/terminal-chat-input-thinking.tsx @@ -1,4 +1,4 @@ -import { log, isLoggingEnabled } from "../../utils/agent/log.js"; +import { log } from "../../utils/agent/log.js"; // removed unused isLoggingEnabled import import { Box, Text, useInput, useStdin } from "ink"; import React, { useState } from "react"; import { useInterval } from "use-interval"; @@ -40,11 +40,9 @@ export default function TerminalChatInputThinking({ const str = Buffer.isBuffer(data) ? data.toString("utf8") : data; if (str === "\x1b\x1b") { - if (isLoggingEnabled()) { - log( - "raw stdin: received collapsed ESC ESC – starting confirmation timer", - ); - } + log( + "raw stdin: received collapsed ESC ESC – starting confirmation timer", + ); setAwaitingConfirm(true); setTimeout(() => setAwaitingConfirm(false), 1500); } @@ -65,15 +63,11 @@ export default function TerminalChatInputThinking({ } if (awaitingConfirm) { - if (isLoggingEnabled()) { - log("useInput: second ESC detected – triggering onInterrupt()"); - } + log("useInput: second ESC detected – triggering onInterrupt()"); onInterrupt(); setAwaitingConfirm(false); } else { - if (isLoggingEnabled()) { - log("useInput: first ESC detected – waiting for confirmation"); - } + log("useInput: first ESC detected – waiting for confirmation"); setAwaitingConfirm(true); setTimeout(() => setAwaitingConfirm(false), 1500); } diff --git a/codex-cli/src/components/chat/terminal-chat-input.tsx b/codex-cli/src/components/chat/terminal-chat-input.tsx index 59265221d4..943637dba5 100644 --- a/codex-cli/src/components/chat/terminal-chat-input.tsx +++ b/codex-cli/src/components/chat/terminal-chat-input.tsx @@ -6,7 +6,7 @@ import type { } from "openai/resources/responses/responses.mjs"; import { TerminalChatCommandReview } from "./terminal-chat-command-review.js"; -import { log, isLoggingEnabled } from "../../utils/agent/log.js"; +import { log } from "../../utils/agent/log.js"; import { loadConfig } from "../../utils/config.js"; import { createInputItem } from "../../utils/input-utils.js"; import { setSessionId } from "../../utils/session.js"; @@ -683,11 +683,9 @@ function TerminalChatInputThinking({ const str = Buffer.isBuffer(data) ? data.toString("utf8") : data; if (str === "\x1b\x1b") { // Treat as the first Escape press – prompt the user for confirmation. - if (isLoggingEnabled()) { - log( - "raw stdin: received collapsed ESC ESC – starting confirmation timer", - ); - } + log( + "raw stdin: received collapsed ESC ESC – starting confirmation timer", + ); setAwaitingConfirm(true); setTimeout(() => setAwaitingConfirm(false), 1500); } @@ -712,15 +710,11 @@ function TerminalChatInputThinking({ } if (awaitingConfirm) { - if (isLoggingEnabled()) { - log("useInput: second ESC detected – triggering onInterrupt()"); - } + log("useInput: second ESC detected – triggering onInterrupt()"); onInterrupt(); setAwaitingConfirm(false); } else { - if (isLoggingEnabled()) { - log("useInput: first ESC detected – waiting for confirmation"); - } + log("useInput: first ESC detected – waiting for confirmation"); setAwaitingConfirm(true); setTimeout(() => setAwaitingConfirm(false), 1500); } diff --git a/codex-cli/src/components/chat/terminal-chat-new-input.tsx b/codex-cli/src/components/chat/terminal-chat-new-input.tsx index 9ceb4bbccc..a4558c94be 100644 --- a/codex-cli/src/components/chat/terminal-chat-new-input.tsx +++ b/codex-cli/src/components/chat/terminal-chat-new-input.tsx @@ -8,7 +8,7 @@ import type { import MultilineTextEditor from "./multiline-editor"; import { TerminalChatCommandReview } from "./terminal-chat-command-review.js"; -import { log, isLoggingEnabled } from "../../utils/agent/log.js"; +import { log } from "../../utils/agent/log.js"; import { loadConfig } from "../../utils/config.js"; import { createInputItem } from "../../utils/input-utils.js"; import { setSessionId } from "../../utils/session.js"; @@ -496,11 +496,9 @@ function TerminalChatInputThinking({ const str = Buffer.isBuffer(data) ? data.toString("utf8") : data; if (str === "\x1b\x1b") { // Treat as the first Escape press – prompt the user for confirmation. - if (isLoggingEnabled()) { - log( - "raw stdin: received collapsed ESC ESC – starting confirmation timer", - ); - } + log( + "raw stdin: received collapsed ESC ESC – starting confirmation timer", + ); setAwaitingConfirm(true); setTimeout(() => setAwaitingConfirm(false), 1500); } @@ -522,15 +520,11 @@ function TerminalChatInputThinking({ } if (awaitingConfirm) { - if (isLoggingEnabled()) { - log("useInput: second ESC detected – triggering onInterrupt()"); - } + log("useInput: second ESC detected – triggering onInterrupt()"); onInterrupt(); setAwaitingConfirm(false); } else { - if (isLoggingEnabled()) { - log("useInput: first ESC detected – waiting for confirmation"); - } + log("useInput: first ESC detected – waiting for confirmation"); setAwaitingConfirm(true); setTimeout(() => setAwaitingConfirm(false), 1500); } diff --git a/codex-cli/src/components/chat/terminal-chat.tsx b/codex-cli/src/components/chat/terminal-chat.tsx index f228ed19c4..9ed1568dd8 100644 --- a/codex-cli/src/components/chat/terminal-chat.tsx +++ b/codex-cli/src/components/chat/terminal-chat.tsx @@ -15,7 +15,7 @@ import { formatCommandForDisplay } from "../../format-command.js"; import { useConfirmation } from "../../hooks/use-confirmation.js"; import { useTerminalSize } from "../../hooks/use-terminal-size.js"; import { AgentLoop } from "../../utils/agent/agent-loop.js"; -import { isLoggingEnabled, log } from "../../utils/agent/log.js"; +import { log } from "../../utils/agent/log.js"; import { ReviewDecision } from "../../utils/agent/review.js"; import { generateCompactSummary } from "../../utils/compact-summary.js"; import { OPENAI_BASE_URL } from "../../utils/config.js"; @@ -197,30 +197,24 @@ export default function TerminalChat({ // ──────────────────────────────────────────────────────────────── // DEBUG: log every render w/ key bits of state // ──────────────────────────────────────────────────────────────── - if (isLoggingEnabled()) { - log( - `render – agent? ${Boolean(agentRef.current)} loading=${loading} items=${ - items.length - }`, - ); - } + log( + `render – agent? ${Boolean(agentRef.current)} loading=${loading} items=${ + items.length + }`, + ); useEffect(() => { // Skip recreating the agent if awaiting a decision on a pending confirmation if (confirmationPrompt != null) { - if (isLoggingEnabled()) { - log("skip AgentLoop recreation due to pending confirmationPrompt"); - } + log("skip AgentLoop recreation due to pending confirmationPrompt"); return; } - if (isLoggingEnabled()) { - log("creating NEW AgentLoop"); - log( - `model=${model} instructions=${Boolean( - config.instructions, - )} approvalPolicy=${approvalPolicy}`, - ); - } + log("creating NEW AgentLoop"); + log( + `model=${model} instructions=${Boolean( + config.instructions, + )} approvalPolicy=${approvalPolicy}`, + ); // Tear down any existing loop before creating a new one agentRef.current?.terminate(); @@ -288,14 +282,10 @@ export default function TerminalChat({ // force a render so JSX below can "see" the freshly created agent forceUpdate(); - if (isLoggingEnabled()) { - log(`AgentLoop created: ${inspect(agentRef.current, { depth: 1 })}`); - } + log(`AgentLoop created: ${inspect(agentRef.current, { depth: 1 })}`); return () => { - if (isLoggingEnabled()) { - log("terminating AgentLoop"); - } + log("terminating AgentLoop"); agentRef.current?.terminate(); agentRef.current = undefined; forceUpdate(); // re‑render after teardown too @@ -377,9 +367,7 @@ export default function TerminalChat({ // Let's also track whenever the ref becomes available const agent = agentRef.current; useEffect(() => { - if (isLoggingEnabled()) { - log(`agentRef.current is now ${Boolean(agent)}`); - } + log(`agentRef.current is now ${Boolean(agent)}`); }, [agent]); // --------------------------------------------------------------------- @@ -504,11 +492,9 @@ export default function TerminalChat({ if (!agent) { return; } - if (isLoggingEnabled()) { - log( - "TerminalChat: interruptAgent invoked – calling agent.cancel()", - ); - } + log( + "TerminalChat: interruptAgent invoked – calling agent.cancel()", + ); agent.cancel(); setLoading(false); @@ -544,13 +530,11 @@ export default function TerminalChat({ currentModel={model} hasLastResponse={Boolean(lastResponseId)} onSelect={(newModel) => { - if (isLoggingEnabled()) { - log( - "TerminalChat: interruptAgent invoked – calling agent.cancel()", - ); - if (!agent) { - log("TerminalChat: agent is not ready yet"); - } + log( + "TerminalChat: interruptAgent invoked – calling agent.cancel()", + ); + if (!agent) { + log("TerminalChat: agent is not ready yet"); } agent?.cancel(); setLoading(false); diff --git a/codex-cli/src/utils/agent/agent-loop.ts b/codex-cli/src/utils/agent/agent-loop.ts index cbe3c56960..172ba67295 100644 --- a/codex-cli/src/utils/agent/agent-loop.ts +++ b/codex-cli/src/utils/agent/agent-loop.ts @@ -8,7 +8,7 @@ import type { } from "openai/resources/responses/responses.mjs"; import type { Reasoning } from "openai/resources.mjs"; -import { log, isLoggingEnabled } from "./log.js"; +import { log } from "./log.js"; import { OPENAI_BASE_URL, OPENAI_TIMEOUT_MS } from "../config.js"; import { parseToolCallArguments } from "../parsers.js"; import { @@ -116,15 +116,13 @@ export class AgentLoop { // Reset the current stream to allow new requests this.currentStream = null; - if (isLoggingEnabled()) { - log( - `AgentLoop.cancel() invoked – currentStream=${Boolean( - this.currentStream, - )} execAbortController=${Boolean( - this.execAbortController, - )} generation=${this.generation}`, - ); - } + log( + `AgentLoop.cancel() invoked – currentStream=${Boolean( + this.currentStream, + )} execAbortController=${Boolean(this.execAbortController)} generation=${ + this.generation + }`, + ); ( this.currentStream as { controller?: { abort?: () => void } } | null )?.controller?.abort?.(); @@ -136,9 +134,7 @@ export class AgentLoop { // Create a new abort controller for future tool calls this.execAbortController = new AbortController(); - if (isLoggingEnabled()) { - log("AgentLoop.cancel(): execAbortController.abort() called"); - } + log("AgentLoop.cancel(): execAbortController.abort() called"); // NOTE: We intentionally do *not* clear `lastResponseId` here. If the // stream produced a `function_call` before the user cancelled, OpenAI now @@ -174,9 +170,7 @@ export class AgentLoop { // this.onItem(cancelNotice); this.generation += 1; - if (isLoggingEnabled()) { - log(`AgentLoop.cancel(): generation bumped to ${this.generation}`); - } + log(`AgentLoop.cancel(): generation bumped to ${this.generation}`); } /** @@ -315,13 +309,11 @@ export class AgentLoop { const callId: string = (item as any).call_id ?? (item as any).id; const args = parseToolCallArguments(rawArguments ?? "{}"); - if (isLoggingEnabled()) { - log( - `handleFunctionCall(): name=${ - name ?? "undefined" - } callId=${callId} args=${rawArguments}`, - ); - } + log( + `handleFunctionCall(): name=${ + name ?? "undefined" + } callId=${callId} args=${rawArguments}`, + ); if (args == null) { const outputItem: ResponseInputItem.FunctionCallOutput = { @@ -407,11 +399,9 @@ export class AgentLoop { // Create a fresh AbortController for this run so that tool calls from a // previous run do not accidentally get signalled. this.execAbortController = new AbortController(); - if (isLoggingEnabled()) { - log( - `AgentLoop.run(): new execAbortController created (${this.execAbortController.signal}) for generation ${this.generation}`, - ); - } + log( + `AgentLoop.run(): new execAbortController created (${this.execAbortController.signal}) for generation ${this.generation}`, + ); // NOTE: We no longer (re‑)attach an `abort` listener to `hardAbort` here. // A single listener that forwards the `abort` to the current // `execAbortController` is installed once in the constructor. Re‑adding a @@ -502,11 +492,9 @@ export class AgentLoop { const mergedInstructions = [prefix, this.instructions] .filter(Boolean) .join("\n"); - if (isLoggingEnabled()) { - log( - `instructions (length ${mergedInstructions.length}): ${mergedInstructions}`, - ); - } + log( + `instructions (length ${mergedInstructions.length}): ${mergedInstructions}`, + ); // eslint-disable-next-line no-await-in-loop stream = await this.oai.responses.create({ model: this.model, @@ -733,9 +721,7 @@ export class AgentLoop { try { // eslint-disable-next-line no-await-in-loop for await (const event of stream) { - if (isLoggingEnabled()) { - log(`AgentLoop.run(): response event ${event.type}`); - } + log(`AgentLoop.run(): response event ${event.type}`); // process and surface each item (no‑op until we can depend on streaming events) if (event.type === "response.output_item.done") { diff --git a/codex-cli/src/utils/agent/handle-exec-command.ts b/codex-cli/src/utils/agent/handle-exec-command.ts index 1af390e226..97b78c0817 100644 --- a/codex-cli/src/utils/agent/handle-exec-command.ts +++ b/codex-cli/src/utils/agent/handle-exec-command.ts @@ -223,23 +223,22 @@ async function execCommand( workdir = process.cwd(); } } - if (isLoggingEnabled()) { - if (applyPatchCommand != null) { - log("EXEC running apply_patch command"); - } else { - const { cmd, timeoutInMillis } = execInput; - // Seconds are a bit easier to read in log messages and most timeouts - // are specified as multiples of 1000, anyway. - const timeout = - timeoutInMillis != null - ? Math.round(timeoutInMillis / 1000).toString() - : "undefined"; - log( - `EXEC running \`${formatCommandForDisplay( - cmd, - )}\` in workdir=${workdir} with timeout=${timeout}s`, - ); - } + + if (applyPatchCommand != null) { + log("EXEC running apply_patch command"); + } else if (isLoggingEnabled()) { + const { cmd, timeoutInMillis } = execInput; + // Seconds are a bit easier to read in log messages and most timeouts + // are specified as multiples of 1000, anyway. + const timeout = + timeoutInMillis != null + ? Math.round(timeoutInMillis / 1000).toString() + : "undefined"; + log( + `EXEC running \`${formatCommandForDisplay( + cmd, + )}\` in workdir=${workdir} with timeout=${timeout}s`, + ); } // Note execApplyPatch() and exec() are coded defensively and should not diff --git a/codex-cli/src/utils/agent/log.ts b/codex-cli/src/utils/agent/log.ts index e804386566..e4aca06c79 100644 --- a/codex-cli/src/utils/agent/log.ts +++ b/codex-cli/src/utils/agent/log.ts @@ -124,6 +124,14 @@ export function log(message: string): void { (logger ?? initLogger()).log(message); } +/** + * USE SPARINGLY! This function should only be used to guard a call to log() if + * the log message is large and you want to avoid constructing it if logging is + * disabled. + * + * `log()` is already a no-op if DEBUG is not set, so an extra + * `isLoggingEnabled()` check is unnecessary. + */ export function isLoggingEnabled(): boolean { return (logger ?? initLogger()).isLoggingEnabled(); } diff --git a/codex-cli/src/utils/agent/platform-commands.ts b/codex-cli/src/utils/agent/platform-commands.ts index 7be02c7ac5..085c575d44 100644 --- a/codex-cli/src/utils/agent/platform-commands.ts +++ b/codex-cli/src/utils/agent/platform-commands.ts @@ -2,7 +2,7 @@ * Utility functions for handling platform-specific commands */ -import { log, isLoggingEnabled } from "./log.js"; +import { log } from "./log.js"; /** * Map of Unix commands to their Windows equivalents @@ -59,9 +59,7 @@ export function adaptCommandForPlatform(command: Array): Array { return command; } - if (isLoggingEnabled()) { - log(`Adapting command '${cmd}' for Windows platform`); - } + log(`Adapting command '${cmd}' for Windows platform`); // Create a new command array with the adapted command const adaptedCommand = [...command]; @@ -78,9 +76,7 @@ export function adaptCommandForPlatform(command: Array): Array { } } - if (isLoggingEnabled()) { - log(`Adapted command: ${adaptedCommand.join(" ")}`); - } + log(`Adapted command: ${adaptedCommand.join(" ")}`); return adaptedCommand; } diff --git a/codex-cli/src/utils/agent/sandbox/raw-exec.ts b/codex-cli/src/utils/agent/sandbox/raw-exec.ts index 6cfb304731..f2ca03bf49 100644 --- a/codex-cli/src/utils/agent/sandbox/raw-exec.ts +++ b/codex-cli/src/utils/agent/sandbox/raw-exec.ts @@ -7,7 +7,7 @@ import type { StdioPipe, } from "child_process"; -import { log, isLoggingEnabled } from "../log.js"; +import { log } from "../log.js"; import { adaptCommandForPlatform } from "../platform-commands.js"; import { spawn } from "child_process"; import * as os from "os"; @@ -27,10 +27,7 @@ export function exec( // Adapt command for the current platform (e.g., convert 'ls' to 'dir' on Windows) const adaptedCommand = adaptCommandForPlatform(command); - if ( - isLoggingEnabled() && - JSON.stringify(adaptedCommand) !== JSON.stringify(command) - ) { + if (JSON.stringify(adaptedCommand) !== JSON.stringify(command)) { log( `Command adapted for platform: ${command.join( " ", @@ -95,9 +92,7 @@ export function exec( // timely fashion. if (abortSignal) { const abortHandler = () => { - if (isLoggingEnabled()) { - log(`raw-exec: abort signal received – killing child ${child.pid}`); - } + log(`raw-exec: abort signal received – killing child ${child.pid}`); const killTarget = (signal: NodeJS.Signals) => { if (!child.pid) { return; @@ -194,11 +189,9 @@ export function exec( exitCode = 1; } - if (isLoggingEnabled()) { - log( - `raw-exec: child ${child.pid} exited code=${exitCode} signal=${signal}`, - ); - } + log( + `raw-exec: child ${child.pid} exited code=${exitCode} signal=${signal}`, + ); resolve({ stdout, stderr, diff --git a/codex-cli/src/utils/config.ts b/codex-cli/src/utils/config.ts index 8467653580..3be77059e0 100644 --- a/codex-cli/src/utils/config.ts +++ b/codex-cli/src/utils/config.ts @@ -8,7 +8,7 @@ import type { FullAutoErrorMode } from "./auto-approval-mode.js"; -import { log, isLoggingEnabled } from "./agent/log.js"; +import { log } from "./agent/log.js"; import { AutoApprovalMode } from "./auto-approval-mode.js"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; import { load as loadYaml, dump as dumpYaml } from "js-yaml"; @@ -245,15 +245,11 @@ export const loadConfig = ( ? resolvePath(cwd, options.projectDocPath) : discoverProjectDocPath(cwd); if (projectDocPath) { - if (isLoggingEnabled()) { - log( - `[codex] Loaded project doc from ${projectDocPath} (${projectDoc.length} bytes)`, - ); - } + log( + `[codex] Loaded project doc from ${projectDocPath} (${projectDoc.length} bytes)`, + ); } else { - if (isLoggingEnabled()) { - log(`[codex] No project doc found in ${cwd}`); - } + log(`[codex] No project doc found in ${cwd}`); } } From bce7ab2feafe7c919f507051af02ec5d39247b01 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Sat, 19 Apr 2025 11:07:35 -0700 Subject: [PATCH 20/41] remove unnecessary isLoggingEnabled() checks --- .../chat/terminal-chat-input-thinking.tsx | 18 ++--- .../components/chat/terminal-chat-input.tsx | 18 ++--- .../chat/terminal-chat-new-input.tsx | 18 ++--- .../src/components/chat/terminal-chat.tsx | 65 +++++++------------ codex-cli/src/utils/agent/agent-loop.ts | 58 +++++++---------- .../src/utils/agent/handle-exec-command.ts | 33 +++++----- codex-cli/src/utils/agent/log.ts | 8 +++ .../src/utils/agent/platform-commands.ts | 10 +-- codex-cli/src/utils/agent/sandbox/raw-exec.ts | 19 ++---- codex-cli/src/utils/config.ts | 14 ++-- 10 files changed, 103 insertions(+), 158 deletions(-) diff --git a/codex-cli/src/components/chat/terminal-chat-input-thinking.tsx b/codex-cli/src/components/chat/terminal-chat-input-thinking.tsx index 213dd8c9af..fdc8bd218c 100644 --- a/codex-cli/src/components/chat/terminal-chat-input-thinking.tsx +++ b/codex-cli/src/components/chat/terminal-chat-input-thinking.tsx @@ -1,4 +1,4 @@ -import { log, isLoggingEnabled } from "../../utils/agent/log.js"; +import { log } from "../../utils/agent/log.js"; import { Box, Text, useInput, useStdin } from "ink"; import React, { useState } from "react"; import { useInterval } from "use-interval"; @@ -40,11 +40,9 @@ export default function TerminalChatInputThinking({ const str = Buffer.isBuffer(data) ? data.toString("utf8") : data; if (str === "\x1b\x1b") { - if (isLoggingEnabled()) { - log( - "raw stdin: received collapsed ESC ESC – starting confirmation timer", - ); - } + log( + "raw stdin: received collapsed ESC ESC – starting confirmation timer", + ); setAwaitingConfirm(true); setTimeout(() => setAwaitingConfirm(false), 1500); } @@ -65,15 +63,11 @@ export default function TerminalChatInputThinking({ } if (awaitingConfirm) { - if (isLoggingEnabled()) { - log("useInput: second ESC detected – triggering onInterrupt()"); - } + log("useInput: second ESC detected – triggering onInterrupt()"); onInterrupt(); setAwaitingConfirm(false); } else { - if (isLoggingEnabled()) { - log("useInput: first ESC detected – waiting for confirmation"); - } + log("useInput: first ESC detected – waiting for confirmation"); setAwaitingConfirm(true); setTimeout(() => setAwaitingConfirm(false), 1500); } diff --git a/codex-cli/src/components/chat/terminal-chat-input.tsx b/codex-cli/src/components/chat/terminal-chat-input.tsx index 59265221d4..943637dba5 100644 --- a/codex-cli/src/components/chat/terminal-chat-input.tsx +++ b/codex-cli/src/components/chat/terminal-chat-input.tsx @@ -6,7 +6,7 @@ import type { } from "openai/resources/responses/responses.mjs"; import { TerminalChatCommandReview } from "./terminal-chat-command-review.js"; -import { log, isLoggingEnabled } from "../../utils/agent/log.js"; +import { log } from "../../utils/agent/log.js"; import { loadConfig } from "../../utils/config.js"; import { createInputItem } from "../../utils/input-utils.js"; import { setSessionId } from "../../utils/session.js"; @@ -683,11 +683,9 @@ function TerminalChatInputThinking({ const str = Buffer.isBuffer(data) ? data.toString("utf8") : data; if (str === "\x1b\x1b") { // Treat as the first Escape press – prompt the user for confirmation. - if (isLoggingEnabled()) { - log( - "raw stdin: received collapsed ESC ESC – starting confirmation timer", - ); - } + log( + "raw stdin: received collapsed ESC ESC – starting confirmation timer", + ); setAwaitingConfirm(true); setTimeout(() => setAwaitingConfirm(false), 1500); } @@ -712,15 +710,11 @@ function TerminalChatInputThinking({ } if (awaitingConfirm) { - if (isLoggingEnabled()) { - log("useInput: second ESC detected – triggering onInterrupt()"); - } + log("useInput: second ESC detected – triggering onInterrupt()"); onInterrupt(); setAwaitingConfirm(false); } else { - if (isLoggingEnabled()) { - log("useInput: first ESC detected – waiting for confirmation"); - } + log("useInput: first ESC detected – waiting for confirmation"); setAwaitingConfirm(true); setTimeout(() => setAwaitingConfirm(false), 1500); } diff --git a/codex-cli/src/components/chat/terminal-chat-new-input.tsx b/codex-cli/src/components/chat/terminal-chat-new-input.tsx index 9ceb4bbccc..a4558c94be 100644 --- a/codex-cli/src/components/chat/terminal-chat-new-input.tsx +++ b/codex-cli/src/components/chat/terminal-chat-new-input.tsx @@ -8,7 +8,7 @@ import type { import MultilineTextEditor from "./multiline-editor"; import { TerminalChatCommandReview } from "./terminal-chat-command-review.js"; -import { log, isLoggingEnabled } from "../../utils/agent/log.js"; +import { log } from "../../utils/agent/log.js"; import { loadConfig } from "../../utils/config.js"; import { createInputItem } from "../../utils/input-utils.js"; import { setSessionId } from "../../utils/session.js"; @@ -496,11 +496,9 @@ function TerminalChatInputThinking({ const str = Buffer.isBuffer(data) ? data.toString("utf8") : data; if (str === "\x1b\x1b") { // Treat as the first Escape press – prompt the user for confirmation. - if (isLoggingEnabled()) { - log( - "raw stdin: received collapsed ESC ESC – starting confirmation timer", - ); - } + log( + "raw stdin: received collapsed ESC ESC – starting confirmation timer", + ); setAwaitingConfirm(true); setTimeout(() => setAwaitingConfirm(false), 1500); } @@ -522,15 +520,11 @@ function TerminalChatInputThinking({ } if (awaitingConfirm) { - if (isLoggingEnabled()) { - log("useInput: second ESC detected – triggering onInterrupt()"); - } + log("useInput: second ESC detected – triggering onInterrupt()"); onInterrupt(); setAwaitingConfirm(false); } else { - if (isLoggingEnabled()) { - log("useInput: first ESC detected – waiting for confirmation"); - } + log("useInput: first ESC detected – waiting for confirmation"); setAwaitingConfirm(true); setTimeout(() => setAwaitingConfirm(false), 1500); } diff --git a/codex-cli/src/components/chat/terminal-chat.tsx b/codex-cli/src/components/chat/terminal-chat.tsx index f228ed19c4..7602721652 100644 --- a/codex-cli/src/components/chat/terminal-chat.tsx +++ b/codex-cli/src/components/chat/terminal-chat.tsx @@ -15,7 +15,7 @@ import { formatCommandForDisplay } from "../../format-command.js"; import { useConfirmation } from "../../hooks/use-confirmation.js"; import { useTerminalSize } from "../../hooks/use-terminal-size.js"; import { AgentLoop } from "../../utils/agent/agent-loop.js"; -import { isLoggingEnabled, log } from "../../utils/agent/log.js"; +import { log } from "../../utils/agent/log.js"; import { ReviewDecision } from "../../utils/agent/review.js"; import { generateCompactSummary } from "../../utils/compact-summary.js"; import { OPENAI_BASE_URL } from "../../utils/config.js"; @@ -197,30 +197,25 @@ export default function TerminalChat({ // ──────────────────────────────────────────────────────────────── // DEBUG: log every render w/ key bits of state // ──────────────────────────────────────────────────────────────── - if (isLoggingEnabled()) { - log( - `render – agent? ${Boolean(agentRef.current)} loading=${loading} items=${ - items.length - }`, - ); - } + log( + `render – agent? ${Boolean(agentRef.current)} loading=${loading} items=${ + items.length + }`, + ); useEffect(() => { // Skip recreating the agent if awaiting a decision on a pending confirmation if (confirmationPrompt != null) { - if (isLoggingEnabled()) { - log("skip AgentLoop recreation due to pending confirmationPrompt"); - } + log("skip AgentLoop recreation due to pending confirmationPrompt"); return; } - if (isLoggingEnabled()) { - log("creating NEW AgentLoop"); - log( - `model=${model} instructions=${Boolean( - config.instructions, - )} approvalPolicy=${approvalPolicy}`, - ); - } + + log("creating NEW AgentLoop"); + log( + `model=${model} instructions=${Boolean( + config.instructions, + )} approvalPolicy=${approvalPolicy}`, + ); // Tear down any existing loop before creating a new one agentRef.current?.terminate(); @@ -288,14 +283,10 @@ export default function TerminalChat({ // force a render so JSX below can "see" the freshly created agent forceUpdate(); - if (isLoggingEnabled()) { - log(`AgentLoop created: ${inspect(agentRef.current, { depth: 1 })}`); - } + log(`AgentLoop created: ${inspect(agentRef.current, { depth: 1 })}`); return () => { - if (isLoggingEnabled()) { - log("terminating AgentLoop"); - } + log("terminating AgentLoop"); agentRef.current?.terminate(); agentRef.current = undefined; forceUpdate(); // re‑render after teardown too @@ -377,9 +368,7 @@ export default function TerminalChat({ // Let's also track whenever the ref becomes available const agent = agentRef.current; useEffect(() => { - if (isLoggingEnabled()) { - log(`agentRef.current is now ${Boolean(agent)}`); - } + log(`agentRef.current is now ${Boolean(agent)}`); }, [agent]); // --------------------------------------------------------------------- @@ -504,11 +493,9 @@ export default function TerminalChat({ if (!agent) { return; } - if (isLoggingEnabled()) { - log( - "TerminalChat: interruptAgent invoked – calling agent.cancel()", - ); - } + log( + "TerminalChat: interruptAgent invoked – calling agent.cancel()", + ); agent.cancel(); setLoading(false); @@ -544,13 +531,11 @@ export default function TerminalChat({ currentModel={model} hasLastResponse={Boolean(lastResponseId)} onSelect={(newModel) => { - if (isLoggingEnabled()) { - log( - "TerminalChat: interruptAgent invoked – calling agent.cancel()", - ); - if (!agent) { - log("TerminalChat: agent is not ready yet"); - } + log( + "TerminalChat: interruptAgent invoked – calling agent.cancel()", + ); + if (!agent) { + log("TerminalChat: agent is not ready yet"); } agent?.cancel(); setLoading(false); diff --git a/codex-cli/src/utils/agent/agent-loop.ts b/codex-cli/src/utils/agent/agent-loop.ts index cbe3c56960..172ba67295 100644 --- a/codex-cli/src/utils/agent/agent-loop.ts +++ b/codex-cli/src/utils/agent/agent-loop.ts @@ -8,7 +8,7 @@ import type { } from "openai/resources/responses/responses.mjs"; import type { Reasoning } from "openai/resources.mjs"; -import { log, isLoggingEnabled } from "./log.js"; +import { log } from "./log.js"; import { OPENAI_BASE_URL, OPENAI_TIMEOUT_MS } from "../config.js"; import { parseToolCallArguments } from "../parsers.js"; import { @@ -116,15 +116,13 @@ export class AgentLoop { // Reset the current stream to allow new requests this.currentStream = null; - if (isLoggingEnabled()) { - log( - `AgentLoop.cancel() invoked – currentStream=${Boolean( - this.currentStream, - )} execAbortController=${Boolean( - this.execAbortController, - )} generation=${this.generation}`, - ); - } + log( + `AgentLoop.cancel() invoked – currentStream=${Boolean( + this.currentStream, + )} execAbortController=${Boolean(this.execAbortController)} generation=${ + this.generation + }`, + ); ( this.currentStream as { controller?: { abort?: () => void } } | null )?.controller?.abort?.(); @@ -136,9 +134,7 @@ export class AgentLoop { // Create a new abort controller for future tool calls this.execAbortController = new AbortController(); - if (isLoggingEnabled()) { - log("AgentLoop.cancel(): execAbortController.abort() called"); - } + log("AgentLoop.cancel(): execAbortController.abort() called"); // NOTE: We intentionally do *not* clear `lastResponseId` here. If the // stream produced a `function_call` before the user cancelled, OpenAI now @@ -174,9 +170,7 @@ export class AgentLoop { // this.onItem(cancelNotice); this.generation += 1; - if (isLoggingEnabled()) { - log(`AgentLoop.cancel(): generation bumped to ${this.generation}`); - } + log(`AgentLoop.cancel(): generation bumped to ${this.generation}`); } /** @@ -315,13 +309,11 @@ export class AgentLoop { const callId: string = (item as any).call_id ?? (item as any).id; const args = parseToolCallArguments(rawArguments ?? "{}"); - if (isLoggingEnabled()) { - log( - `handleFunctionCall(): name=${ - name ?? "undefined" - } callId=${callId} args=${rawArguments}`, - ); - } + log( + `handleFunctionCall(): name=${ + name ?? "undefined" + } callId=${callId} args=${rawArguments}`, + ); if (args == null) { const outputItem: ResponseInputItem.FunctionCallOutput = { @@ -407,11 +399,9 @@ export class AgentLoop { // Create a fresh AbortController for this run so that tool calls from a // previous run do not accidentally get signalled. this.execAbortController = new AbortController(); - if (isLoggingEnabled()) { - log( - `AgentLoop.run(): new execAbortController created (${this.execAbortController.signal}) for generation ${this.generation}`, - ); - } + log( + `AgentLoop.run(): new execAbortController created (${this.execAbortController.signal}) for generation ${this.generation}`, + ); // NOTE: We no longer (re‑)attach an `abort` listener to `hardAbort` here. // A single listener that forwards the `abort` to the current // `execAbortController` is installed once in the constructor. Re‑adding a @@ -502,11 +492,9 @@ export class AgentLoop { const mergedInstructions = [prefix, this.instructions] .filter(Boolean) .join("\n"); - if (isLoggingEnabled()) { - log( - `instructions (length ${mergedInstructions.length}): ${mergedInstructions}`, - ); - } + log( + `instructions (length ${mergedInstructions.length}): ${mergedInstructions}`, + ); // eslint-disable-next-line no-await-in-loop stream = await this.oai.responses.create({ model: this.model, @@ -733,9 +721,7 @@ export class AgentLoop { try { // eslint-disable-next-line no-await-in-loop for await (const event of stream) { - if (isLoggingEnabled()) { - log(`AgentLoop.run(): response event ${event.type}`); - } + log(`AgentLoop.run(): response event ${event.type}`); // process and surface each item (no‑op until we can depend on streaming events) if (event.type === "response.output_item.done") { diff --git a/codex-cli/src/utils/agent/handle-exec-command.ts b/codex-cli/src/utils/agent/handle-exec-command.ts index 1af390e226..97b78c0817 100644 --- a/codex-cli/src/utils/agent/handle-exec-command.ts +++ b/codex-cli/src/utils/agent/handle-exec-command.ts @@ -223,23 +223,22 @@ async function execCommand( workdir = process.cwd(); } } - if (isLoggingEnabled()) { - if (applyPatchCommand != null) { - log("EXEC running apply_patch command"); - } else { - const { cmd, timeoutInMillis } = execInput; - // Seconds are a bit easier to read in log messages and most timeouts - // are specified as multiples of 1000, anyway. - const timeout = - timeoutInMillis != null - ? Math.round(timeoutInMillis / 1000).toString() - : "undefined"; - log( - `EXEC running \`${formatCommandForDisplay( - cmd, - )}\` in workdir=${workdir} with timeout=${timeout}s`, - ); - } + + if (applyPatchCommand != null) { + log("EXEC running apply_patch command"); + } else if (isLoggingEnabled()) { + const { cmd, timeoutInMillis } = execInput; + // Seconds are a bit easier to read in log messages and most timeouts + // are specified as multiples of 1000, anyway. + const timeout = + timeoutInMillis != null + ? Math.round(timeoutInMillis / 1000).toString() + : "undefined"; + log( + `EXEC running \`${formatCommandForDisplay( + cmd, + )}\` in workdir=${workdir} with timeout=${timeout}s`, + ); } // Note execApplyPatch() and exec() are coded defensively and should not diff --git a/codex-cli/src/utils/agent/log.ts b/codex-cli/src/utils/agent/log.ts index e804386566..e4aca06c79 100644 --- a/codex-cli/src/utils/agent/log.ts +++ b/codex-cli/src/utils/agent/log.ts @@ -124,6 +124,14 @@ export function log(message: string): void { (logger ?? initLogger()).log(message); } +/** + * USE SPARINGLY! This function should only be used to guard a call to log() if + * the log message is large and you want to avoid constructing it if logging is + * disabled. + * + * `log()` is already a no-op if DEBUG is not set, so an extra + * `isLoggingEnabled()` check is unnecessary. + */ export function isLoggingEnabled(): boolean { return (logger ?? initLogger()).isLoggingEnabled(); } diff --git a/codex-cli/src/utils/agent/platform-commands.ts b/codex-cli/src/utils/agent/platform-commands.ts index 7be02c7ac5..085c575d44 100644 --- a/codex-cli/src/utils/agent/platform-commands.ts +++ b/codex-cli/src/utils/agent/platform-commands.ts @@ -2,7 +2,7 @@ * Utility functions for handling platform-specific commands */ -import { log, isLoggingEnabled } from "./log.js"; +import { log } from "./log.js"; /** * Map of Unix commands to their Windows equivalents @@ -59,9 +59,7 @@ export function adaptCommandForPlatform(command: Array): Array { return command; } - if (isLoggingEnabled()) { - log(`Adapting command '${cmd}' for Windows platform`); - } + log(`Adapting command '${cmd}' for Windows platform`); // Create a new command array with the adapted command const adaptedCommand = [...command]; @@ -78,9 +76,7 @@ export function adaptCommandForPlatform(command: Array): Array { } } - if (isLoggingEnabled()) { - log(`Adapted command: ${adaptedCommand.join(" ")}`); - } + log(`Adapted command: ${adaptedCommand.join(" ")}`); return adaptedCommand; } diff --git a/codex-cli/src/utils/agent/sandbox/raw-exec.ts b/codex-cli/src/utils/agent/sandbox/raw-exec.ts index 6cfb304731..f2ca03bf49 100644 --- a/codex-cli/src/utils/agent/sandbox/raw-exec.ts +++ b/codex-cli/src/utils/agent/sandbox/raw-exec.ts @@ -7,7 +7,7 @@ import type { StdioPipe, } from "child_process"; -import { log, isLoggingEnabled } from "../log.js"; +import { log } from "../log.js"; import { adaptCommandForPlatform } from "../platform-commands.js"; import { spawn } from "child_process"; import * as os from "os"; @@ -27,10 +27,7 @@ export function exec( // Adapt command for the current platform (e.g., convert 'ls' to 'dir' on Windows) const adaptedCommand = adaptCommandForPlatform(command); - if ( - isLoggingEnabled() && - JSON.stringify(adaptedCommand) !== JSON.stringify(command) - ) { + if (JSON.stringify(adaptedCommand) !== JSON.stringify(command)) { log( `Command adapted for platform: ${command.join( " ", @@ -95,9 +92,7 @@ export function exec( // timely fashion. if (abortSignal) { const abortHandler = () => { - if (isLoggingEnabled()) { - log(`raw-exec: abort signal received – killing child ${child.pid}`); - } + log(`raw-exec: abort signal received – killing child ${child.pid}`); const killTarget = (signal: NodeJS.Signals) => { if (!child.pid) { return; @@ -194,11 +189,9 @@ export function exec( exitCode = 1; } - if (isLoggingEnabled()) { - log( - `raw-exec: child ${child.pid} exited code=${exitCode} signal=${signal}`, - ); - } + log( + `raw-exec: child ${child.pid} exited code=${exitCode} signal=${signal}`, + ); resolve({ stdout, stderr, diff --git a/codex-cli/src/utils/config.ts b/codex-cli/src/utils/config.ts index 8467653580..3be77059e0 100644 --- a/codex-cli/src/utils/config.ts +++ b/codex-cli/src/utils/config.ts @@ -8,7 +8,7 @@ import type { FullAutoErrorMode } from "./auto-approval-mode.js"; -import { log, isLoggingEnabled } from "./agent/log.js"; +import { log } from "./agent/log.js"; import { AutoApprovalMode } from "./auto-approval-mode.js"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; import { load as loadYaml, dump as dumpYaml } from "js-yaml"; @@ -245,15 +245,11 @@ export const loadConfig = ( ? resolvePath(cwd, options.projectDocPath) : discoverProjectDocPath(cwd); if (projectDocPath) { - if (isLoggingEnabled()) { - log( - `[codex] Loaded project doc from ${projectDocPath} (${projectDoc.length} bytes)`, - ); - } + log( + `[codex] Loaded project doc from ${projectDocPath} (${projectDoc.length} bytes)`, + ); } else { - if (isLoggingEnabled()) { - log(`[codex] No project doc found in ${cwd}`); - } + log(`[codex] No project doc found in ${cwd}`); } } From 75919d78fc019981dcece69664eeafad009405d7 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Sat, 19 Apr 2025 11:23:58 -0700 Subject: [PATCH 21/41] CONFIG_DIR should not be in the list of writable roots by default --- .../src/utils/agent/sandbox/macos-seatbelt.ts | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/codex-cli/src/utils/agent/sandbox/macos-seatbelt.ts b/codex-cli/src/utils/agent/sandbox/macos-seatbelt.ts index 0317458261..760ba63d8c 100644 --- a/codex-cli/src/utils/agent/sandbox/macos-seatbelt.ts +++ b/codex-cli/src/utils/agent/sandbox/macos-seatbelt.ts @@ -3,11 +3,9 @@ import type { SpawnOptions } from "child_process"; import { exec } from "./raw-exec.js"; import { log } from "../log.js"; -import { CONFIG_DIR } from "src/utils/config.js"; function getCommonRoots() { return [ - CONFIG_DIR, // Without this root, it'll cause: // pyenv: cannot rehash: $HOME/.pyenv/shims isn't writable `${process.env["HOME"]}/.pyenv`, @@ -17,16 +15,17 @@ function getCommonRoots() { export function execWithSeatbelt( cmd: Array, opts: SpawnOptions, - writableRoots: Array, + writableRoots: ReadonlyArray, abortSignal?: AbortSignal, ): Promise { let scopedWritePolicy: string; let policyTemplateParams: Array; - if (writableRoots.length > 0) { - // Add `~/.codex` to the list of writable roots - // (if there's any already, not in read-only mode) - getCommonRoots().map((root) => writableRoots.push(root)); - const { policies, params } = writableRoots + + const fullWritableRoots = [...writableRoots, ...getCommonRoots()]; + // In practice, fullWritableRoots will be non-empty, but we check just in + // case the logic to build up fullWritableRoots changes. + if (fullWritableRoots.length > 0) { + const { policies, params } = fullWritableRoots .map((root, index) => ({ policy: `(subpath (param "WRITABLE_ROOT_${index}"))`, param: `-DWRITABLE_ROOT_${index}=${root}`, From 7a16e239fc72822bcf1728c77df1569f56c6af6f Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Sat, 19 Apr 2025 11:23:58 -0700 Subject: [PATCH 22/41] remove unnecessary isLoggingEnabled() checks --- .../chat/terminal-chat-input-thinking.tsx | 18 ++--- .../components/chat/terminal-chat-input.tsx | 18 ++--- .../chat/terminal-chat-new-input.tsx | 18 ++--- .../src/components/chat/terminal-chat.tsx | 65 +++++++------------ codex-cli/src/utils/agent/agent-loop.ts | 58 +++++++---------- .../src/utils/agent/handle-exec-command.ts | 33 +++++----- codex-cli/src/utils/agent/log.ts | 8 +++ .../src/utils/agent/platform-commands.ts | 10 +-- codex-cli/src/utils/agent/sandbox/raw-exec.ts | 19 ++---- codex-cli/src/utils/config.ts | 14 ++-- 10 files changed, 103 insertions(+), 158 deletions(-) diff --git a/codex-cli/src/components/chat/terminal-chat-input-thinking.tsx b/codex-cli/src/components/chat/terminal-chat-input-thinking.tsx index 213dd8c9af..fdc8bd218c 100644 --- a/codex-cli/src/components/chat/terminal-chat-input-thinking.tsx +++ b/codex-cli/src/components/chat/terminal-chat-input-thinking.tsx @@ -1,4 +1,4 @@ -import { log, isLoggingEnabled } from "../../utils/agent/log.js"; +import { log } from "../../utils/agent/log.js"; import { Box, Text, useInput, useStdin } from "ink"; import React, { useState } from "react"; import { useInterval } from "use-interval"; @@ -40,11 +40,9 @@ export default function TerminalChatInputThinking({ const str = Buffer.isBuffer(data) ? data.toString("utf8") : data; if (str === "\x1b\x1b") { - if (isLoggingEnabled()) { - log( - "raw stdin: received collapsed ESC ESC – starting confirmation timer", - ); - } + log( + "raw stdin: received collapsed ESC ESC – starting confirmation timer", + ); setAwaitingConfirm(true); setTimeout(() => setAwaitingConfirm(false), 1500); } @@ -65,15 +63,11 @@ export default function TerminalChatInputThinking({ } if (awaitingConfirm) { - if (isLoggingEnabled()) { - log("useInput: second ESC detected – triggering onInterrupt()"); - } + log("useInput: second ESC detected – triggering onInterrupt()"); onInterrupt(); setAwaitingConfirm(false); } else { - if (isLoggingEnabled()) { - log("useInput: first ESC detected – waiting for confirmation"); - } + log("useInput: first ESC detected – waiting for confirmation"); setAwaitingConfirm(true); setTimeout(() => setAwaitingConfirm(false), 1500); } diff --git a/codex-cli/src/components/chat/terminal-chat-input.tsx b/codex-cli/src/components/chat/terminal-chat-input.tsx index 59265221d4..943637dba5 100644 --- a/codex-cli/src/components/chat/terminal-chat-input.tsx +++ b/codex-cli/src/components/chat/terminal-chat-input.tsx @@ -6,7 +6,7 @@ import type { } from "openai/resources/responses/responses.mjs"; import { TerminalChatCommandReview } from "./terminal-chat-command-review.js"; -import { log, isLoggingEnabled } from "../../utils/agent/log.js"; +import { log } from "../../utils/agent/log.js"; import { loadConfig } from "../../utils/config.js"; import { createInputItem } from "../../utils/input-utils.js"; import { setSessionId } from "../../utils/session.js"; @@ -683,11 +683,9 @@ function TerminalChatInputThinking({ const str = Buffer.isBuffer(data) ? data.toString("utf8") : data; if (str === "\x1b\x1b") { // Treat as the first Escape press – prompt the user for confirmation. - if (isLoggingEnabled()) { - log( - "raw stdin: received collapsed ESC ESC – starting confirmation timer", - ); - } + log( + "raw stdin: received collapsed ESC ESC – starting confirmation timer", + ); setAwaitingConfirm(true); setTimeout(() => setAwaitingConfirm(false), 1500); } @@ -712,15 +710,11 @@ function TerminalChatInputThinking({ } if (awaitingConfirm) { - if (isLoggingEnabled()) { - log("useInput: second ESC detected – triggering onInterrupt()"); - } + log("useInput: second ESC detected – triggering onInterrupt()"); onInterrupt(); setAwaitingConfirm(false); } else { - if (isLoggingEnabled()) { - log("useInput: first ESC detected – waiting for confirmation"); - } + log("useInput: first ESC detected – waiting for confirmation"); setAwaitingConfirm(true); setTimeout(() => setAwaitingConfirm(false), 1500); } diff --git a/codex-cli/src/components/chat/terminal-chat-new-input.tsx b/codex-cli/src/components/chat/terminal-chat-new-input.tsx index 9ceb4bbccc..a4558c94be 100644 --- a/codex-cli/src/components/chat/terminal-chat-new-input.tsx +++ b/codex-cli/src/components/chat/terminal-chat-new-input.tsx @@ -8,7 +8,7 @@ import type { import MultilineTextEditor from "./multiline-editor"; import { TerminalChatCommandReview } from "./terminal-chat-command-review.js"; -import { log, isLoggingEnabled } from "../../utils/agent/log.js"; +import { log } from "../../utils/agent/log.js"; import { loadConfig } from "../../utils/config.js"; import { createInputItem } from "../../utils/input-utils.js"; import { setSessionId } from "../../utils/session.js"; @@ -496,11 +496,9 @@ function TerminalChatInputThinking({ const str = Buffer.isBuffer(data) ? data.toString("utf8") : data; if (str === "\x1b\x1b") { // Treat as the first Escape press – prompt the user for confirmation. - if (isLoggingEnabled()) { - log( - "raw stdin: received collapsed ESC ESC – starting confirmation timer", - ); - } + log( + "raw stdin: received collapsed ESC ESC – starting confirmation timer", + ); setAwaitingConfirm(true); setTimeout(() => setAwaitingConfirm(false), 1500); } @@ -522,15 +520,11 @@ function TerminalChatInputThinking({ } if (awaitingConfirm) { - if (isLoggingEnabled()) { - log("useInput: second ESC detected – triggering onInterrupt()"); - } + log("useInput: second ESC detected – triggering onInterrupt()"); onInterrupt(); setAwaitingConfirm(false); } else { - if (isLoggingEnabled()) { - log("useInput: first ESC detected – waiting for confirmation"); - } + log("useInput: first ESC detected – waiting for confirmation"); setAwaitingConfirm(true); setTimeout(() => setAwaitingConfirm(false), 1500); } diff --git a/codex-cli/src/components/chat/terminal-chat.tsx b/codex-cli/src/components/chat/terminal-chat.tsx index f228ed19c4..7602721652 100644 --- a/codex-cli/src/components/chat/terminal-chat.tsx +++ b/codex-cli/src/components/chat/terminal-chat.tsx @@ -15,7 +15,7 @@ import { formatCommandForDisplay } from "../../format-command.js"; import { useConfirmation } from "../../hooks/use-confirmation.js"; import { useTerminalSize } from "../../hooks/use-terminal-size.js"; import { AgentLoop } from "../../utils/agent/agent-loop.js"; -import { isLoggingEnabled, log } from "../../utils/agent/log.js"; +import { log } from "../../utils/agent/log.js"; import { ReviewDecision } from "../../utils/agent/review.js"; import { generateCompactSummary } from "../../utils/compact-summary.js"; import { OPENAI_BASE_URL } from "../../utils/config.js"; @@ -197,30 +197,25 @@ export default function TerminalChat({ // ──────────────────────────────────────────────────────────────── // DEBUG: log every render w/ key bits of state // ──────────────────────────────────────────────────────────────── - if (isLoggingEnabled()) { - log( - `render – agent? ${Boolean(agentRef.current)} loading=${loading} items=${ - items.length - }`, - ); - } + log( + `render – agent? ${Boolean(agentRef.current)} loading=${loading} items=${ + items.length + }`, + ); useEffect(() => { // Skip recreating the agent if awaiting a decision on a pending confirmation if (confirmationPrompt != null) { - if (isLoggingEnabled()) { - log("skip AgentLoop recreation due to pending confirmationPrompt"); - } + log("skip AgentLoop recreation due to pending confirmationPrompt"); return; } - if (isLoggingEnabled()) { - log("creating NEW AgentLoop"); - log( - `model=${model} instructions=${Boolean( - config.instructions, - )} approvalPolicy=${approvalPolicy}`, - ); - } + + log("creating NEW AgentLoop"); + log( + `model=${model} instructions=${Boolean( + config.instructions, + )} approvalPolicy=${approvalPolicy}`, + ); // Tear down any existing loop before creating a new one agentRef.current?.terminate(); @@ -288,14 +283,10 @@ export default function TerminalChat({ // force a render so JSX below can "see" the freshly created agent forceUpdate(); - if (isLoggingEnabled()) { - log(`AgentLoop created: ${inspect(agentRef.current, { depth: 1 })}`); - } + log(`AgentLoop created: ${inspect(agentRef.current, { depth: 1 })}`); return () => { - if (isLoggingEnabled()) { - log("terminating AgentLoop"); - } + log("terminating AgentLoop"); agentRef.current?.terminate(); agentRef.current = undefined; forceUpdate(); // re‑render after teardown too @@ -377,9 +368,7 @@ export default function TerminalChat({ // Let's also track whenever the ref becomes available const agent = agentRef.current; useEffect(() => { - if (isLoggingEnabled()) { - log(`agentRef.current is now ${Boolean(agent)}`); - } + log(`agentRef.current is now ${Boolean(agent)}`); }, [agent]); // --------------------------------------------------------------------- @@ -504,11 +493,9 @@ export default function TerminalChat({ if (!agent) { return; } - if (isLoggingEnabled()) { - log( - "TerminalChat: interruptAgent invoked – calling agent.cancel()", - ); - } + log( + "TerminalChat: interruptAgent invoked – calling agent.cancel()", + ); agent.cancel(); setLoading(false); @@ -544,13 +531,11 @@ export default function TerminalChat({ currentModel={model} hasLastResponse={Boolean(lastResponseId)} onSelect={(newModel) => { - if (isLoggingEnabled()) { - log( - "TerminalChat: interruptAgent invoked – calling agent.cancel()", - ); - if (!agent) { - log("TerminalChat: agent is not ready yet"); - } + log( + "TerminalChat: interruptAgent invoked – calling agent.cancel()", + ); + if (!agent) { + log("TerminalChat: agent is not ready yet"); } agent?.cancel(); setLoading(false); diff --git a/codex-cli/src/utils/agent/agent-loop.ts b/codex-cli/src/utils/agent/agent-loop.ts index cbe3c56960..172ba67295 100644 --- a/codex-cli/src/utils/agent/agent-loop.ts +++ b/codex-cli/src/utils/agent/agent-loop.ts @@ -8,7 +8,7 @@ import type { } from "openai/resources/responses/responses.mjs"; import type { Reasoning } from "openai/resources.mjs"; -import { log, isLoggingEnabled } from "./log.js"; +import { log } from "./log.js"; import { OPENAI_BASE_URL, OPENAI_TIMEOUT_MS } from "../config.js"; import { parseToolCallArguments } from "../parsers.js"; import { @@ -116,15 +116,13 @@ export class AgentLoop { // Reset the current stream to allow new requests this.currentStream = null; - if (isLoggingEnabled()) { - log( - `AgentLoop.cancel() invoked – currentStream=${Boolean( - this.currentStream, - )} execAbortController=${Boolean( - this.execAbortController, - )} generation=${this.generation}`, - ); - } + log( + `AgentLoop.cancel() invoked – currentStream=${Boolean( + this.currentStream, + )} execAbortController=${Boolean(this.execAbortController)} generation=${ + this.generation + }`, + ); ( this.currentStream as { controller?: { abort?: () => void } } | null )?.controller?.abort?.(); @@ -136,9 +134,7 @@ export class AgentLoop { // Create a new abort controller for future tool calls this.execAbortController = new AbortController(); - if (isLoggingEnabled()) { - log("AgentLoop.cancel(): execAbortController.abort() called"); - } + log("AgentLoop.cancel(): execAbortController.abort() called"); // NOTE: We intentionally do *not* clear `lastResponseId` here. If the // stream produced a `function_call` before the user cancelled, OpenAI now @@ -174,9 +170,7 @@ export class AgentLoop { // this.onItem(cancelNotice); this.generation += 1; - if (isLoggingEnabled()) { - log(`AgentLoop.cancel(): generation bumped to ${this.generation}`); - } + log(`AgentLoop.cancel(): generation bumped to ${this.generation}`); } /** @@ -315,13 +309,11 @@ export class AgentLoop { const callId: string = (item as any).call_id ?? (item as any).id; const args = parseToolCallArguments(rawArguments ?? "{}"); - if (isLoggingEnabled()) { - log( - `handleFunctionCall(): name=${ - name ?? "undefined" - } callId=${callId} args=${rawArguments}`, - ); - } + log( + `handleFunctionCall(): name=${ + name ?? "undefined" + } callId=${callId} args=${rawArguments}`, + ); if (args == null) { const outputItem: ResponseInputItem.FunctionCallOutput = { @@ -407,11 +399,9 @@ export class AgentLoop { // Create a fresh AbortController for this run so that tool calls from a // previous run do not accidentally get signalled. this.execAbortController = new AbortController(); - if (isLoggingEnabled()) { - log( - `AgentLoop.run(): new execAbortController created (${this.execAbortController.signal}) for generation ${this.generation}`, - ); - } + log( + `AgentLoop.run(): new execAbortController created (${this.execAbortController.signal}) for generation ${this.generation}`, + ); // NOTE: We no longer (re‑)attach an `abort` listener to `hardAbort` here. // A single listener that forwards the `abort` to the current // `execAbortController` is installed once in the constructor. Re‑adding a @@ -502,11 +492,9 @@ export class AgentLoop { const mergedInstructions = [prefix, this.instructions] .filter(Boolean) .join("\n"); - if (isLoggingEnabled()) { - log( - `instructions (length ${mergedInstructions.length}): ${mergedInstructions}`, - ); - } + log( + `instructions (length ${mergedInstructions.length}): ${mergedInstructions}`, + ); // eslint-disable-next-line no-await-in-loop stream = await this.oai.responses.create({ model: this.model, @@ -733,9 +721,7 @@ export class AgentLoop { try { // eslint-disable-next-line no-await-in-loop for await (const event of stream) { - if (isLoggingEnabled()) { - log(`AgentLoop.run(): response event ${event.type}`); - } + log(`AgentLoop.run(): response event ${event.type}`); // process and surface each item (no‑op until we can depend on streaming events) if (event.type === "response.output_item.done") { diff --git a/codex-cli/src/utils/agent/handle-exec-command.ts b/codex-cli/src/utils/agent/handle-exec-command.ts index 1af390e226..97b78c0817 100644 --- a/codex-cli/src/utils/agent/handle-exec-command.ts +++ b/codex-cli/src/utils/agent/handle-exec-command.ts @@ -223,23 +223,22 @@ async function execCommand( workdir = process.cwd(); } } - if (isLoggingEnabled()) { - if (applyPatchCommand != null) { - log("EXEC running apply_patch command"); - } else { - const { cmd, timeoutInMillis } = execInput; - // Seconds are a bit easier to read in log messages and most timeouts - // are specified as multiples of 1000, anyway. - const timeout = - timeoutInMillis != null - ? Math.round(timeoutInMillis / 1000).toString() - : "undefined"; - log( - `EXEC running \`${formatCommandForDisplay( - cmd, - )}\` in workdir=${workdir} with timeout=${timeout}s`, - ); - } + + if (applyPatchCommand != null) { + log("EXEC running apply_patch command"); + } else if (isLoggingEnabled()) { + const { cmd, timeoutInMillis } = execInput; + // Seconds are a bit easier to read in log messages and most timeouts + // are specified as multiples of 1000, anyway. + const timeout = + timeoutInMillis != null + ? Math.round(timeoutInMillis / 1000).toString() + : "undefined"; + log( + `EXEC running \`${formatCommandForDisplay( + cmd, + )}\` in workdir=${workdir} with timeout=${timeout}s`, + ); } // Note execApplyPatch() and exec() are coded defensively and should not diff --git a/codex-cli/src/utils/agent/log.ts b/codex-cli/src/utils/agent/log.ts index e804386566..e4aca06c79 100644 --- a/codex-cli/src/utils/agent/log.ts +++ b/codex-cli/src/utils/agent/log.ts @@ -124,6 +124,14 @@ export function log(message: string): void { (logger ?? initLogger()).log(message); } +/** + * USE SPARINGLY! This function should only be used to guard a call to log() if + * the log message is large and you want to avoid constructing it if logging is + * disabled. + * + * `log()` is already a no-op if DEBUG is not set, so an extra + * `isLoggingEnabled()` check is unnecessary. + */ export function isLoggingEnabled(): boolean { return (logger ?? initLogger()).isLoggingEnabled(); } diff --git a/codex-cli/src/utils/agent/platform-commands.ts b/codex-cli/src/utils/agent/platform-commands.ts index 7be02c7ac5..085c575d44 100644 --- a/codex-cli/src/utils/agent/platform-commands.ts +++ b/codex-cli/src/utils/agent/platform-commands.ts @@ -2,7 +2,7 @@ * Utility functions for handling platform-specific commands */ -import { log, isLoggingEnabled } from "./log.js"; +import { log } from "./log.js"; /** * Map of Unix commands to their Windows equivalents @@ -59,9 +59,7 @@ export function adaptCommandForPlatform(command: Array): Array { return command; } - if (isLoggingEnabled()) { - log(`Adapting command '${cmd}' for Windows platform`); - } + log(`Adapting command '${cmd}' for Windows platform`); // Create a new command array with the adapted command const adaptedCommand = [...command]; @@ -78,9 +76,7 @@ export function adaptCommandForPlatform(command: Array): Array { } } - if (isLoggingEnabled()) { - log(`Adapted command: ${adaptedCommand.join(" ")}`); - } + log(`Adapted command: ${adaptedCommand.join(" ")}`); return adaptedCommand; } diff --git a/codex-cli/src/utils/agent/sandbox/raw-exec.ts b/codex-cli/src/utils/agent/sandbox/raw-exec.ts index 6cfb304731..f2ca03bf49 100644 --- a/codex-cli/src/utils/agent/sandbox/raw-exec.ts +++ b/codex-cli/src/utils/agent/sandbox/raw-exec.ts @@ -7,7 +7,7 @@ import type { StdioPipe, } from "child_process"; -import { log, isLoggingEnabled } from "../log.js"; +import { log } from "../log.js"; import { adaptCommandForPlatform } from "../platform-commands.js"; import { spawn } from "child_process"; import * as os from "os"; @@ -27,10 +27,7 @@ export function exec( // Adapt command for the current platform (e.g., convert 'ls' to 'dir' on Windows) const adaptedCommand = adaptCommandForPlatform(command); - if ( - isLoggingEnabled() && - JSON.stringify(adaptedCommand) !== JSON.stringify(command) - ) { + if (JSON.stringify(adaptedCommand) !== JSON.stringify(command)) { log( `Command adapted for platform: ${command.join( " ", @@ -95,9 +92,7 @@ export function exec( // timely fashion. if (abortSignal) { const abortHandler = () => { - if (isLoggingEnabled()) { - log(`raw-exec: abort signal received – killing child ${child.pid}`); - } + log(`raw-exec: abort signal received – killing child ${child.pid}`); const killTarget = (signal: NodeJS.Signals) => { if (!child.pid) { return; @@ -194,11 +189,9 @@ export function exec( exitCode = 1; } - if (isLoggingEnabled()) { - log( - `raw-exec: child ${child.pid} exited code=${exitCode} signal=${signal}`, - ); - } + log( + `raw-exec: child ${child.pid} exited code=${exitCode} signal=${signal}`, + ); resolve({ stdout, stderr, diff --git a/codex-cli/src/utils/config.ts b/codex-cli/src/utils/config.ts index 8467653580..3be77059e0 100644 --- a/codex-cli/src/utils/config.ts +++ b/codex-cli/src/utils/config.ts @@ -8,7 +8,7 @@ import type { FullAutoErrorMode } from "./auto-approval-mode.js"; -import { log, isLoggingEnabled } from "./agent/log.js"; +import { log } from "./agent/log.js"; import { AutoApprovalMode } from "./auto-approval-mode.js"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; import { load as loadYaml, dump as dumpYaml } from "js-yaml"; @@ -245,15 +245,11 @@ export const loadConfig = ( ? resolvePath(cwd, options.projectDocPath) : discoverProjectDocPath(cwd); if (projectDocPath) { - if (isLoggingEnabled()) { - log( - `[codex] Loaded project doc from ${projectDocPath} (${projectDoc.length} bytes)`, - ); - } + log( + `[codex] Loaded project doc from ${projectDocPath} (${projectDoc.length} bytes)`, + ); } else { - if (isLoggingEnabled()) { - log(`[codex] No project doc found in ${cwd}`); - } + log(`[codex] No project doc found in ${cwd}`); } } From 70138aed178b1ca3287d2a02d7d69c6b5148de41 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Sat, 19 Apr 2025 11:23:58 -0700 Subject: [PATCH 23/41] use spawn instead of exec to avoid injection vulnerability --- codex-cli/src/components/chat/terminal-chat.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/codex-cli/src/components/chat/terminal-chat.tsx b/codex-cli/src/components/chat/terminal-chat.tsx index e341cdfbbb..f228ed19c4 100644 --- a/codex-cli/src/components/chat/terminal-chat.tsx +++ b/codex-cli/src/components/chat/terminal-chat.tsx @@ -29,7 +29,7 @@ import HelpOverlay from "../help-overlay.js"; import HistoryOverlay from "../history-overlay.js"; import ModelOverlay from "../model-overlay.js"; import { Box, Text } from "ink"; -import { exec } from "node:child_process"; +import { spawn } from "node:child_process"; import OpenAI from "openai"; import React, { useEffect, useMemo, useRef, useState } from "react"; import { inspect } from "util"; @@ -364,9 +364,10 @@ export default function TerminalChat({ const safePreview = preview.replace(/"/g, '\\"'); const title = "Codex CLI"; const cwd = PWD; - exec( - `osascript -e 'display notification "${safePreview}" with title "${title}" subtitle "${cwd}" sound name "Ping"'`, - ); + spawn("osascript", [ + "-e", + `display notification "${safePreview}" with title "${title}" subtitle "${cwd}" sound name "Ping"`, + ]); } } } From c03bb964384d4e9a93bf5797b84f885b049def71 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Sat, 19 Apr 2025 11:23:58 -0700 Subject: [PATCH 24/41] reduce max output of ExecResult --- codex-cli/src/utils/agent/sandbox/raw-exec.ts | 130 +++++++++++++----- 1 file changed, 97 insertions(+), 33 deletions(-) diff --git a/codex-cli/src/utils/agent/sandbox/raw-exec.ts b/codex-cli/src/utils/agent/sandbox/raw-exec.ts index f2ca03bf49..ce10eae149 100644 --- a/codex-cli/src/utils/agent/sandbox/raw-exec.ts +++ b/codex-cli/src/utils/agent/sandbox/raw-exec.ts @@ -12,7 +12,10 @@ import { adaptCommandForPlatform } from "../platform-commands.js"; import { spawn } from "child_process"; import * as os from "os"; -const MAX_BUFFER = 1024 * 100; // 100 KB +// Maximum output cap: either MAX_OUTPUT_LINES lines or MAX_OUTPUT_BYTES bytes, +// whichever limit is reached first. +const MAX_OUTPUT_BYTES = 1024 * 10; // 10 KB +const MAX_OUTPUT_LINES = 256; /** * This function should never return a rejected promise: errors should be @@ -143,37 +146,17 @@ export function exec( // resolve the promise and translate the failure into a regular // ExecResult object so the rest of the agent loop can carry on gracefully. - const stdoutChunks: Array = []; - const stderrChunks: Array = []; - let numStdoutBytes = 0; - let numStderrBytes = 0; - let hitMaxStdout = false; - let hitMaxStderr = false; + // Collect stdout and stderr up to configured limits + const stdoutCollector = createTruncatingCollector(); + const stderrCollector = createTruncatingCollector(); return new Promise((resolve) => { - child.stdout?.on("data", (data: Buffer) => { - if (!hitMaxStdout) { - numStdoutBytes += data.length; - if (numStdoutBytes <= MAX_BUFFER) { - stdoutChunks.push(data); - } else { - hitMaxStdout = true; - } - } - }); - child.stderr?.on("data", (data: Buffer) => { - if (!hitMaxStderr) { - numStderrBytes += data.length; - if (numStderrBytes <= MAX_BUFFER) { - stderrChunks.push(data); - } else { - hitMaxStderr = true; - } - } - }); + stdoutCollector.attach(child.stdout!); + stderrCollector.attach(child.stderr!); + child.on("exit", (code, signal) => { - const stdout = Buffer.concat(stdoutChunks).toString("utf8"); - const stderr = Buffer.concat(stderrChunks).toString("utf8"); + const stdout = stdoutCollector.getString(); + const stderr = stderrCollector.getString(); // Map (code, signal) to an exit code. We expect exactly one of the two // values to be non-null, but we code defensively to handle the case where @@ -192,19 +175,100 @@ export function exec( log( `raw-exec: child ${child.pid} exited code=${exitCode} signal=${signal}`, ); - resolve({ + + const execResult = { stdout, stderr, exitCode, - }); + }; + resolve( + addTruncationWarningsIfNecessary( + execResult, + stdoutCollector.hit, + stderrCollector.hit, + ), + ); }); child.on("error", (err) => { - resolve({ + const execResult = { stdout: "", stderr: String(err), exitCode: 1, - }); + }; + resolve( + addTruncationWarningsIfNecessary( + execResult, + stdoutCollector.hit, + stderrCollector.hit, + ), + ); }); }); } + +/** + * Creates a collector that accumulates data Buffers from a stream up to + * specified byte and line limits. After either limit is exceeded, further + * data is ignored. + */ +function createTruncatingCollector( + byteLimit: number = MAX_OUTPUT_BYTES, + lineLimit: number = MAX_OUTPUT_LINES, +) { + const chunks: Array = []; + let totalBytes = 0; + let totalLines = 0; + let hitLimit = false; + return { + attach(stream?: NodeJS.ReadableStream) { + stream?.on("data", (data: Buffer) => { + if (hitLimit) { + return; + } + totalBytes += data.length; + for (let i = 0; i < data.length; i++) { + if (data[i] === 0x0a) { + totalLines++; + } + } + if (totalBytes <= byteLimit && totalLines <= lineLimit) { + chunks.push(data); + } else { + hitLimit = true; + } + }); + }, + getString() { + return Buffer.concat(chunks).toString("utf8"); + }, + /** True if either byte or line limit was exceeded */ + get hit(): boolean { + return hitLimit; + }, + }; +} + +/** + * Adds a truncation warnings to stdout and stderr, if appropriate. + */ +function addTruncationWarningsIfNecessary( + execResult: ExecResult, + hitMaxStdout: boolean, + hitMaxStderr: boolean, +): ExecResult { + if (!hitMaxStdout && !hitMaxStderr) { + return execResult; + } else { + const { stdout, stderr, exitCode } = execResult; + return { + stdout: hitMaxStdout + ? stdout + "\n\n[Output truncated: too many lines or bytes]" + : stdout, + stderr: hitMaxStderr + ? stderr + "\n\n[Output truncated: too many lines or bytes]" + : stderr, + exitCode, + }; + } +} From 507bc768bd25bf520be46ffc721a1a83a24153bd Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Sat, 19 Apr 2025 11:23:58 -0700 Subject: [PATCH 25/41] CONFIG_DIR should not be in the list of writable roots by default --- .../src/utils/agent/sandbox/macos-seatbelt.ts | 15 +++++++-------- codex-cli/src/utils/agent/sandbox/raw-exec.ts | 2 +- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/codex-cli/src/utils/agent/sandbox/macos-seatbelt.ts b/codex-cli/src/utils/agent/sandbox/macos-seatbelt.ts index 0317458261..760ba63d8c 100644 --- a/codex-cli/src/utils/agent/sandbox/macos-seatbelt.ts +++ b/codex-cli/src/utils/agent/sandbox/macos-seatbelt.ts @@ -3,11 +3,9 @@ import type { SpawnOptions } from "child_process"; import { exec } from "./raw-exec.js"; import { log } from "../log.js"; -import { CONFIG_DIR } from "src/utils/config.js"; function getCommonRoots() { return [ - CONFIG_DIR, // Without this root, it'll cause: // pyenv: cannot rehash: $HOME/.pyenv/shims isn't writable `${process.env["HOME"]}/.pyenv`, @@ -17,16 +15,17 @@ function getCommonRoots() { export function execWithSeatbelt( cmd: Array, opts: SpawnOptions, - writableRoots: Array, + writableRoots: ReadonlyArray, abortSignal?: AbortSignal, ): Promise { let scopedWritePolicy: string; let policyTemplateParams: Array; - if (writableRoots.length > 0) { - // Add `~/.codex` to the list of writable roots - // (if there's any already, not in read-only mode) - getCommonRoots().map((root) => writableRoots.push(root)); - const { policies, params } = writableRoots + + const fullWritableRoots = [...writableRoots, ...getCommonRoots()]; + // In practice, fullWritableRoots will be non-empty, but we check just in + // case the logic to build up fullWritableRoots changes. + if (fullWritableRoots.length > 0) { + const { policies, params } = fullWritableRoots .map((root, index) => ({ policy: `(subpath (param "WRITABLE_ROOT_${index}"))`, param: `-DWRITABLE_ROOT_${index}=${root}`, diff --git a/codex-cli/src/utils/agent/sandbox/raw-exec.ts b/codex-cli/src/utils/agent/sandbox/raw-exec.ts index 6cfb304731..e7bcdb2a13 100644 --- a/codex-cli/src/utils/agent/sandbox/raw-exec.ts +++ b/codex-cli/src/utils/agent/sandbox/raw-exec.ts @@ -21,7 +21,7 @@ const MAX_BUFFER = 1024 * 100; // 100 KB export function exec( command: Array, options: SpawnOptions, - _writableRoots: Array, + _writableRoots: ReadonlyArray, abortSignal?: AbortSignal, ): Promise { // Adapt command for the current platform (e.g., convert 'ls' to 'dir' on Windows) From cdb7325192bf8d9a9b1fa20f8039b5fee92f2314 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Sat, 19 Apr 2025 11:45:21 -0700 Subject: [PATCH 26/41] reduce max output of ExecResult --- codex-cli/src/utils/agent/sandbox/raw-exec.ts | 130 +++++++++++++----- 1 file changed, 97 insertions(+), 33 deletions(-) diff --git a/codex-cli/src/utils/agent/sandbox/raw-exec.ts b/codex-cli/src/utils/agent/sandbox/raw-exec.ts index 35ae8a4a92..1b9bc1db25 100644 --- a/codex-cli/src/utils/agent/sandbox/raw-exec.ts +++ b/codex-cli/src/utils/agent/sandbox/raw-exec.ts @@ -12,7 +12,10 @@ import { adaptCommandForPlatform } from "../platform-commands.js"; import { spawn } from "child_process"; import * as os from "os"; -const MAX_BUFFER = 1024 * 100; // 100 KB +// Maximum output cap: either MAX_OUTPUT_LINES lines or MAX_OUTPUT_BYTES bytes, +// whichever limit is reached first. +const MAX_OUTPUT_BYTES = 1024 * 10; // 10 KB +const MAX_OUTPUT_LINES = 256; /** * This function should never return a rejected promise: errors should be @@ -143,37 +146,17 @@ export function exec( // resolve the promise and translate the failure into a regular // ExecResult object so the rest of the agent loop can carry on gracefully. - const stdoutChunks: Array = []; - const stderrChunks: Array = []; - let numStdoutBytes = 0; - let numStderrBytes = 0; - let hitMaxStdout = false; - let hitMaxStderr = false; + // Collect stdout and stderr up to configured limits + const stdoutCollector = createTruncatingCollector(); + const stderrCollector = createTruncatingCollector(); return new Promise((resolve) => { - child.stdout?.on("data", (data: Buffer) => { - if (!hitMaxStdout) { - numStdoutBytes += data.length; - if (numStdoutBytes <= MAX_BUFFER) { - stdoutChunks.push(data); - } else { - hitMaxStdout = true; - } - } - }); - child.stderr?.on("data", (data: Buffer) => { - if (!hitMaxStderr) { - numStderrBytes += data.length; - if (numStderrBytes <= MAX_BUFFER) { - stderrChunks.push(data); - } else { - hitMaxStderr = true; - } - } - }); + stdoutCollector.attach(child.stdout!); + stderrCollector.attach(child.stderr!); + child.on("exit", (code, signal) => { - const stdout = Buffer.concat(stdoutChunks).toString("utf8"); - const stderr = Buffer.concat(stderrChunks).toString("utf8"); + const stdout = stdoutCollector.getString(); + const stderr = stderrCollector.getString(); // Map (code, signal) to an exit code. We expect exactly one of the two // values to be non-null, but we code defensively to handle the case where @@ -192,19 +175,100 @@ export function exec( log( `raw-exec: child ${child.pid} exited code=${exitCode} signal=${signal}`, ); - resolve({ + + const execResult = { stdout, stderr, exitCode, - }); + }; + resolve( + addTruncationWarningsIfNecessary( + execResult, + stdoutCollector.hit, + stderrCollector.hit, + ), + ); }); child.on("error", (err) => { - resolve({ + const execResult = { stdout: "", stderr: String(err), exitCode: 1, - }); + }; + resolve( + addTruncationWarningsIfNecessary( + execResult, + stdoutCollector.hit, + stderrCollector.hit, + ), + ); }); }); } + +/** + * Creates a collector that accumulates data Buffers from a stream up to + * specified byte and line limits. After either limit is exceeded, further + * data is ignored. + */ +function createTruncatingCollector( + byteLimit: number = MAX_OUTPUT_BYTES, + lineLimit: number = MAX_OUTPUT_LINES, +) { + const chunks: Array = []; + let totalBytes = 0; + let totalLines = 0; + let hitLimit = false; + return { + attach(stream?: NodeJS.ReadableStream) { + stream?.on("data", (data: Buffer) => { + if (hitLimit) { + return; + } + totalBytes += data.length; + for (let i = 0; i < data.length; i++) { + if (data[i] === 0x0a) { + totalLines++; + } + } + if (totalBytes <= byteLimit && totalLines <= lineLimit) { + chunks.push(data); + } else { + hitLimit = true; + } + }); + }, + getString() { + return Buffer.concat(chunks).toString("utf8"); + }, + /** True if either byte or line limit was exceeded */ + get hit(): boolean { + return hitLimit; + }, + }; +} + +/** + * Adds a truncation warnings to stdout and stderr, if appropriate. + */ +function addTruncationWarningsIfNecessary( + execResult: ExecResult, + hitMaxStdout: boolean, + hitMaxStderr: boolean, +): ExecResult { + if (!hitMaxStdout && !hitMaxStderr) { + return execResult; + } else { + const { stdout, stderr, exitCode } = execResult; + return { + stdout: hitMaxStdout + ? stdout + "\n\n[Output truncated: too many lines or bytes]" + : stdout, + stderr: hitMaxStderr + ? stderr + "\n\n[Output truncated: too many lines or bytes]" + : stderr, + exitCode, + }; + } +} From 1757ff3eb222f01b33b6b1889f2d1d8aabc8d8a7 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Sat, 19 Apr 2025 11:45:21 -0700 Subject: [PATCH 27/41] remove unnecessary isLoggingEnabled() checks --- .../chat/terminal-chat-input-thinking.tsx | 18 ++--- .../components/chat/terminal-chat-input.tsx | 18 ++--- .../chat/terminal-chat-new-input.tsx | 18 ++--- .../src/components/chat/terminal-chat.tsx | 65 +++++++------------ codex-cli/src/utils/agent/agent-loop.ts | 58 +++++++---------- .../src/utils/agent/handle-exec-command.ts | 33 +++++----- codex-cli/src/utils/agent/log.ts | 8 +++ .../src/utils/agent/platform-commands.ts | 10 +-- codex-cli/src/utils/agent/sandbox/raw-exec.ts | 19 ++---- codex-cli/src/utils/config.ts | 14 ++-- 10 files changed, 103 insertions(+), 158 deletions(-) diff --git a/codex-cli/src/components/chat/terminal-chat-input-thinking.tsx b/codex-cli/src/components/chat/terminal-chat-input-thinking.tsx index 213dd8c9af..fdc8bd218c 100644 --- a/codex-cli/src/components/chat/terminal-chat-input-thinking.tsx +++ b/codex-cli/src/components/chat/terminal-chat-input-thinking.tsx @@ -1,4 +1,4 @@ -import { log, isLoggingEnabled } from "../../utils/agent/log.js"; +import { log } from "../../utils/agent/log.js"; import { Box, Text, useInput, useStdin } from "ink"; import React, { useState } from "react"; import { useInterval } from "use-interval"; @@ -40,11 +40,9 @@ export default function TerminalChatInputThinking({ const str = Buffer.isBuffer(data) ? data.toString("utf8") : data; if (str === "\x1b\x1b") { - if (isLoggingEnabled()) { - log( - "raw stdin: received collapsed ESC ESC – starting confirmation timer", - ); - } + log( + "raw stdin: received collapsed ESC ESC – starting confirmation timer", + ); setAwaitingConfirm(true); setTimeout(() => setAwaitingConfirm(false), 1500); } @@ -65,15 +63,11 @@ export default function TerminalChatInputThinking({ } if (awaitingConfirm) { - if (isLoggingEnabled()) { - log("useInput: second ESC detected – triggering onInterrupt()"); - } + log("useInput: second ESC detected – triggering onInterrupt()"); onInterrupt(); setAwaitingConfirm(false); } else { - if (isLoggingEnabled()) { - log("useInput: first ESC detected – waiting for confirmation"); - } + log("useInput: first ESC detected – waiting for confirmation"); setAwaitingConfirm(true); setTimeout(() => setAwaitingConfirm(false), 1500); } diff --git a/codex-cli/src/components/chat/terminal-chat-input.tsx b/codex-cli/src/components/chat/terminal-chat-input.tsx index 59265221d4..943637dba5 100644 --- a/codex-cli/src/components/chat/terminal-chat-input.tsx +++ b/codex-cli/src/components/chat/terminal-chat-input.tsx @@ -6,7 +6,7 @@ import type { } from "openai/resources/responses/responses.mjs"; import { TerminalChatCommandReview } from "./terminal-chat-command-review.js"; -import { log, isLoggingEnabled } from "../../utils/agent/log.js"; +import { log } from "../../utils/agent/log.js"; import { loadConfig } from "../../utils/config.js"; import { createInputItem } from "../../utils/input-utils.js"; import { setSessionId } from "../../utils/session.js"; @@ -683,11 +683,9 @@ function TerminalChatInputThinking({ const str = Buffer.isBuffer(data) ? data.toString("utf8") : data; if (str === "\x1b\x1b") { // Treat as the first Escape press – prompt the user for confirmation. - if (isLoggingEnabled()) { - log( - "raw stdin: received collapsed ESC ESC – starting confirmation timer", - ); - } + log( + "raw stdin: received collapsed ESC ESC – starting confirmation timer", + ); setAwaitingConfirm(true); setTimeout(() => setAwaitingConfirm(false), 1500); } @@ -712,15 +710,11 @@ function TerminalChatInputThinking({ } if (awaitingConfirm) { - if (isLoggingEnabled()) { - log("useInput: second ESC detected – triggering onInterrupt()"); - } + log("useInput: second ESC detected – triggering onInterrupt()"); onInterrupt(); setAwaitingConfirm(false); } else { - if (isLoggingEnabled()) { - log("useInput: first ESC detected – waiting for confirmation"); - } + log("useInput: first ESC detected – waiting for confirmation"); setAwaitingConfirm(true); setTimeout(() => setAwaitingConfirm(false), 1500); } diff --git a/codex-cli/src/components/chat/terminal-chat-new-input.tsx b/codex-cli/src/components/chat/terminal-chat-new-input.tsx index 9ceb4bbccc..a4558c94be 100644 --- a/codex-cli/src/components/chat/terminal-chat-new-input.tsx +++ b/codex-cli/src/components/chat/terminal-chat-new-input.tsx @@ -8,7 +8,7 @@ import type { import MultilineTextEditor from "./multiline-editor"; import { TerminalChatCommandReview } from "./terminal-chat-command-review.js"; -import { log, isLoggingEnabled } from "../../utils/agent/log.js"; +import { log } from "../../utils/agent/log.js"; import { loadConfig } from "../../utils/config.js"; import { createInputItem } from "../../utils/input-utils.js"; import { setSessionId } from "../../utils/session.js"; @@ -496,11 +496,9 @@ function TerminalChatInputThinking({ const str = Buffer.isBuffer(data) ? data.toString("utf8") : data; if (str === "\x1b\x1b") { // Treat as the first Escape press – prompt the user for confirmation. - if (isLoggingEnabled()) { - log( - "raw stdin: received collapsed ESC ESC – starting confirmation timer", - ); - } + log( + "raw stdin: received collapsed ESC ESC – starting confirmation timer", + ); setAwaitingConfirm(true); setTimeout(() => setAwaitingConfirm(false), 1500); } @@ -522,15 +520,11 @@ function TerminalChatInputThinking({ } if (awaitingConfirm) { - if (isLoggingEnabled()) { - log("useInput: second ESC detected – triggering onInterrupt()"); - } + log("useInput: second ESC detected – triggering onInterrupt()"); onInterrupt(); setAwaitingConfirm(false); } else { - if (isLoggingEnabled()) { - log("useInput: first ESC detected – waiting for confirmation"); - } + log("useInput: first ESC detected – waiting for confirmation"); setAwaitingConfirm(true); setTimeout(() => setAwaitingConfirm(false), 1500); } diff --git a/codex-cli/src/components/chat/terminal-chat.tsx b/codex-cli/src/components/chat/terminal-chat.tsx index f228ed19c4..7602721652 100644 --- a/codex-cli/src/components/chat/terminal-chat.tsx +++ b/codex-cli/src/components/chat/terminal-chat.tsx @@ -15,7 +15,7 @@ import { formatCommandForDisplay } from "../../format-command.js"; import { useConfirmation } from "../../hooks/use-confirmation.js"; import { useTerminalSize } from "../../hooks/use-terminal-size.js"; import { AgentLoop } from "../../utils/agent/agent-loop.js"; -import { isLoggingEnabled, log } from "../../utils/agent/log.js"; +import { log } from "../../utils/agent/log.js"; import { ReviewDecision } from "../../utils/agent/review.js"; import { generateCompactSummary } from "../../utils/compact-summary.js"; import { OPENAI_BASE_URL } from "../../utils/config.js"; @@ -197,30 +197,25 @@ export default function TerminalChat({ // ──────────────────────────────────────────────────────────────── // DEBUG: log every render w/ key bits of state // ──────────────────────────────────────────────────────────────── - if (isLoggingEnabled()) { - log( - `render – agent? ${Boolean(agentRef.current)} loading=${loading} items=${ - items.length - }`, - ); - } + log( + `render – agent? ${Boolean(agentRef.current)} loading=${loading} items=${ + items.length + }`, + ); useEffect(() => { // Skip recreating the agent if awaiting a decision on a pending confirmation if (confirmationPrompt != null) { - if (isLoggingEnabled()) { - log("skip AgentLoop recreation due to pending confirmationPrompt"); - } + log("skip AgentLoop recreation due to pending confirmationPrompt"); return; } - if (isLoggingEnabled()) { - log("creating NEW AgentLoop"); - log( - `model=${model} instructions=${Boolean( - config.instructions, - )} approvalPolicy=${approvalPolicy}`, - ); - } + + log("creating NEW AgentLoop"); + log( + `model=${model} instructions=${Boolean( + config.instructions, + )} approvalPolicy=${approvalPolicy}`, + ); // Tear down any existing loop before creating a new one agentRef.current?.terminate(); @@ -288,14 +283,10 @@ export default function TerminalChat({ // force a render so JSX below can "see" the freshly created agent forceUpdate(); - if (isLoggingEnabled()) { - log(`AgentLoop created: ${inspect(agentRef.current, { depth: 1 })}`); - } + log(`AgentLoop created: ${inspect(agentRef.current, { depth: 1 })}`); return () => { - if (isLoggingEnabled()) { - log("terminating AgentLoop"); - } + log("terminating AgentLoop"); agentRef.current?.terminate(); agentRef.current = undefined; forceUpdate(); // re‑render after teardown too @@ -377,9 +368,7 @@ export default function TerminalChat({ // Let's also track whenever the ref becomes available const agent = agentRef.current; useEffect(() => { - if (isLoggingEnabled()) { - log(`agentRef.current is now ${Boolean(agent)}`); - } + log(`agentRef.current is now ${Boolean(agent)}`); }, [agent]); // --------------------------------------------------------------------- @@ -504,11 +493,9 @@ export default function TerminalChat({ if (!agent) { return; } - if (isLoggingEnabled()) { - log( - "TerminalChat: interruptAgent invoked – calling agent.cancel()", - ); - } + log( + "TerminalChat: interruptAgent invoked – calling agent.cancel()", + ); agent.cancel(); setLoading(false); @@ -544,13 +531,11 @@ export default function TerminalChat({ currentModel={model} hasLastResponse={Boolean(lastResponseId)} onSelect={(newModel) => { - if (isLoggingEnabled()) { - log( - "TerminalChat: interruptAgent invoked – calling agent.cancel()", - ); - if (!agent) { - log("TerminalChat: agent is not ready yet"); - } + log( + "TerminalChat: interruptAgent invoked – calling agent.cancel()", + ); + if (!agent) { + log("TerminalChat: agent is not ready yet"); } agent?.cancel(); setLoading(false); diff --git a/codex-cli/src/utils/agent/agent-loop.ts b/codex-cli/src/utils/agent/agent-loop.ts index cbe3c56960..172ba67295 100644 --- a/codex-cli/src/utils/agent/agent-loop.ts +++ b/codex-cli/src/utils/agent/agent-loop.ts @@ -8,7 +8,7 @@ import type { } from "openai/resources/responses/responses.mjs"; import type { Reasoning } from "openai/resources.mjs"; -import { log, isLoggingEnabled } from "./log.js"; +import { log } from "./log.js"; import { OPENAI_BASE_URL, OPENAI_TIMEOUT_MS } from "../config.js"; import { parseToolCallArguments } from "../parsers.js"; import { @@ -116,15 +116,13 @@ export class AgentLoop { // Reset the current stream to allow new requests this.currentStream = null; - if (isLoggingEnabled()) { - log( - `AgentLoop.cancel() invoked – currentStream=${Boolean( - this.currentStream, - )} execAbortController=${Boolean( - this.execAbortController, - )} generation=${this.generation}`, - ); - } + log( + `AgentLoop.cancel() invoked – currentStream=${Boolean( + this.currentStream, + )} execAbortController=${Boolean(this.execAbortController)} generation=${ + this.generation + }`, + ); ( this.currentStream as { controller?: { abort?: () => void } } | null )?.controller?.abort?.(); @@ -136,9 +134,7 @@ export class AgentLoop { // Create a new abort controller for future tool calls this.execAbortController = new AbortController(); - if (isLoggingEnabled()) { - log("AgentLoop.cancel(): execAbortController.abort() called"); - } + log("AgentLoop.cancel(): execAbortController.abort() called"); // NOTE: We intentionally do *not* clear `lastResponseId` here. If the // stream produced a `function_call` before the user cancelled, OpenAI now @@ -174,9 +170,7 @@ export class AgentLoop { // this.onItem(cancelNotice); this.generation += 1; - if (isLoggingEnabled()) { - log(`AgentLoop.cancel(): generation bumped to ${this.generation}`); - } + log(`AgentLoop.cancel(): generation bumped to ${this.generation}`); } /** @@ -315,13 +309,11 @@ export class AgentLoop { const callId: string = (item as any).call_id ?? (item as any).id; const args = parseToolCallArguments(rawArguments ?? "{}"); - if (isLoggingEnabled()) { - log( - `handleFunctionCall(): name=${ - name ?? "undefined" - } callId=${callId} args=${rawArguments}`, - ); - } + log( + `handleFunctionCall(): name=${ + name ?? "undefined" + } callId=${callId} args=${rawArguments}`, + ); if (args == null) { const outputItem: ResponseInputItem.FunctionCallOutput = { @@ -407,11 +399,9 @@ export class AgentLoop { // Create a fresh AbortController for this run so that tool calls from a // previous run do not accidentally get signalled. this.execAbortController = new AbortController(); - if (isLoggingEnabled()) { - log( - `AgentLoop.run(): new execAbortController created (${this.execAbortController.signal}) for generation ${this.generation}`, - ); - } + log( + `AgentLoop.run(): new execAbortController created (${this.execAbortController.signal}) for generation ${this.generation}`, + ); // NOTE: We no longer (re‑)attach an `abort` listener to `hardAbort` here. // A single listener that forwards the `abort` to the current // `execAbortController` is installed once in the constructor. Re‑adding a @@ -502,11 +492,9 @@ export class AgentLoop { const mergedInstructions = [prefix, this.instructions] .filter(Boolean) .join("\n"); - if (isLoggingEnabled()) { - log( - `instructions (length ${mergedInstructions.length}): ${mergedInstructions}`, - ); - } + log( + `instructions (length ${mergedInstructions.length}): ${mergedInstructions}`, + ); // eslint-disable-next-line no-await-in-loop stream = await this.oai.responses.create({ model: this.model, @@ -733,9 +721,7 @@ export class AgentLoop { try { // eslint-disable-next-line no-await-in-loop for await (const event of stream) { - if (isLoggingEnabled()) { - log(`AgentLoop.run(): response event ${event.type}`); - } + log(`AgentLoop.run(): response event ${event.type}`); // process and surface each item (no‑op until we can depend on streaming events) if (event.type === "response.output_item.done") { diff --git a/codex-cli/src/utils/agent/handle-exec-command.ts b/codex-cli/src/utils/agent/handle-exec-command.ts index 1af390e226..97b78c0817 100644 --- a/codex-cli/src/utils/agent/handle-exec-command.ts +++ b/codex-cli/src/utils/agent/handle-exec-command.ts @@ -223,23 +223,22 @@ async function execCommand( workdir = process.cwd(); } } - if (isLoggingEnabled()) { - if (applyPatchCommand != null) { - log("EXEC running apply_patch command"); - } else { - const { cmd, timeoutInMillis } = execInput; - // Seconds are a bit easier to read in log messages and most timeouts - // are specified as multiples of 1000, anyway. - const timeout = - timeoutInMillis != null - ? Math.round(timeoutInMillis / 1000).toString() - : "undefined"; - log( - `EXEC running \`${formatCommandForDisplay( - cmd, - )}\` in workdir=${workdir} with timeout=${timeout}s`, - ); - } + + if (applyPatchCommand != null) { + log("EXEC running apply_patch command"); + } else if (isLoggingEnabled()) { + const { cmd, timeoutInMillis } = execInput; + // Seconds are a bit easier to read in log messages and most timeouts + // are specified as multiples of 1000, anyway. + const timeout = + timeoutInMillis != null + ? Math.round(timeoutInMillis / 1000).toString() + : "undefined"; + log( + `EXEC running \`${formatCommandForDisplay( + cmd, + )}\` in workdir=${workdir} with timeout=${timeout}s`, + ); } // Note execApplyPatch() and exec() are coded defensively and should not diff --git a/codex-cli/src/utils/agent/log.ts b/codex-cli/src/utils/agent/log.ts index e804386566..e4aca06c79 100644 --- a/codex-cli/src/utils/agent/log.ts +++ b/codex-cli/src/utils/agent/log.ts @@ -124,6 +124,14 @@ export function log(message: string): void { (logger ?? initLogger()).log(message); } +/** + * USE SPARINGLY! This function should only be used to guard a call to log() if + * the log message is large and you want to avoid constructing it if logging is + * disabled. + * + * `log()` is already a no-op if DEBUG is not set, so an extra + * `isLoggingEnabled()` check is unnecessary. + */ export function isLoggingEnabled(): boolean { return (logger ?? initLogger()).isLoggingEnabled(); } diff --git a/codex-cli/src/utils/agent/platform-commands.ts b/codex-cli/src/utils/agent/platform-commands.ts index 7be02c7ac5..085c575d44 100644 --- a/codex-cli/src/utils/agent/platform-commands.ts +++ b/codex-cli/src/utils/agent/platform-commands.ts @@ -2,7 +2,7 @@ * Utility functions for handling platform-specific commands */ -import { log, isLoggingEnabled } from "./log.js"; +import { log } from "./log.js"; /** * Map of Unix commands to their Windows equivalents @@ -59,9 +59,7 @@ export function adaptCommandForPlatform(command: Array): Array { return command; } - if (isLoggingEnabled()) { - log(`Adapting command '${cmd}' for Windows platform`); - } + log(`Adapting command '${cmd}' for Windows platform`); // Create a new command array with the adapted command const adaptedCommand = [...command]; @@ -78,9 +76,7 @@ export function adaptCommandForPlatform(command: Array): Array { } } - if (isLoggingEnabled()) { - log(`Adapted command: ${adaptedCommand.join(" ")}`); - } + log(`Adapted command: ${adaptedCommand.join(" ")}`); return adaptedCommand; } diff --git a/codex-cli/src/utils/agent/sandbox/raw-exec.ts b/codex-cli/src/utils/agent/sandbox/raw-exec.ts index e7bcdb2a13..35ae8a4a92 100644 --- a/codex-cli/src/utils/agent/sandbox/raw-exec.ts +++ b/codex-cli/src/utils/agent/sandbox/raw-exec.ts @@ -7,7 +7,7 @@ import type { StdioPipe, } from "child_process"; -import { log, isLoggingEnabled } from "../log.js"; +import { log } from "../log.js"; import { adaptCommandForPlatform } from "../platform-commands.js"; import { spawn } from "child_process"; import * as os from "os"; @@ -27,10 +27,7 @@ export function exec( // Adapt command for the current platform (e.g., convert 'ls' to 'dir' on Windows) const adaptedCommand = adaptCommandForPlatform(command); - if ( - isLoggingEnabled() && - JSON.stringify(adaptedCommand) !== JSON.stringify(command) - ) { + if (JSON.stringify(adaptedCommand) !== JSON.stringify(command)) { log( `Command adapted for platform: ${command.join( " ", @@ -95,9 +92,7 @@ export function exec( // timely fashion. if (abortSignal) { const abortHandler = () => { - if (isLoggingEnabled()) { - log(`raw-exec: abort signal received – killing child ${child.pid}`); - } + log(`raw-exec: abort signal received – killing child ${child.pid}`); const killTarget = (signal: NodeJS.Signals) => { if (!child.pid) { return; @@ -194,11 +189,9 @@ export function exec( exitCode = 1; } - if (isLoggingEnabled()) { - log( - `raw-exec: child ${child.pid} exited code=${exitCode} signal=${signal}`, - ); - } + log( + `raw-exec: child ${child.pid} exited code=${exitCode} signal=${signal}`, + ); resolve({ stdout, stderr, diff --git a/codex-cli/src/utils/config.ts b/codex-cli/src/utils/config.ts index 8467653580..3be77059e0 100644 --- a/codex-cli/src/utils/config.ts +++ b/codex-cli/src/utils/config.ts @@ -8,7 +8,7 @@ import type { FullAutoErrorMode } from "./auto-approval-mode.js"; -import { log, isLoggingEnabled } from "./agent/log.js"; +import { log } from "./agent/log.js"; import { AutoApprovalMode } from "./auto-approval-mode.js"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; import { load as loadYaml, dump as dumpYaml } from "js-yaml"; @@ -245,15 +245,11 @@ export const loadConfig = ( ? resolvePath(cwd, options.projectDocPath) : discoverProjectDocPath(cwd); if (projectDocPath) { - if (isLoggingEnabled()) { - log( - `[codex] Loaded project doc from ${projectDocPath} (${projectDoc.length} bytes)`, - ); - } + log( + `[codex] Loaded project doc from ${projectDocPath} (${projectDoc.length} bytes)`, + ); } else { - if (isLoggingEnabled()) { - log(`[codex] No project doc found in ${cwd}`); - } + log(`[codex] No project doc found in ${cwd}`); } } From cdef7dfddcb4090a4042b1c03a33c101dca9cc5f Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Sat, 19 Apr 2025 11:45:21 -0700 Subject: [PATCH 28/41] reduce max output of ExecResult --- codex-cli/src/utils/agent/sandbox/raw-exec.ts | 130 +++++++++++++----- 1 file changed, 96 insertions(+), 34 deletions(-) diff --git a/codex-cli/src/utils/agent/sandbox/raw-exec.ts b/codex-cli/src/utils/agent/sandbox/raw-exec.ts index 35ae8a4a92..22add83ed8 100644 --- a/codex-cli/src/utils/agent/sandbox/raw-exec.ts +++ b/codex-cli/src/utils/agent/sandbox/raw-exec.ts @@ -12,7 +12,10 @@ import { adaptCommandForPlatform } from "../platform-commands.js"; import { spawn } from "child_process"; import * as os from "os"; -const MAX_BUFFER = 1024 * 100; // 100 KB +// Maximum output cap: either MAX_OUTPUT_LINES lines or MAX_OUTPUT_BYTES bytes, +// whichever limit is reached first. +const MAX_OUTPUT_BYTES = 1024 * 10; // 10 KB +const MAX_OUTPUT_LINES = 256; /** * This function should never return a rejected promise: errors should be @@ -143,37 +146,14 @@ export function exec( // resolve the promise and translate the failure into a regular // ExecResult object so the rest of the agent loop can carry on gracefully. - const stdoutChunks: Array = []; - const stderrChunks: Array = []; - let numStdoutBytes = 0; - let numStderrBytes = 0; - let hitMaxStdout = false; - let hitMaxStderr = false; - return new Promise((resolve) => { - child.stdout?.on("data", (data: Buffer) => { - if (!hitMaxStdout) { - numStdoutBytes += data.length; - if (numStdoutBytes <= MAX_BUFFER) { - stdoutChunks.push(data); - } else { - hitMaxStdout = true; - } - } - }); - child.stderr?.on("data", (data: Buffer) => { - if (!hitMaxStderr) { - numStderrBytes += data.length; - if (numStderrBytes <= MAX_BUFFER) { - stderrChunks.push(data); - } else { - hitMaxStderr = true; - } - } - }); + // Collect stdout and stderr up to configured limits. + const stdoutCollector = createTruncatingCollector(child.stdout!); + const stderrCollector = createTruncatingCollector(child.stderr!); + child.on("exit", (code, signal) => { - const stdout = Buffer.concat(stdoutChunks).toString("utf8"); - const stderr = Buffer.concat(stderrChunks).toString("utf8"); + const stdout = stdoutCollector.getString(); + const stderr = stderrCollector.getString(); // Map (code, signal) to an exit code. We expect exactly one of the two // values to be non-null, but we code defensively to handle the case where @@ -192,19 +172,101 @@ export function exec( log( `raw-exec: child ${child.pid} exited code=${exitCode} signal=${signal}`, ); - resolve({ + + const execResult = { stdout, stderr, exitCode, - }); + }; + resolve( + addTruncationWarningsIfNecessary( + execResult, + stdoutCollector.hit, + stderrCollector.hit, + ), + ); }); child.on("error", (err) => { - resolve({ + const execResult = { stdout: "", stderr: String(err), exitCode: 1, - }); + }; + resolve( + addTruncationWarningsIfNecessary( + execResult, + stdoutCollector.hit, + stderrCollector.hit, + ), + ); }); }); } + +/** + * Creates a collector that accumulates data Buffers from a stream up to + * specified byte and line limits. After either limit is exceeded, further + * data is ignored. + */ +function createTruncatingCollector( + stream: NodeJS.ReadableStream, + byteLimit: number = MAX_OUTPUT_BYTES, + lineLimit: number = MAX_OUTPUT_LINES, +) { + const chunks: Array = []; + let totalBytes = 0; + let totalLines = 0; + let hitLimit = false; + + stream?.on("data", (data: Buffer) => { + if (hitLimit) { + return; + } + totalBytes += data.length; + for (let i = 0; i < data.length; i++) { + if (data[i] === 0x0a) { + totalLines++; + } + } + if (totalBytes <= byteLimit && totalLines <= lineLimit) { + chunks.push(data); + } else { + hitLimit = true; + } + }); + + return { + getString() { + return Buffer.concat(chunks).toString("utf8"); + }, + /** True if either byte or line limit was exceeded */ + get hit(): boolean { + return hitLimit; + }, + }; +} + +/** + * Adds a truncation warnings to stdout and stderr, if appropriate. + */ +function addTruncationWarningsIfNecessary( + execResult: ExecResult, + hitMaxStdout: boolean, + hitMaxStderr: boolean, +): ExecResult { + if (!hitMaxStdout && !hitMaxStderr) { + return execResult; + } else { + const { stdout, stderr, exitCode } = execResult; + return { + stdout: hitMaxStdout + ? stdout + "\n\n[Output truncated: too many lines or bytes]" + : stdout, + stderr: hitMaxStderr + ? stderr + "\n\n[Output truncated: too many lines or bytes]" + : stderr, + exitCode, + }; + } +} From a7a4a69ccc5527bf691b2f0d1d230745790f1cdd Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Sat, 19 Apr 2025 18:29:11 -0700 Subject: [PATCH 29/41] CONFIG_DIR should not be in the list of writable roots by default --- .../src/utils/agent/sandbox/macos-seatbelt.ts | 15 +++++++-------- codex-cli/src/utils/agent/sandbox/raw-exec.ts | 2 +- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/codex-cli/src/utils/agent/sandbox/macos-seatbelt.ts b/codex-cli/src/utils/agent/sandbox/macos-seatbelt.ts index 0317458261..760ba63d8c 100644 --- a/codex-cli/src/utils/agent/sandbox/macos-seatbelt.ts +++ b/codex-cli/src/utils/agent/sandbox/macos-seatbelt.ts @@ -3,11 +3,9 @@ import type { SpawnOptions } from "child_process"; import { exec } from "./raw-exec.js"; import { log } from "../log.js"; -import { CONFIG_DIR } from "src/utils/config.js"; function getCommonRoots() { return [ - CONFIG_DIR, // Without this root, it'll cause: // pyenv: cannot rehash: $HOME/.pyenv/shims isn't writable `${process.env["HOME"]}/.pyenv`, @@ -17,16 +15,17 @@ function getCommonRoots() { export function execWithSeatbelt( cmd: Array, opts: SpawnOptions, - writableRoots: Array, + writableRoots: ReadonlyArray, abortSignal?: AbortSignal, ): Promise { let scopedWritePolicy: string; let policyTemplateParams: Array; - if (writableRoots.length > 0) { - // Add `~/.codex` to the list of writable roots - // (if there's any already, not in read-only mode) - getCommonRoots().map((root) => writableRoots.push(root)); - const { policies, params } = writableRoots + + const fullWritableRoots = [...writableRoots, ...getCommonRoots()]; + // In practice, fullWritableRoots will be non-empty, but we check just in + // case the logic to build up fullWritableRoots changes. + if (fullWritableRoots.length > 0) { + const { policies, params } = fullWritableRoots .map((root, index) => ({ policy: `(subpath (param "WRITABLE_ROOT_${index}"))`, param: `-DWRITABLE_ROOT_${index}=${root}`, diff --git a/codex-cli/src/utils/agent/sandbox/raw-exec.ts b/codex-cli/src/utils/agent/sandbox/raw-exec.ts index 6cfb304731..e7bcdb2a13 100644 --- a/codex-cli/src/utils/agent/sandbox/raw-exec.ts +++ b/codex-cli/src/utils/agent/sandbox/raw-exec.ts @@ -21,7 +21,7 @@ const MAX_BUFFER = 1024 * 100; // 100 KB export function exec( command: Array, options: SpawnOptions, - _writableRoots: Array, + _writableRoots: ReadonlyArray, abortSignal?: AbortSignal, ): Promise { // Adapt command for the current platform (e.g., convert 'ls' to 'dir' on Windows) From 4025cb6909e2cc90234918dfdfeb07cebf2f6365 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Sat, 19 Apr 2025 18:29:11 -0700 Subject: [PATCH 30/41] remove unnecessary isLoggingEnabled() checks --- .../chat/terminal-chat-input-thinking.tsx | 18 ++--- .../components/chat/terminal-chat-input.tsx | 18 ++--- .../chat/terminal-chat-new-input.tsx | 18 ++--- .../src/components/chat/terminal-chat.tsx | 65 +++++++------------ codex-cli/src/utils/agent/agent-loop.ts | 58 +++++++---------- .../src/utils/agent/handle-exec-command.ts | 33 +++++----- codex-cli/src/utils/agent/log.ts | 8 +++ .../src/utils/agent/platform-commands.ts | 10 +-- codex-cli/src/utils/agent/sandbox/raw-exec.ts | 19 ++---- codex-cli/src/utils/config.ts | 14 ++-- 10 files changed, 103 insertions(+), 158 deletions(-) diff --git a/codex-cli/src/components/chat/terminal-chat-input-thinking.tsx b/codex-cli/src/components/chat/terminal-chat-input-thinking.tsx index 213dd8c9af..fdc8bd218c 100644 --- a/codex-cli/src/components/chat/terminal-chat-input-thinking.tsx +++ b/codex-cli/src/components/chat/terminal-chat-input-thinking.tsx @@ -1,4 +1,4 @@ -import { log, isLoggingEnabled } from "../../utils/agent/log.js"; +import { log } from "../../utils/agent/log.js"; import { Box, Text, useInput, useStdin } from "ink"; import React, { useState } from "react"; import { useInterval } from "use-interval"; @@ -40,11 +40,9 @@ export default function TerminalChatInputThinking({ const str = Buffer.isBuffer(data) ? data.toString("utf8") : data; if (str === "\x1b\x1b") { - if (isLoggingEnabled()) { - log( - "raw stdin: received collapsed ESC ESC – starting confirmation timer", - ); - } + log( + "raw stdin: received collapsed ESC ESC – starting confirmation timer", + ); setAwaitingConfirm(true); setTimeout(() => setAwaitingConfirm(false), 1500); } @@ -65,15 +63,11 @@ export default function TerminalChatInputThinking({ } if (awaitingConfirm) { - if (isLoggingEnabled()) { - log("useInput: second ESC detected – triggering onInterrupt()"); - } + log("useInput: second ESC detected – triggering onInterrupt()"); onInterrupt(); setAwaitingConfirm(false); } else { - if (isLoggingEnabled()) { - log("useInput: first ESC detected – waiting for confirmation"); - } + log("useInput: first ESC detected – waiting for confirmation"); setAwaitingConfirm(true); setTimeout(() => setAwaitingConfirm(false), 1500); } diff --git a/codex-cli/src/components/chat/terminal-chat-input.tsx b/codex-cli/src/components/chat/terminal-chat-input.tsx index e1bcbf9ea4..525bbd9c23 100644 --- a/codex-cli/src/components/chat/terminal-chat-input.tsx +++ b/codex-cli/src/components/chat/terminal-chat-input.tsx @@ -6,7 +6,7 @@ import type { } from "openai/resources/responses/responses.mjs"; import { TerminalChatCommandReview } from "./terminal-chat-command-review.js"; -import { log, isLoggingEnabled } from "../../utils/agent/log.js"; +import { log } from "../../utils/agent/log.js"; import { loadConfig } from "../../utils/config.js"; import { createInputItem } from "../../utils/input-utils.js"; import { setSessionId } from "../../utils/session.js"; @@ -692,11 +692,9 @@ function TerminalChatInputThinking({ const str = Buffer.isBuffer(data) ? data.toString("utf8") : data; if (str === "\x1b\x1b") { // Treat as the first Escape press – prompt the user for confirmation. - if (isLoggingEnabled()) { - log( - "raw stdin: received collapsed ESC ESC – starting confirmation timer", - ); - } + log( + "raw stdin: received collapsed ESC ESC – starting confirmation timer", + ); setAwaitingConfirm(true); setTimeout(() => setAwaitingConfirm(false), 1500); } @@ -721,15 +719,11 @@ function TerminalChatInputThinking({ } if (awaitingConfirm) { - if (isLoggingEnabled()) { - log("useInput: second ESC detected – triggering onInterrupt()"); - } + log("useInput: second ESC detected – triggering onInterrupt()"); onInterrupt(); setAwaitingConfirm(false); } else { - if (isLoggingEnabled()) { - log("useInput: first ESC detected – waiting for confirmation"); - } + log("useInput: first ESC detected – waiting for confirmation"); setAwaitingConfirm(true); setTimeout(() => setAwaitingConfirm(false), 1500); } diff --git a/codex-cli/src/components/chat/terminal-chat-new-input.tsx b/codex-cli/src/components/chat/terminal-chat-new-input.tsx index 57acba3625..7dbe130e2c 100644 --- a/codex-cli/src/components/chat/terminal-chat-new-input.tsx +++ b/codex-cli/src/components/chat/terminal-chat-new-input.tsx @@ -8,7 +8,7 @@ import type { import MultilineTextEditor from "./multiline-editor"; import { TerminalChatCommandReview } from "./terminal-chat-command-review.js"; -import { log, isLoggingEnabled } from "../../utils/agent/log.js"; +import { log } from "../../utils/agent/log.js"; import { loadConfig } from "../../utils/config.js"; import { createInputItem } from "../../utils/input-utils.js"; import { setSessionId } from "../../utils/session.js"; @@ -505,11 +505,9 @@ function TerminalChatInputThinking({ const str = Buffer.isBuffer(data) ? data.toString("utf8") : data; if (str === "\x1b\x1b") { // Treat as the first Escape press – prompt the user for confirmation. - if (isLoggingEnabled()) { - log( - "raw stdin: received collapsed ESC ESC – starting confirmation timer", - ); - } + log( + "raw stdin: received collapsed ESC ESC – starting confirmation timer", + ); setAwaitingConfirm(true); setTimeout(() => setAwaitingConfirm(false), 1500); } @@ -531,15 +529,11 @@ function TerminalChatInputThinking({ } if (awaitingConfirm) { - if (isLoggingEnabled()) { - log("useInput: second ESC detected – triggering onInterrupt()"); - } + log("useInput: second ESC detected – triggering onInterrupt()"); onInterrupt(); setAwaitingConfirm(false); } else { - if (isLoggingEnabled()) { - log("useInput: first ESC detected – waiting for confirmation"); - } + log("useInput: first ESC detected – waiting for confirmation"); setAwaitingConfirm(true); setTimeout(() => setAwaitingConfirm(false), 1500); } diff --git a/codex-cli/src/components/chat/terminal-chat.tsx b/codex-cli/src/components/chat/terminal-chat.tsx index fd4cff5df5..825d0b9a92 100644 --- a/codex-cli/src/components/chat/terminal-chat.tsx +++ b/codex-cli/src/components/chat/terminal-chat.tsx @@ -15,7 +15,7 @@ import { formatCommandForDisplay } from "../../format-command.js"; import { useConfirmation } from "../../hooks/use-confirmation.js"; import { useTerminalSize } from "../../hooks/use-terminal-size.js"; import { AgentLoop } from "../../utils/agent/agent-loop.js"; -import { isLoggingEnabled, log } from "../../utils/agent/log.js"; +import { log } from "../../utils/agent/log.js"; import { ReviewDecision } from "../../utils/agent/review.js"; import { generateCompactSummary } from "../../utils/compact-summary.js"; import { OPENAI_BASE_URL } from "../../utils/config.js"; @@ -207,30 +207,25 @@ export default function TerminalChat({ // ──────────────────────────────────────────────────────────────── // DEBUG: log every render w/ key bits of state // ──────────────────────────────────────────────────────────────── - if (isLoggingEnabled()) { - log( - `render – agent? ${Boolean(agentRef.current)} loading=${loading} items=${ - items.length - }`, - ); - } + log( + `render – agent? ${Boolean(agentRef.current)} loading=${loading} items=${ + items.length + }`, + ); useEffect(() => { // Skip recreating the agent if awaiting a decision on a pending confirmation if (confirmationPrompt != null) { - if (isLoggingEnabled()) { - log("skip AgentLoop recreation due to pending confirmationPrompt"); - } + log("skip AgentLoop recreation due to pending confirmationPrompt"); return; } - if (isLoggingEnabled()) { - log("creating NEW AgentLoop"); - log( - `model=${model} instructions=${Boolean( - config.instructions, - )} approvalPolicy=${approvalPolicy}`, - ); - } + + log("creating NEW AgentLoop"); + log( + `model=${model} instructions=${Boolean( + config.instructions, + )} approvalPolicy=${approvalPolicy}`, + ); // Tear down any existing loop before creating a new one agentRef.current?.terminate(); @@ -298,14 +293,10 @@ export default function TerminalChat({ // force a render so JSX below can "see" the freshly created agent forceUpdate(); - if (isLoggingEnabled()) { - log(`AgentLoop created: ${inspect(agentRef.current, { depth: 1 })}`); - } + log(`AgentLoop created: ${inspect(agentRef.current, { depth: 1 })}`); return () => { - if (isLoggingEnabled()) { - log("terminating AgentLoop"); - } + log("terminating AgentLoop"); agentRef.current?.terminate(); agentRef.current = undefined; forceUpdate(); // re‑render after teardown too @@ -387,9 +378,7 @@ export default function TerminalChat({ // Let's also track whenever the ref becomes available const agent = agentRef.current; useEffect(() => { - if (isLoggingEnabled()) { - log(`agentRef.current is now ${Boolean(agent)}`); - } + log(`agentRef.current is now ${Boolean(agent)}`); }, [agent]); // --------------------------------------------------------------------- @@ -534,11 +523,9 @@ export default function TerminalChat({ if (!agent) { return; } - if (isLoggingEnabled()) { - log( - "TerminalChat: interruptAgent invoked – calling agent.cancel()", - ); - } + log( + "TerminalChat: interruptAgent invoked – calling agent.cancel()", + ); agent.cancel(); setLoading(false); @@ -574,13 +561,11 @@ export default function TerminalChat({ currentModel={model} hasLastResponse={Boolean(lastResponseId)} onSelect={(newModel) => { - if (isLoggingEnabled()) { - log( - "TerminalChat: interruptAgent invoked – calling agent.cancel()", - ); - if (!agent) { - log("TerminalChat: agent is not ready yet"); - } + log( + "TerminalChat: interruptAgent invoked – calling agent.cancel()", + ); + if (!agent) { + log("TerminalChat: agent is not ready yet"); } agent?.cancel(); setLoading(false); diff --git a/codex-cli/src/utils/agent/agent-loop.ts b/codex-cli/src/utils/agent/agent-loop.ts index cbe3c56960..172ba67295 100644 --- a/codex-cli/src/utils/agent/agent-loop.ts +++ b/codex-cli/src/utils/agent/agent-loop.ts @@ -8,7 +8,7 @@ import type { } from "openai/resources/responses/responses.mjs"; import type { Reasoning } from "openai/resources.mjs"; -import { log, isLoggingEnabled } from "./log.js"; +import { log } from "./log.js"; import { OPENAI_BASE_URL, OPENAI_TIMEOUT_MS } from "../config.js"; import { parseToolCallArguments } from "../parsers.js"; import { @@ -116,15 +116,13 @@ export class AgentLoop { // Reset the current stream to allow new requests this.currentStream = null; - if (isLoggingEnabled()) { - log( - `AgentLoop.cancel() invoked – currentStream=${Boolean( - this.currentStream, - )} execAbortController=${Boolean( - this.execAbortController, - )} generation=${this.generation}`, - ); - } + log( + `AgentLoop.cancel() invoked – currentStream=${Boolean( + this.currentStream, + )} execAbortController=${Boolean(this.execAbortController)} generation=${ + this.generation + }`, + ); ( this.currentStream as { controller?: { abort?: () => void } } | null )?.controller?.abort?.(); @@ -136,9 +134,7 @@ export class AgentLoop { // Create a new abort controller for future tool calls this.execAbortController = new AbortController(); - if (isLoggingEnabled()) { - log("AgentLoop.cancel(): execAbortController.abort() called"); - } + log("AgentLoop.cancel(): execAbortController.abort() called"); // NOTE: We intentionally do *not* clear `lastResponseId` here. If the // stream produced a `function_call` before the user cancelled, OpenAI now @@ -174,9 +170,7 @@ export class AgentLoop { // this.onItem(cancelNotice); this.generation += 1; - if (isLoggingEnabled()) { - log(`AgentLoop.cancel(): generation bumped to ${this.generation}`); - } + log(`AgentLoop.cancel(): generation bumped to ${this.generation}`); } /** @@ -315,13 +309,11 @@ export class AgentLoop { const callId: string = (item as any).call_id ?? (item as any).id; const args = parseToolCallArguments(rawArguments ?? "{}"); - if (isLoggingEnabled()) { - log( - `handleFunctionCall(): name=${ - name ?? "undefined" - } callId=${callId} args=${rawArguments}`, - ); - } + log( + `handleFunctionCall(): name=${ + name ?? "undefined" + } callId=${callId} args=${rawArguments}`, + ); if (args == null) { const outputItem: ResponseInputItem.FunctionCallOutput = { @@ -407,11 +399,9 @@ export class AgentLoop { // Create a fresh AbortController for this run so that tool calls from a // previous run do not accidentally get signalled. this.execAbortController = new AbortController(); - if (isLoggingEnabled()) { - log( - `AgentLoop.run(): new execAbortController created (${this.execAbortController.signal}) for generation ${this.generation}`, - ); - } + log( + `AgentLoop.run(): new execAbortController created (${this.execAbortController.signal}) for generation ${this.generation}`, + ); // NOTE: We no longer (re‑)attach an `abort` listener to `hardAbort` here. // A single listener that forwards the `abort` to the current // `execAbortController` is installed once in the constructor. Re‑adding a @@ -502,11 +492,9 @@ export class AgentLoop { const mergedInstructions = [prefix, this.instructions] .filter(Boolean) .join("\n"); - if (isLoggingEnabled()) { - log( - `instructions (length ${mergedInstructions.length}): ${mergedInstructions}`, - ); - } + log( + `instructions (length ${mergedInstructions.length}): ${mergedInstructions}`, + ); // eslint-disable-next-line no-await-in-loop stream = await this.oai.responses.create({ model: this.model, @@ -733,9 +721,7 @@ export class AgentLoop { try { // eslint-disable-next-line no-await-in-loop for await (const event of stream) { - if (isLoggingEnabled()) { - log(`AgentLoop.run(): response event ${event.type}`); - } + log(`AgentLoop.run(): response event ${event.type}`); // process and surface each item (no‑op until we can depend on streaming events) if (event.type === "response.output_item.done") { diff --git a/codex-cli/src/utils/agent/handle-exec-command.ts b/codex-cli/src/utils/agent/handle-exec-command.ts index 1af390e226..97b78c0817 100644 --- a/codex-cli/src/utils/agent/handle-exec-command.ts +++ b/codex-cli/src/utils/agent/handle-exec-command.ts @@ -223,23 +223,22 @@ async function execCommand( workdir = process.cwd(); } } - if (isLoggingEnabled()) { - if (applyPatchCommand != null) { - log("EXEC running apply_patch command"); - } else { - const { cmd, timeoutInMillis } = execInput; - // Seconds are a bit easier to read in log messages and most timeouts - // are specified as multiples of 1000, anyway. - const timeout = - timeoutInMillis != null - ? Math.round(timeoutInMillis / 1000).toString() - : "undefined"; - log( - `EXEC running \`${formatCommandForDisplay( - cmd, - )}\` in workdir=${workdir} with timeout=${timeout}s`, - ); - } + + if (applyPatchCommand != null) { + log("EXEC running apply_patch command"); + } else if (isLoggingEnabled()) { + const { cmd, timeoutInMillis } = execInput; + // Seconds are a bit easier to read in log messages and most timeouts + // are specified as multiples of 1000, anyway. + const timeout = + timeoutInMillis != null + ? Math.round(timeoutInMillis / 1000).toString() + : "undefined"; + log( + `EXEC running \`${formatCommandForDisplay( + cmd, + )}\` in workdir=${workdir} with timeout=${timeout}s`, + ); } // Note execApplyPatch() and exec() are coded defensively and should not diff --git a/codex-cli/src/utils/agent/log.ts b/codex-cli/src/utils/agent/log.ts index e804386566..e4aca06c79 100644 --- a/codex-cli/src/utils/agent/log.ts +++ b/codex-cli/src/utils/agent/log.ts @@ -124,6 +124,14 @@ export function log(message: string): void { (logger ?? initLogger()).log(message); } +/** + * USE SPARINGLY! This function should only be used to guard a call to log() if + * the log message is large and you want to avoid constructing it if logging is + * disabled. + * + * `log()` is already a no-op if DEBUG is not set, so an extra + * `isLoggingEnabled()` check is unnecessary. + */ export function isLoggingEnabled(): boolean { return (logger ?? initLogger()).isLoggingEnabled(); } diff --git a/codex-cli/src/utils/agent/platform-commands.ts b/codex-cli/src/utils/agent/platform-commands.ts index 7be02c7ac5..085c575d44 100644 --- a/codex-cli/src/utils/agent/platform-commands.ts +++ b/codex-cli/src/utils/agent/platform-commands.ts @@ -2,7 +2,7 @@ * Utility functions for handling platform-specific commands */ -import { log, isLoggingEnabled } from "./log.js"; +import { log } from "./log.js"; /** * Map of Unix commands to their Windows equivalents @@ -59,9 +59,7 @@ export function adaptCommandForPlatform(command: Array): Array { return command; } - if (isLoggingEnabled()) { - log(`Adapting command '${cmd}' for Windows platform`); - } + log(`Adapting command '${cmd}' for Windows platform`); // Create a new command array with the adapted command const adaptedCommand = [...command]; @@ -78,9 +76,7 @@ export function adaptCommandForPlatform(command: Array): Array { } } - if (isLoggingEnabled()) { - log(`Adapted command: ${adaptedCommand.join(" ")}`); - } + log(`Adapted command: ${adaptedCommand.join(" ")}`); return adaptedCommand; } diff --git a/codex-cli/src/utils/agent/sandbox/raw-exec.ts b/codex-cli/src/utils/agent/sandbox/raw-exec.ts index e7bcdb2a13..35ae8a4a92 100644 --- a/codex-cli/src/utils/agent/sandbox/raw-exec.ts +++ b/codex-cli/src/utils/agent/sandbox/raw-exec.ts @@ -7,7 +7,7 @@ import type { StdioPipe, } from "child_process"; -import { log, isLoggingEnabled } from "../log.js"; +import { log } from "../log.js"; import { adaptCommandForPlatform } from "../platform-commands.js"; import { spawn } from "child_process"; import * as os from "os"; @@ -27,10 +27,7 @@ export function exec( // Adapt command for the current platform (e.g., convert 'ls' to 'dir' on Windows) const adaptedCommand = adaptCommandForPlatform(command); - if ( - isLoggingEnabled() && - JSON.stringify(adaptedCommand) !== JSON.stringify(command) - ) { + if (JSON.stringify(adaptedCommand) !== JSON.stringify(command)) { log( `Command adapted for platform: ${command.join( " ", @@ -95,9 +92,7 @@ export function exec( // timely fashion. if (abortSignal) { const abortHandler = () => { - if (isLoggingEnabled()) { - log(`raw-exec: abort signal received – killing child ${child.pid}`); - } + log(`raw-exec: abort signal received – killing child ${child.pid}`); const killTarget = (signal: NodeJS.Signals) => { if (!child.pid) { return; @@ -194,11 +189,9 @@ export function exec( exitCode = 1; } - if (isLoggingEnabled()) { - log( - `raw-exec: child ${child.pid} exited code=${exitCode} signal=${signal}`, - ); - } + log( + `raw-exec: child ${child.pid} exited code=${exitCode} signal=${signal}`, + ); resolve({ stdout, stderr, diff --git a/codex-cli/src/utils/config.ts b/codex-cli/src/utils/config.ts index 8467653580..3be77059e0 100644 --- a/codex-cli/src/utils/config.ts +++ b/codex-cli/src/utils/config.ts @@ -8,7 +8,7 @@ import type { FullAutoErrorMode } from "./auto-approval-mode.js"; -import { log, isLoggingEnabled } from "./agent/log.js"; +import { log } from "./agent/log.js"; import { AutoApprovalMode } from "./auto-approval-mode.js"; import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs"; import { load as loadYaml, dump as dumpYaml } from "js-yaml"; @@ -245,15 +245,11 @@ export const loadConfig = ( ? resolvePath(cwd, options.projectDocPath) : discoverProjectDocPath(cwd); if (projectDocPath) { - if (isLoggingEnabled()) { - log( - `[codex] Loaded project doc from ${projectDocPath} (${projectDoc.length} bytes)`, - ); - } + log( + `[codex] Loaded project doc from ${projectDocPath} (${projectDoc.length} bytes)`, + ); } else { - if (isLoggingEnabled()) { - log(`[codex] No project doc found in ${cwd}`); - } + log(`[codex] No project doc found in ${cwd}`); } } From 698381bae42bc5c93167af819de46caac9aab911 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Sat, 19 Apr 2025 18:29:11 -0700 Subject: [PATCH 31/41] reduce max output of ExecResult --- codex-cli/src/utils/agent/sandbox/raw-exec.ts | 130 +++++++++++++----- 1 file changed, 96 insertions(+), 34 deletions(-) diff --git a/codex-cli/src/utils/agent/sandbox/raw-exec.ts b/codex-cli/src/utils/agent/sandbox/raw-exec.ts index 35ae8a4a92..22add83ed8 100644 --- a/codex-cli/src/utils/agent/sandbox/raw-exec.ts +++ b/codex-cli/src/utils/agent/sandbox/raw-exec.ts @@ -12,7 +12,10 @@ import { adaptCommandForPlatform } from "../platform-commands.js"; import { spawn } from "child_process"; import * as os from "os"; -const MAX_BUFFER = 1024 * 100; // 100 KB +// Maximum output cap: either MAX_OUTPUT_LINES lines or MAX_OUTPUT_BYTES bytes, +// whichever limit is reached first. +const MAX_OUTPUT_BYTES = 1024 * 10; // 10 KB +const MAX_OUTPUT_LINES = 256; /** * This function should never return a rejected promise: errors should be @@ -143,37 +146,14 @@ export function exec( // resolve the promise and translate the failure into a regular // ExecResult object so the rest of the agent loop can carry on gracefully. - const stdoutChunks: Array = []; - const stderrChunks: Array = []; - let numStdoutBytes = 0; - let numStderrBytes = 0; - let hitMaxStdout = false; - let hitMaxStderr = false; - return new Promise((resolve) => { - child.stdout?.on("data", (data: Buffer) => { - if (!hitMaxStdout) { - numStdoutBytes += data.length; - if (numStdoutBytes <= MAX_BUFFER) { - stdoutChunks.push(data); - } else { - hitMaxStdout = true; - } - } - }); - child.stderr?.on("data", (data: Buffer) => { - if (!hitMaxStderr) { - numStderrBytes += data.length; - if (numStderrBytes <= MAX_BUFFER) { - stderrChunks.push(data); - } else { - hitMaxStderr = true; - } - } - }); + // Collect stdout and stderr up to configured limits. + const stdoutCollector = createTruncatingCollector(child.stdout!); + const stderrCollector = createTruncatingCollector(child.stderr!); + child.on("exit", (code, signal) => { - const stdout = Buffer.concat(stdoutChunks).toString("utf8"); - const stderr = Buffer.concat(stderrChunks).toString("utf8"); + const stdout = stdoutCollector.getString(); + const stderr = stderrCollector.getString(); // Map (code, signal) to an exit code. We expect exactly one of the two // values to be non-null, but we code defensively to handle the case where @@ -192,19 +172,101 @@ export function exec( log( `raw-exec: child ${child.pid} exited code=${exitCode} signal=${signal}`, ); - resolve({ + + const execResult = { stdout, stderr, exitCode, - }); + }; + resolve( + addTruncationWarningsIfNecessary( + execResult, + stdoutCollector.hit, + stderrCollector.hit, + ), + ); }); child.on("error", (err) => { - resolve({ + const execResult = { stdout: "", stderr: String(err), exitCode: 1, - }); + }; + resolve( + addTruncationWarningsIfNecessary( + execResult, + stdoutCollector.hit, + stderrCollector.hit, + ), + ); }); }); } + +/** + * Creates a collector that accumulates data Buffers from a stream up to + * specified byte and line limits. After either limit is exceeded, further + * data is ignored. + */ +function createTruncatingCollector( + stream: NodeJS.ReadableStream, + byteLimit: number = MAX_OUTPUT_BYTES, + lineLimit: number = MAX_OUTPUT_LINES, +) { + const chunks: Array = []; + let totalBytes = 0; + let totalLines = 0; + let hitLimit = false; + + stream?.on("data", (data: Buffer) => { + if (hitLimit) { + return; + } + totalBytes += data.length; + for (let i = 0; i < data.length; i++) { + if (data[i] === 0x0a) { + totalLines++; + } + } + if (totalBytes <= byteLimit && totalLines <= lineLimit) { + chunks.push(data); + } else { + hitLimit = true; + } + }); + + return { + getString() { + return Buffer.concat(chunks).toString("utf8"); + }, + /** True if either byte or line limit was exceeded */ + get hit(): boolean { + return hitLimit; + }, + }; +} + +/** + * Adds a truncation warnings to stdout and stderr, if appropriate. + */ +function addTruncationWarningsIfNecessary( + execResult: ExecResult, + hitMaxStdout: boolean, + hitMaxStderr: boolean, +): ExecResult { + if (!hitMaxStdout && !hitMaxStderr) { + return execResult; + } else { + const { stdout, stderr, exitCode } = execResult; + return { + stdout: hitMaxStdout + ? stdout + "\n\n[Output truncated: too many lines or bytes]" + : stdout, + stderr: hitMaxStderr + ? stderr + "\n\n[Output truncated: too many lines or bytes]" + : stderr, + exitCode, + }; + } +} From b54367d34b192ebe26943a53ab3f1124cde2c44a Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 21 Apr 2025 09:47:58 -0700 Subject: [PATCH 32/41] revert #386 due to unsafe shell command parsing --- README.md | 3 --- codex-cli/src/approvals.ts | 19 ------------------- codex-cli/src/utils/config.ts | 16 ---------------- codex-cli/tests/approvals.test.ts | 31 +------------------------------ 4 files changed, 1 insertion(+), 68 deletions(-) diff --git a/README.md b/README.md index 2094b9398c..e5690d7d48 100644 --- a/README.md +++ b/README.md @@ -289,9 +289,6 @@ model: o4-mini # Default model approvalMode: suggest # or auto-edit, full-auto fullAutoErrorMode: ask-user # or ignore-and-continue notify: true # Enable desktop notifications for responses -safeCommands: - - npm test # Automatically approve npm test - - yarn lint # Automatically approve yarn lint ``` ```json diff --git a/codex-cli/src/approvals.ts b/codex-cli/src/approvals.ts index b9eb50d1d7..f4a35402f9 100644 --- a/codex-cli/src/approvals.ts +++ b/codex-cli/src/approvals.ts @@ -4,7 +4,6 @@ import { identify_files_added, identify_files_needed, } from "./utils/agent/apply-patch"; -import { loadConfig } from "./utils/config"; import * as path from "path"; import { parse } from "shell-quote"; @@ -297,24 +296,6 @@ export function isSafeCommand( ): SafeCommandReason | null { const [cmd0, cmd1, cmd2, cmd3] = command; - const config = loadConfig(); - if (config.safeCommands && Array.isArray(config.safeCommands)) { - for (const safe of config.safeCommands) { - // safe: "npm test" → ["npm", "test"] - const safeArr = typeof safe === "string" ? safe.trim().split(/\s+/) : []; - if ( - safeArr.length > 0 && - safeArr.length <= command.length && - safeArr.every((v, i) => v === command[i]) - ) { - return { - reason: "User-defined safe command", - group: "User config", - }; - } - } - } - switch (cmd0) { case "cd": return { diff --git a/codex-cli/src/utils/config.ts b/codex-cli/src/utils/config.ts index a4a9c0cb0e..190d111762 100644 --- a/codex-cli/src/utils/config.ts +++ b/codex-cli/src/utils/config.ts @@ -78,8 +78,6 @@ export type StoredConfig = { saveHistory?: boolean; sensitivePatterns?: Array; }; - /** User-defined safe commands */ - safeCommands?: Array; }; // Minimal config written on first run. An *empty* model string ensures that @@ -113,8 +111,6 @@ export type AppConfig = { saveHistory: boolean; sensitivePatterns: Array; }; - /** User-defined safe commands */ - safeCommands?: Array; }; // --------------------------------------------------------------------------- @@ -297,7 +293,6 @@ export const loadConfig = ( instructions: combinedInstructions, notify: storedConfig.notify === true, approvalMode: storedConfig.approvalMode, - safeCommands: storedConfig.safeCommands ?? [], }; // ----------------------------------------------------------------------- @@ -375,13 +370,6 @@ export const loadConfig = ( }; } - // Load user-defined safe commands - if (Array.isArray(storedConfig.safeCommands)) { - config.safeCommands = storedConfig.safeCommands.map(String); - } else { - config.safeCommands = []; - } - return config; }; @@ -425,10 +413,6 @@ export const saveConfig = ( sensitivePatterns: config.history.sensitivePatterns, }; } - // Save: User-defined safe commands - if (config.safeCommands && config.safeCommands.length > 0) { - configToSave.safeCommands = config.safeCommands; - } if (ext === ".yaml" || ext === ".yml") { writeFileSync(targetPath, dumpYaml(configToSave), "utf-8"); diff --git a/codex-cli/tests/approvals.test.ts b/codex-cli/tests/approvals.test.ts index 43490cf84e..a39adff460 100644 --- a/codex-cli/tests/approvals.test.ts +++ b/codex-cli/tests/approvals.test.ts @@ -1,13 +1,7 @@ import type { SafetyAssessment } from "../src/approvals"; import { canAutoApprove } from "../src/approvals"; -import { describe, test, expect, vi } from "vitest"; - -vi.mock("../src/utils/config", () => ({ - loadConfig: () => ({ - safeCommands: ["npm test", "sl"], - }), -})); +import { describe, test, expect } from "vitest"; describe("canAutoApprove()", () => { const env = { @@ -95,27 +89,4 @@ describe("canAutoApprove()", () => { expect(check(["cargo", "build"])).toEqual({ type: "ask-user" }); }); - - test("commands in safeCommands config should be safe", async () => { - expect(check(["npm", "test"])).toEqual({ - type: "auto-approve", - reason: "User-defined safe command", - group: "User config", - runInSandbox: false, - }); - - expect(check(["sl"])).toEqual({ - type: "auto-approve", - reason: "User-defined safe command", - group: "User config", - runInSandbox: false, - }); - - expect(check(["npm", "test", "--watch"])).toEqual({ - type: "auto-approve", - reason: "User-defined safe command", - group: "User config", - runInSandbox: false, - }); - }); }); From cb1e6383064b9a2531a19f70f68ae734d207d4f6 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 21 Apr 2025 10:15:48 -0700 Subject: [PATCH 33/41] do not auto-approve the find command if it contains options that write files or spawn commands --- codex-cli/src/approvals.ts | 34 +++++++++++++++++--- codex-cli/tests/approvals.test.ts | 52 +++++++++++++++++++++++++++++++ 2 files changed, 81 insertions(+), 5 deletions(-) diff --git a/codex-cli/src/approvals.ts b/codex-cli/src/approvals.ts index f4a35402f9..ff37a8903f 100644 --- a/codex-cli/src/approvals.ts +++ b/codex-cli/src/approvals.ts @@ -329,11 +329,20 @@ export function isSafeCommand( reason: "Ripgrep search", group: "Searching", }; - case "find": - return { - reason: "Find files or directories", - group: "Searching", - }; + case "find": { + // Certain options to `find` allow executing arbitrary processes, so we + // cannot auto-approve them. + if ( + command.some((arg: string) => UNSAFE_OPTIONS_FOR_FIND_COMMAND.has(arg)) + ) { + break; + } else { + return { + reason: "Find files or directories", + group: "Searching", + }; + } + } case "grep": return { reason: "Text search (grep)", @@ -421,6 +430,21 @@ function isValidSedNArg(arg: string | undefined): boolean { return arg != null && /^(\d+,)?\d+p$/.test(arg); } +const UNSAFE_OPTIONS_FOR_FIND_COMMAND: ReadonlySet = new Set([ + // Options that can execute arbitrary commands. + "-exec", + "-execdir", + "-ok", + "-okdir", + // Option that deletes matching files. + "-delete", + // Options that write pathnames to a file. + "-fls", + "-fprint", + "-fprint0", + "-fprintf", +]); + // ---------------- Helper utilities for complex shell expressions ----------------- // A conservative allow-list of bash operators that do not, on their own, cause diff --git a/codex-cli/tests/approvals.test.ts b/codex-cli/tests/approvals.test.ts index a39adff460..a90abad6eb 100644 --- a/codex-cli/tests/approvals.test.ts +++ b/codex-cli/tests/approvals.test.ts @@ -89,4 +89,56 @@ describe("canAutoApprove()", () => { expect(check(["cargo", "build"])).toEqual({ type: "ask-user" }); }); + + test("find", () => { + expect(check(["find", ".", "-name", "file.txt"])).toEqual({ + type: "auto-approve", + reason: "Find files or directories", + group: "Searching", + runInSandbox: false, + }); + + // Options that can execute arbitrary commands. + expect( + check(["find", ".", "-name", "file.txt", "-exec", "rm", "{}", ";"]), + ).toEqual({ + type: "ask-user", + }); + expect( + check(["find", ".", "-name", "*.py", "-execdir", "python3", "{}", ";"]), + ).toEqual({ + type: "ask-user", + }); + expect( + check(["find", ".", "-name", "file.txt", "-ok", "rm", "{}", ";"]), + ).toEqual({ + type: "ask-user", + }); + expect( + check(["find", ".", "-name", "*.py", "-okdir", "python3", "{}", ";"]), + ).toEqual({ + type: "ask-user", + }); + + // Option that deletes matching files. + expect(check(["find", ".", "-delete", "-name", "file.txt"])).toEqual({ + type: "ask-user", + }); + + // Options that write pathnames to a file. + expect(check(["find", ".", "-fls", "/etc/passwd"])).toEqual({ + type: "ask-user", + }); + expect(check(["find", ".", "-fprint", "/etc/passwd"])).toEqual({ + type: "ask-user", + }); + expect(check(["find", ".", "-fprint0", "/etc/passwd"])).toEqual({ + type: "ask-user", + }); + expect( + check(["find", ".", "-fprintf", "/root/suid.txt", "%#m %u %p\n"]), + ).toEqual({ + type: "ask-user", + }); + }); }); From a1afcb6916fa5b17d12d20637718b03ffd968969 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 21 Apr 2025 16:37:23 -0700 Subject: [PATCH 34/41] add instructions for connecting to a visual debugger under Contributing --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index 1abae778cf..e07db1b02f 100644 --- a/README.md +++ b/README.md @@ -448,6 +448,15 @@ pnpm lint:fix pnpm format:fix ``` +### Debugging + +To debug the CLI with a visual debugger, do the following in the `codex-cli` folder: + +- Build: run `pnpm run build`, which will generate `cli.js.map` alongside `cli.js` +- Run the CLI with `node --inspect-brk ./dist/cli.js` The program then waits until a debugger is attached before proceeding. Options: + - In VS Code, choose **Debug: Attach to Node Process** from the command palette and choose the option in the dropdown with debug port `9229` (likely the first option) + - Go to in Chrome and find **localhost:9229** and click **trace** + #### Nix Flake Development Prerequisite: Nix >= 2.4 with flakes enabled (`experimental-features = nix-command flakes` in `~/.config/nix/nix.conf`). From d13a3708b3634c12449d7406a82a52482558e2ef Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 21 Apr 2025 16:58:28 -0700 Subject: [PATCH 35/41] include fractional portion of chunk that exceeds stdout/stderr limit --- codex-cli/src/utils/agent/sandbox/raw-exec.ts | 35 ++++++++++++++++--- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/codex-cli/src/utils/agent/sandbox/raw-exec.ts b/codex-cli/src/utils/agent/sandbox/raw-exec.ts index b3d1d8ec25..a04cd0b4e1 100644 --- a/codex-cli/src/utils/agent/sandbox/raw-exec.ts +++ b/codex-cli/src/utils/agent/sandbox/raw-exec.ts @@ -223,15 +223,42 @@ function createTruncatingCollector( if (hitLimit) { return; } - totalBytes += data.length; - for (let i = 0; i < data.length; i++) { + const dataLength = data.length; + let newlineCount = 0; + for (let i = 0; i < dataLength; i++) { if (data[i] === 0x0a) { - totalLines++; + newlineCount++; } } - if (totalBytes <= byteLimit && totalLines <= lineLimit) { + // If entire chunk fits within byte and line limits, take it whole + if (totalBytes + dataLength <= byteLimit && totalLines + newlineCount <= lineLimit) { chunks.push(data); + totalBytes += dataLength; + totalLines += newlineCount; } else { + // Otherwise, take a partial slice up to the first limit breach + const allowedBytes = byteLimit - totalBytes; + const allowedLines = lineLimit - totalLines; + let bytesTaken = 0; + let linesSeen = 0; + for (let i = 0; i < dataLength; i++) { + if (bytesTaken === allowedBytes) { + break; + } + const byte = data[i]; + if (byte === 0x0a) { + if (linesSeen === allowedLines) { + break; + } + linesSeen++; + } + bytesTaken++; + } + if (bytesTaken > 0) { + chunks.push(data.slice(0, bytesTaken)); + totalBytes += bytesTaken; + totalLines += linesSeen; + } hitLimit = true; } }); From 0f9dfe3b8ae64e2274411d67be976eed028d9821 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 21 Apr 2025 16:58:28 -0700 Subject: [PATCH 36/41] include fractional portion of chunk that exceeds stdout/stderr limit --- .../sandbox/create-truncating-collector.ts | 72 +++++++++++++++++++ codex-cli/src/utils/agent/sandbox/raw-exec.ts | 44 +----------- .../tests/create-truncating-collector.test.ts | 55 ++++++++++++++ 3 files changed, 128 insertions(+), 43 deletions(-) create mode 100644 codex-cli/src/utils/agent/sandbox/create-truncating-collector.ts create mode 100644 codex-cli/tests/create-truncating-collector.test.ts diff --git a/codex-cli/src/utils/agent/sandbox/create-truncating-collector.ts b/codex-cli/src/utils/agent/sandbox/create-truncating-collector.ts new file mode 100644 index 0000000000..2e8b6b63e7 --- /dev/null +++ b/codex-cli/src/utils/agent/sandbox/create-truncating-collector.ts @@ -0,0 +1,72 @@ +/** + * Creates a collector that accumulates data Buffers from a stream up to + * specified byte and line limits. After either limit is exceeded, further + * data is ignored. + */ +export function createTruncatingCollector( + stream: NodeJS.ReadableStream, + byteLimit: number = MAX_OUTPUT_BYTES, + lineLimit: number = MAX_OUTPUT_LINES, +) { + const chunks: Array = []; + let totalBytes = 0; + let totalLines = 0; + let hitLimit = false; + + stream?.on("data", (data: Buffer) => { + if (hitLimit) { + return; + } + const dataLength = data.length; + let newlineCount = 0; + for (let i = 0; i < dataLength; i++) { + if (data[i] === 0x0a) { + newlineCount++; + } + } + // If entire chunk fits within byte and line limits, take it whole + if ( + totalBytes + dataLength <= byteLimit && + totalLines + newlineCount <= lineLimit + ) { + chunks.push(data); + totalBytes += dataLength; + totalLines += newlineCount; + } else { + // Otherwise, take a partial slice up to the first limit breach + const allowedBytes = byteLimit - totalBytes; + const allowedLines = lineLimit - totalLines; + let bytesTaken = 0; + let linesSeen = 0; + for (let i = 0; i < dataLength; i++) { + if (bytesTaken === allowedBytes) { + break; + } + const byte = data[i]; + if (byte === 0x0a) { + if (linesSeen === allowedLines) { + break; + } + linesSeen++; + } + bytesTaken++; + } + if (bytesTaken > 0) { + chunks.push(data.slice(0, bytesTaken)); + totalBytes += bytesTaken; + totalLines += linesSeen; + } + hitLimit = true; + } + }); + + return { + getString() { + return Buffer.concat(chunks).toString("utf8"); + }, + /** True if either byte or line limit was exceeded */ + get hit(): boolean { + return hitLimit; + }, + }; +} diff --git a/codex-cli/src/utils/agent/sandbox/raw-exec.ts b/codex-cli/src/utils/agent/sandbox/raw-exec.ts index b3d1d8ec25..9917536f65 100644 --- a/codex-cli/src/utils/agent/sandbox/raw-exec.ts +++ b/codex-cli/src/utils/agent/sandbox/raw-exec.ts @@ -9,6 +9,7 @@ import type { import { log } from "../../logger/log.js"; import { adaptCommandForPlatform } from "../platform-commands.js"; +import { createTruncatingCollector } from "./create-truncating-collector"; import { spawn } from "child_process"; import * as os from "os"; @@ -204,49 +205,6 @@ export function exec( }); } -/** - * Creates a collector that accumulates data Buffers from a stream up to - * specified byte and line limits. After either limit is exceeded, further - * data is ignored. - */ -function createTruncatingCollector( - stream: NodeJS.ReadableStream, - byteLimit: number = MAX_OUTPUT_BYTES, - lineLimit: number = MAX_OUTPUT_LINES, -) { - const chunks: Array = []; - let totalBytes = 0; - let totalLines = 0; - let hitLimit = false; - - stream?.on("data", (data: Buffer) => { - if (hitLimit) { - return; - } - totalBytes += data.length; - for (let i = 0; i < data.length; i++) { - if (data[i] === 0x0a) { - totalLines++; - } - } - if (totalBytes <= byteLimit && totalLines <= lineLimit) { - chunks.push(data); - } else { - hitLimit = true; - } - }); - - return { - getString() { - return Buffer.concat(chunks).toString("utf8"); - }, - /** True if either byte or line limit was exceeded */ - get hit(): boolean { - return hitLimit; - }, - }; -} - /** * Adds a truncation warnings to stdout and stderr, if appropriate. */ diff --git a/codex-cli/tests/create-truncating-collector.test.ts b/codex-cli/tests/create-truncating-collector.test.ts new file mode 100644 index 0000000000..ad3dee558f --- /dev/null +++ b/codex-cli/tests/create-truncating-collector.test.ts @@ -0,0 +1,55 @@ +import { PassThrough } from "stream"; +import { once } from "events"; +import { describe, it, expect } from "vitest"; +import { createTruncatingCollector } from "../src/utils/agent/sandbox/create-truncating-collector.js"; + +describe("createTruncatingCollector", () => { + it("collects data under limits without truncation", async () => { + const stream = new PassThrough(); + const collector = createTruncatingCollector(stream, 100, 10); + const data = "line1\nline2\n"; + stream.end(Buffer.from(data)); + await once(stream, "end"); + expect(collector.getString()).toBe(data); + expect(collector.hit).toBe(false); + }); + + it("truncates data over byte limit", async () => { + const stream = new PassThrough(); + const collector = createTruncatingCollector(stream, 5, 100); + stream.end(Buffer.from("hello world")); + await once(stream, "end"); + expect(collector.getString()).toBe("hello"); + expect(collector.hit).toBe(true); + }); + + it("truncates data over line limit", async () => { + const stream = new PassThrough(); + const collector = createTruncatingCollector(stream, 1000, 2); + const data = "a\nb\nc\nd\n"; + stream.end(Buffer.from(data)); + await once(stream, "end"); + expect(collector.getString()).toBe("a\nb\n"); + expect(collector.hit).toBe(true); + }); + + it("stops collecting after limit is hit across multiple writes", async () => { + const stream = new PassThrough(); + const collector = createTruncatingCollector(stream, 10, 2); + stream.write(Buffer.from("1\n")); + stream.write(Buffer.from("2\n3\n4\n")); + stream.end(); + await once(stream, "end"); + expect(collector.getString()).toBe("1\n2\n"); + expect(collector.hit).toBe(true); + }); + + it("handles zero limits", async () => { + const stream = new PassThrough(); + const collector = createTruncatingCollector(stream, 0, 0); + stream.end(Buffer.from("anything\n")); + await once(stream, "end"); + expect(collector.getString()).toBe(""); + expect(collector.hit).toBe(true); + }); +}); From 5fa24ca8ab1d1ca418c4e06665ada795ffb4581a Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 21 Apr 2025 16:58:28 -0700 Subject: [PATCH 37/41] include fractional portion of chunk that exceeds stdout/stderr limit --- .../sandbox/create-truncating-collector.ts | 80 +++++++++++++++++++ codex-cli/src/utils/agent/sandbox/raw-exec.ts | 49 +----------- .../tests/create-truncating-collector.test.ts | 55 +++++++++++++ 3 files changed, 136 insertions(+), 48 deletions(-) create mode 100644 codex-cli/src/utils/agent/sandbox/create-truncating-collector.ts create mode 100644 codex-cli/tests/create-truncating-collector.test.ts diff --git a/codex-cli/src/utils/agent/sandbox/create-truncating-collector.ts b/codex-cli/src/utils/agent/sandbox/create-truncating-collector.ts new file mode 100644 index 0000000000..ed98444b91 --- /dev/null +++ b/codex-cli/src/utils/agent/sandbox/create-truncating-collector.ts @@ -0,0 +1,80 @@ +// Maximum output cap: either MAX_OUTPUT_LINES lines or MAX_OUTPUT_BYTES bytes, +// whichever limit is reached first. +const MAX_OUTPUT_BYTES = 1024 * 10; // 10 KB +const MAX_OUTPUT_LINES = 256; + +/** + * Creates a collector that accumulates data Buffers from a stream up to + * specified byte and line limits. After either limit is exceeded, further + * data is ignored. + */ +export function createTruncatingCollector( + stream: NodeJS.ReadableStream, + byteLimit: number = MAX_OUTPUT_BYTES, + lineLimit: number = MAX_OUTPUT_LINES, +): { + getString: () => string; + hit: boolean; +} { + const chunks: Array = []; + let totalBytes = 0; + let totalLines = 0; + let hitLimit = false; + + stream?.on("data", (data: Buffer) => { + if (hitLimit) { + return; + } + const dataLength = data.length; + let newlineCount = 0; + for (let i = 0; i < dataLength; i++) { + if (data[i] === 0x0a) { + newlineCount++; + } + } + // If entire chunk fits within byte and line limits, take it whole + if ( + totalBytes + dataLength <= byteLimit && + totalLines + newlineCount <= lineLimit + ) { + chunks.push(data); + totalBytes += dataLength; + totalLines += newlineCount; + } else { + // Otherwise, take a partial slice up to the first limit breach + const allowedBytes = byteLimit - totalBytes; + const allowedLines = lineLimit - totalLines; + let bytesTaken = 0; + let linesSeen = 0; + for (let i = 0; i < dataLength; i++) { + if (bytesTaken === allowedBytes) { + break; + } + const byte = data[i]; + if (byte === 0x0a) { + if (linesSeen === allowedLines) { + break; + } + linesSeen++; + } + bytesTaken++; + } + if (bytesTaken > 0) { + chunks.push(data.slice(0, bytesTaken)); + totalBytes += bytesTaken; + totalLines += linesSeen; + } + hitLimit = true; + } + }); + + return { + getString() { + return Buffer.concat(chunks).toString("utf8"); + }, + /** True if either byte or line limit was exceeded */ + get hit(): boolean { + return hitLimit; + }, + }; +} diff --git a/codex-cli/src/utils/agent/sandbox/raw-exec.ts b/codex-cli/src/utils/agent/sandbox/raw-exec.ts index b3d1d8ec25..b33feb8518 100644 --- a/codex-cli/src/utils/agent/sandbox/raw-exec.ts +++ b/codex-cli/src/utils/agent/sandbox/raw-exec.ts @@ -9,14 +9,10 @@ import type { import { log } from "../../logger/log.js"; import { adaptCommandForPlatform } from "../platform-commands.js"; +import { createTruncatingCollector } from "./create-truncating-collector"; import { spawn } from "child_process"; import * as os from "os"; -// Maximum output cap: either MAX_OUTPUT_LINES lines or MAX_OUTPUT_BYTES bytes, -// whichever limit is reached first. -const MAX_OUTPUT_BYTES = 1024 * 10; // 10 KB -const MAX_OUTPUT_LINES = 256; - /** * This function should never return a rejected promise: errors should be * mapped to a non-zero exit code and the error message should be in stderr. @@ -204,49 +200,6 @@ export function exec( }); } -/** - * Creates a collector that accumulates data Buffers from a stream up to - * specified byte and line limits. After either limit is exceeded, further - * data is ignored. - */ -function createTruncatingCollector( - stream: NodeJS.ReadableStream, - byteLimit: number = MAX_OUTPUT_BYTES, - lineLimit: number = MAX_OUTPUT_LINES, -) { - const chunks: Array = []; - let totalBytes = 0; - let totalLines = 0; - let hitLimit = false; - - stream?.on("data", (data: Buffer) => { - if (hitLimit) { - return; - } - totalBytes += data.length; - for (let i = 0; i < data.length; i++) { - if (data[i] === 0x0a) { - totalLines++; - } - } - if (totalBytes <= byteLimit && totalLines <= lineLimit) { - chunks.push(data); - } else { - hitLimit = true; - } - }); - - return { - getString() { - return Buffer.concat(chunks).toString("utf8"); - }, - /** True if either byte or line limit was exceeded */ - get hit(): boolean { - return hitLimit; - }, - }; -} - /** * Adds a truncation warnings to stdout and stderr, if appropriate. */ diff --git a/codex-cli/tests/create-truncating-collector.test.ts b/codex-cli/tests/create-truncating-collector.test.ts new file mode 100644 index 0000000000..ad3dee558f --- /dev/null +++ b/codex-cli/tests/create-truncating-collector.test.ts @@ -0,0 +1,55 @@ +import { PassThrough } from "stream"; +import { once } from "events"; +import { describe, it, expect } from "vitest"; +import { createTruncatingCollector } from "../src/utils/agent/sandbox/create-truncating-collector.js"; + +describe("createTruncatingCollector", () => { + it("collects data under limits without truncation", async () => { + const stream = new PassThrough(); + const collector = createTruncatingCollector(stream, 100, 10); + const data = "line1\nline2\n"; + stream.end(Buffer.from(data)); + await once(stream, "end"); + expect(collector.getString()).toBe(data); + expect(collector.hit).toBe(false); + }); + + it("truncates data over byte limit", async () => { + const stream = new PassThrough(); + const collector = createTruncatingCollector(stream, 5, 100); + stream.end(Buffer.from("hello world")); + await once(stream, "end"); + expect(collector.getString()).toBe("hello"); + expect(collector.hit).toBe(true); + }); + + it("truncates data over line limit", async () => { + const stream = new PassThrough(); + const collector = createTruncatingCollector(stream, 1000, 2); + const data = "a\nb\nc\nd\n"; + stream.end(Buffer.from(data)); + await once(stream, "end"); + expect(collector.getString()).toBe("a\nb\n"); + expect(collector.hit).toBe(true); + }); + + it("stops collecting after limit is hit across multiple writes", async () => { + const stream = new PassThrough(); + const collector = createTruncatingCollector(stream, 10, 2); + stream.write(Buffer.from("1\n")); + stream.write(Buffer.from("2\n3\n4\n")); + stream.end(); + await once(stream, "end"); + expect(collector.getString()).toBe("1\n2\n"); + expect(collector.hit).toBe(true); + }); + + it("handles zero limits", async () => { + const stream = new PassThrough(); + const collector = createTruncatingCollector(stream, 0, 0); + stream.end(Buffer.from("anything\n")); + await once(stream, "end"); + expect(collector.getString()).toBe(""); + expect(collector.hit).toBe(true); + }); +}); From 8ff34daa4891e3597e6f7634dce5f4accaf3c8f2 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 21 Apr 2025 16:58:28 -0700 Subject: [PATCH 38/41] include fractional portion of chunk that exceeds stdout/stderr limit --- .../sandbox/create-truncating-collector.ts | 78 +++++++++++++++++++ codex-cli/src/utils/agent/sandbox/raw-exec.ts | 49 +----------- .../tests/create-truncating-collector.test.ts | 55 +++++++++++++ 3 files changed, 134 insertions(+), 48 deletions(-) create mode 100644 codex-cli/src/utils/agent/sandbox/create-truncating-collector.ts create mode 100644 codex-cli/tests/create-truncating-collector.test.ts diff --git a/codex-cli/src/utils/agent/sandbox/create-truncating-collector.ts b/codex-cli/src/utils/agent/sandbox/create-truncating-collector.ts new file mode 100644 index 0000000000..518d475c78 --- /dev/null +++ b/codex-cli/src/utils/agent/sandbox/create-truncating-collector.ts @@ -0,0 +1,78 @@ +// Maximum output cap: either MAX_OUTPUT_LINES lines or MAX_OUTPUT_BYTES bytes, +// whichever limit is reached first. +const MAX_OUTPUT_BYTES = 1024 * 10; // 10 KB +const MAX_OUTPUT_LINES = 256; + +/** + * Creates a collector that accumulates data Buffers from a stream up to + * specified byte and line limits. After either limit is exceeded, further + * data is ignored. + */ +export function createTruncatingCollector( + stream: NodeJS.ReadableStream, + byteLimit: number = MAX_OUTPUT_BYTES, + lineLimit: number = MAX_OUTPUT_LINES, +): { + getString: () => string; + hit: boolean; +} { + const chunks: Array = []; + let totalBytes = 0; + let totalLines = 0; + let hitLimit = false; + + stream?.on("data", (data: Buffer) => { + if (hitLimit) { + return; + } + const dataLength = data.length; + let newlineCount = 0; + for (let i = 0; i < dataLength; i++) { + if (data[i] === 0x0a) { + newlineCount++; + } + } + // If entire chunk fits within byte and line limits, take it whole + if ( + totalBytes + dataLength <= byteLimit && + totalLines + newlineCount <= lineLimit + ) { + chunks.push(data); + totalBytes += dataLength; + totalLines += newlineCount; + } else { + // Otherwise, take a partial slice up to the first limit breach + const allowedBytes = byteLimit - totalBytes; + const allowedLines = lineLimit - totalLines; + let bytesTaken = 0; + let linesSeen = 0; + for (let i = 0; i < dataLength; i++) { + // Stop if byte or line limit is reached + if (bytesTaken === allowedBytes || linesSeen === allowedLines) { + break; + } + const byte = data[i]; + if (byte === 0x0a) { + linesSeen++; + } + bytesTaken++; + } + if (bytesTaken > 0) { + chunks.push(data.slice(0, bytesTaken)); + totalBytes += bytesTaken; + totalLines += linesSeen; + } + hitLimit = true; + } + }); + + return { + getString() { + return Buffer.concat(chunks).toString("utf8"); + }, + /** True if either byte or line limit was exceeded */ + get hit(): boolean { + return hitLimit; + }, + }; +} diff --git a/codex-cli/src/utils/agent/sandbox/raw-exec.ts b/codex-cli/src/utils/agent/sandbox/raw-exec.ts index b3d1d8ec25..b33feb8518 100644 --- a/codex-cli/src/utils/agent/sandbox/raw-exec.ts +++ b/codex-cli/src/utils/agent/sandbox/raw-exec.ts @@ -9,14 +9,10 @@ import type { import { log } from "../../logger/log.js"; import { adaptCommandForPlatform } from "../platform-commands.js"; +import { createTruncatingCollector } from "./create-truncating-collector"; import { spawn } from "child_process"; import * as os from "os"; -// Maximum output cap: either MAX_OUTPUT_LINES lines or MAX_OUTPUT_BYTES bytes, -// whichever limit is reached first. -const MAX_OUTPUT_BYTES = 1024 * 10; // 10 KB -const MAX_OUTPUT_LINES = 256; - /** * This function should never return a rejected promise: errors should be * mapped to a non-zero exit code and the error message should be in stderr. @@ -204,49 +200,6 @@ export function exec( }); } -/** - * Creates a collector that accumulates data Buffers from a stream up to - * specified byte and line limits. After either limit is exceeded, further - * data is ignored. - */ -function createTruncatingCollector( - stream: NodeJS.ReadableStream, - byteLimit: number = MAX_OUTPUT_BYTES, - lineLimit: number = MAX_OUTPUT_LINES, -) { - const chunks: Array = []; - let totalBytes = 0; - let totalLines = 0; - let hitLimit = false; - - stream?.on("data", (data: Buffer) => { - if (hitLimit) { - return; - } - totalBytes += data.length; - for (let i = 0; i < data.length; i++) { - if (data[i] === 0x0a) { - totalLines++; - } - } - if (totalBytes <= byteLimit && totalLines <= lineLimit) { - chunks.push(data); - } else { - hitLimit = true; - } - }); - - return { - getString() { - return Buffer.concat(chunks).toString("utf8"); - }, - /** True if either byte or line limit was exceeded */ - get hit(): boolean { - return hitLimit; - }, - }; -} - /** * Adds a truncation warnings to stdout and stderr, if appropriate. */ diff --git a/codex-cli/tests/create-truncating-collector.test.ts b/codex-cli/tests/create-truncating-collector.test.ts new file mode 100644 index 0000000000..ad3dee558f --- /dev/null +++ b/codex-cli/tests/create-truncating-collector.test.ts @@ -0,0 +1,55 @@ +import { PassThrough } from "stream"; +import { once } from "events"; +import { describe, it, expect } from "vitest"; +import { createTruncatingCollector } from "../src/utils/agent/sandbox/create-truncating-collector.js"; + +describe("createTruncatingCollector", () => { + it("collects data under limits without truncation", async () => { + const stream = new PassThrough(); + const collector = createTruncatingCollector(stream, 100, 10); + const data = "line1\nline2\n"; + stream.end(Buffer.from(data)); + await once(stream, "end"); + expect(collector.getString()).toBe(data); + expect(collector.hit).toBe(false); + }); + + it("truncates data over byte limit", async () => { + const stream = new PassThrough(); + const collector = createTruncatingCollector(stream, 5, 100); + stream.end(Buffer.from("hello world")); + await once(stream, "end"); + expect(collector.getString()).toBe("hello"); + expect(collector.hit).toBe(true); + }); + + it("truncates data over line limit", async () => { + const stream = new PassThrough(); + const collector = createTruncatingCollector(stream, 1000, 2); + const data = "a\nb\nc\nd\n"; + stream.end(Buffer.from(data)); + await once(stream, "end"); + expect(collector.getString()).toBe("a\nb\n"); + expect(collector.hit).toBe(true); + }); + + it("stops collecting after limit is hit across multiple writes", async () => { + const stream = new PassThrough(); + const collector = createTruncatingCollector(stream, 10, 2); + stream.write(Buffer.from("1\n")); + stream.write(Buffer.from("2\n3\n4\n")); + stream.end(); + await once(stream, "end"); + expect(collector.getString()).toBe("1\n2\n"); + expect(collector.hit).toBe(true); + }); + + it("handles zero limits", async () => { + const stream = new PassThrough(); + const collector = createTruncatingCollector(stream, 0, 0); + stream.end(Buffer.from("anything\n")); + await once(stream, "end"); + expect(collector.getString()).toBe(""); + expect(collector.hit).toBe(true); + }); +}); From c616733e559f19d54b7be11edeab34da7d6b6bbe Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 21 Apr 2025 23:17:11 -0700 Subject: [PATCH 39/41] Enforce ASCII in README.md --- .github/workflows/ci.yml | 3 + README.md | 157 +++++++++++++++++++-------------------- scripts/asciicheck.py | 127 +++++++++++++++++++++++++++++++ 3 files changed, 208 insertions(+), 79 deletions(-) create mode 100755 scripts/asciicheck.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e99c2daa04..1f5dd0d31a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,3 +67,6 @@ jobs: - name: Build run: pnpm run build + + - name: Ensure README.md contains only ASCII and certain Unicode code points + run: ./scripts/asciicheck.py README.md diff --git a/README.md b/README.md index c9fcc93c80..d9a7665aae 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,13 @@ - [Experimental Technology Disclaimer](#experimental-technology-disclaimer) - [Quickstart](#quickstart) -- [Why Codex?](#whycodex) -- [Security Model \& Permissions](#securitymodelpermissions) +- [Why Codex?](#whycodex) +- [Security Model \& Permissions](#securitymodelpermissions) - [Platform sandboxing details](#platform-sandboxing-details) -- [System Requirements](#systemrequirements) -- [CLI Reference](#clireference) -- [Memory \& Project Docs](#memoryprojectdocs) -- [Non‑interactive / CI mode](#noninteractivecimode) +- [System Requirements](#systemrequirements) +- [CLI Reference](#clireference) +- [Memory \& Project Docs](#memoryprojectdocs) +- [Non-interactive / CI mode](#noninteractivecimode) - [Recipes](#recipes) - [Installation](#installation) - [Configuration](#configuration) @@ -27,7 +27,7 @@ - [Contributing](#contributing) - [Development workflow](#development-workflow) - [Nix Flake Development](#nix-flake-development) - - [Writing high‑impact code changes](#writing-highimpact-code-changes) + - [Writing high-impact code changes](#writing-highimpact-code-changes) - [Opening a pull request](#opening-a-pull-request) - [Review process](#review-process) - [Community values](#community-values) @@ -35,7 +35,7 @@ - [Contributor License Agreement (CLA)](#contributor-license-agreement-cla) - [Quick fixes](#quick-fixes) - [Releasing `codex`](#releasing-codex) -- [Security \& Responsible AI](#securityresponsibleai) +- [Security \& Responsible AI](#securityresponsibleai) - [License](#license) - [Zero Data Retention (ZDR) Organization Limitation](#zero-data-retention-zdr-organization-limitation) @@ -45,7 +45,7 @@ ## Experimental Technology Disclaimer -Codex CLI is an experimental project under active development. It is not yet stable, may contain bugs, incomplete features, or undergo breaking changes. We’re building it in the open with the community and welcome: +Codex CLI is an experimental project under active development. It is not yet stable, may contain bugs, incomplete features, or undergo breaking changes. We're building it in the open with the community and welcome: - Bug reports - Feature requests @@ -115,59 +115,59 @@ codex "explain this codebase to me" codex --approval-mode full-auto "create the fanciest todo-list app" ``` -That’s it – Codex will scaffold a file, run it inside a sandbox, install any +That's it - Codex will scaffold a file, run it inside a sandbox, install any missing dependencies, and show you the live result. Approve the changes and -they’ll be committed to your working directory. +they'll be committed to your working directory. --- -## Why Codex? +## Why Codex? Codex CLI is built for developers who already **live in the terminal** and want -ChatGPT‑level reasoning **plus** the power to actually run code, manipulate -files, and iterate – all under version control. In short, it’s _chat‑driven +ChatGPT-level reasoning **plus** the power to actually run code, manipulate +files, and iterate - all under version control. In short, it's _chat-driven development_ that understands and executes your repo. -- **Zero setup** — bring your OpenAI API key and it just works! +- **Zero setup** - bring your OpenAI API key and it just works! - **Full auto-approval, while safe + secure** by running network-disabled and directory-sandboxed -- **Multimodal** — pass in screenshots or diagrams to implement features ✨ +- **Multimodal** - pass in screenshots or diagrams to implement features ✨ And it's **fully open-source** so you can see and contribute to how it develops! --- -## Security Model & Permissions +## Security Model & Permissions Codex lets you decide _how much autonomy_ the agent receives and auto-approval policy via the `--approval-mode` flag (or the interactive onboarding prompt): -| Mode | What the agent may do without asking | Still requires approval | -| ------------------------- | -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | -| **Suggest**
(default) | • Read any file in the repo | • **All** file writes/patches
• **Any** arbitrary shell commands (aside from reading files) | -| **Auto Edit** | • Read **and** apply‑patch writes to files | • **All** shell commands | -| **Full Auto** | • Read/write files
• Execute shell commands (network disabled, writes limited to your workdir) | – | +| Mode | What the agent may do without asking | Still requires approval | +| ------------------------- | --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | +| **Suggest**
(default) |
  • Read any file in the repo |
  • **All** file writes/patches
  • **Any** arbitrary shell commands (aside from reading files) | +| **Auto Edit** |
  • Read **and** apply-patch writes to files |
  • **All** shell commands | +| **Full Auto** |
  • Read/write files
  • Execute shell commands (network disabled, writes limited to your workdir) | - | -In **Full Auto** every command is run **network‑disabled** and confined to the -current working directory (plus temporary files) for defense‑in‑depth. Codex -will also show a warning/confirmation if you start in **auto‑edit** or -**full‑auto** while the directory is _not_ tracked by Git, so you always have a +In **Full Auto** every command is run **network-disabled** and confined to the +current working directory (plus temporary files) for defense-in-depth. Codex +will also show a warning/confirmation if you start in **auto-edit** or +**full-auto** while the directory is _not_ tracked by Git, so you always have a safety net. -Coming soon: you’ll be able to whitelist specific commands to auto‑execute with -the network enabled, once we’re confident in additional safeguards. +Coming soon: you'll be able to whitelist specific commands to auto-execute with +the network enabled, once we're confident in additional safeguards. ### Platform sandboxing details The hardening mechanism Codex uses depends on your OS: -- **macOS 12+** – commands are wrapped with **Apple Seatbelt** (`sandbox-exec`). +- **macOS 12+** - commands are wrapped with **Apple Seatbelt** (`sandbox-exec`). - - Everything is placed in a read‑only jail except for a small set of + - Everything is placed in a read-only jail except for a small set of writable roots (`$PWD`, `$TMPDIR`, `~/.codex`, etc.). - - Outbound network is _fully blocked_ by default – even if a child process + - Outbound network is _fully blocked_ by default - even if a child process tries to `curl` somewhere it will fail. -- **Linux** – there is no sandboxing by default. +- **Linux** - there is no sandboxing by default. We recommend using Docker for sandboxing, where Codex launches itself inside a **minimal container image** and mounts your repo _read/write_ at the same path. A custom `iptables`/`ipset` firewall script denies all egress except the @@ -176,47 +176,47 @@ The hardening mechanism Codex uses depends on your OS: --- -## System Requirements +## System Requirements | Requirement | Details | | --------------------------- | --------------------------------------------------------------- | -| Operating systems | macOS 12+, Ubuntu 20.04+/Debian 10+, or Windows 11 **via WSL2** | +| Operating systems | macOS 12+, Ubuntu 20.04+/Debian 10+, or Windows 11 **via WSL2** | | Node.js | **22 or newer** (LTS recommended) | -| Git (optional, recommended) | 2.23+ for built‑in PR helpers | -| RAM | 4‑GB minimum (8‑GB recommended) | +| Git (optional, recommended) | 2.23+ for built-in PR helpers | +| RAM | 4-GB minimum (8-GB recommended) | > Never run `sudo npm install -g`; fix npm permissions instead. --- -## CLI Reference +## CLI Reference | Command | Purpose | Example | | ------------------------------------ | ----------------------------------- | ------------------------------------ | | `codex` | Interactive REPL | `codex` | -| `codex "…"` | Initial prompt for interactive REPL | `codex "fix lint errors"` | -| `codex -q "…"` | Non‑interactive "quiet mode" | `codex -q --json "explain utils.ts"` | +| `codex "..."` | Initial prompt for interactive REPL | `codex "fix lint errors"` | +| `codex -q "..."` | Non-interactive "quiet mode" | `codex -q --json "explain utils.ts"` | | `codex completion ` | Print shell completion script | `codex completion bash` | Key flags: `--model/-m`, `--approval-mode/-a`, `--quiet/-q`, and `--notify`. --- -## Memory & Project Docs +## Memory & Project Docs Codex merges Markdown instructions in this order: -1. `~/.codex/instructions.md` – personal global guidance -2. `codex.md` at repo root – shared project notes -3. `codex.md` in cwd – sub‑package specifics +1. `~/.codex/instructions.md` - personal global guidance +2. `codex.md` at repo root - shared project notes +3. `codex.md` in cwd - sub-package specifics Disable with `--no-project-doc` or `CODEX_DISABLE_PROJECT_DOC=1`. --- -## Non‑interactive / CI mode +## Non-interactive / CI mode -Run Codex head‑less in pipelines. Example GitHub Action step: +Run Codex head-less in pipelines. Example GitHub Action step: ```yaml - name: Update changelog via Codex @@ -240,15 +240,15 @@ DEBUG=true codex ## Recipes -Below are a few bite‑size examples you can copy‑paste. Replace the text in quotes with your own task. See the [prompting guide](https://github.com/openai/codex/blob/main/codex-cli/examples/prompting_guide.md) for more tips and usage patterns. +Below are a few bite-size examples you can copy-paste. Replace the text in quotes with your own task. See the [prompting guide](https://github.com/openai/codex/blob/main/codex-cli/examples/prompting_guide.md) for more tips and usage patterns. | ✨ | What you type | What happens | | --- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | -| 1 | `codex "Refactor the Dashboard component to React Hooks"` | Codex rewrites the class component, runs `npm test`, and shows the diff. | +| 1 | `codex "Refactor the Dashboard component to React Hooks"` | Codex rewrites the class component, runs `npm test`, and shows the diff. | | 2 | `codex "Generate SQL migrations for adding a users table"` | Infers your ORM, creates migration files, and runs them in a sandboxed DB. | | 3 | `codex "Write unit tests for utils/date.ts"` | Generates tests, executes them, and iterates until they pass. | -| 4 | `codex "Bulk‑rename *.jpeg → *.jpg with git mv"` | Safely renames files and updates imports/usages. | -| 5 | `codex "Explain what this regex does: ^(?=.*[A-Z]).{8,}$"` | Outputs a step‑by‑step human explanation. | +| 4 | `codex "Bulk-rename *.jpeg -> *.jpg with git mv"` | Safely renames files and updates imports/usages. | +| 5 | `codex "Explain what this regex does: ^(?=.*[A-Z]).{8,}$"` | Outputs a step-by-step human explanation. | | 6 | `codex "Carefully review this repo, and propose 3 high impact well-scoped PRs"` | Suggests impactful PRs in the current codebase. | | 7 | `codex "Look for vulnerabilities and create a security review report"` | Finds and explains security bugs. | @@ -257,7 +257,7 @@ Below are a few bite‑size examples you can copy‑paste. Replace the text in q ## Installation
    -From npm (Recommended) +From npm (Recommended) ```bash npm install -g @openai/codex @@ -272,7 +272,7 @@ pnpm add -g @openai/codex
    -Build from source +Build from source ```bash # Clone the repository and navigate to the CLI package @@ -289,7 +289,7 @@ pnpm build # Get the usage and the options node ./dist/cli.js --help -# Run the locally‑built CLI directly +# Run the locally-built CLI directly node ./dist/cli.js # Or link the command globally for convenience @@ -363,7 +363,7 @@ Codex runs model-generated commands in a sandbox. If a proposed command or file
    Does it work on Windows? -Not directly. It requires [Windows Subsystem for Linux (WSL2)](https://learn.microsoft.com/en-us/windows/wsl/install) – Codex has been tested on macOS and Linux with Node ≥ 22. +Not directly. It requires [Windows Subsystem for Linux (WSL2)](https://learn.microsoft.com/en-us/windows/wsl/install) - Codex has been tested on macOS and Linux with Node 22.
    @@ -394,12 +394,12 @@ OpenAI rejected the request. Error details: Status: 400, Code: unsupported_param ## Funding Opportunity -We’re excited to launch a **$1 million initiative** supporting open source projects that use Codex CLI and other OpenAI models. +We're excited to launch a **$1 million initiative** supporting open source projects that use Codex CLI and other OpenAI models. - Grants are awarded in **$25,000** API credit increments. - Applications are reviewed **on a rolling basis**. -**Interested? [Apply here](https://openai.com/form/codex-open-source-fund/).** +**Interested? [Apply here](https://openai.com/form/codex-open-source-fund/).** --- @@ -407,14 +407,14 @@ We’re excited to launch a **$1 million initiative** supporting open source pr This project is under active development and the code will likely change pretty significantly. We'll update this message once that's complete! -More broadly we welcome contributions – whether you are opening your very first pull request or you’re a seasoned maintainer. At the same time we care about reliability and long‑term maintainability, so the bar for merging code is intentionally **high**. The guidelines below spell out what “high‑quality” means in practice and should make the whole process transparent and friendly. +More broadly we welcome contributions - whether you are opening your very first pull request or you're a seasoned maintainer. At the same time we care about reliability and long-term maintainability, so the bar for merging code is intentionally **high**. The guidelines below spell out what "high-quality" means in practice and should make the whole process transparent and friendly. ### Development workflow -- Create a _topic branch_ from `main` – e.g. `feat/interactive-prompt`. +- Create a _topic branch_ from `main` - e.g. `feat/interactive-prompt`. - Keep your changes focused. Multiple unrelated fixes should be opened as separate PRs. -- Use `pnpm test:watch` during development for super‑fast feedback. -- We use **Vitest** for unit tests, **ESLint** + **Prettier** for style, and **TypeScript** for type‑checking. +- Use `pnpm test:watch` during development for super-fast feedback. +- We use **Vitest** for unit tests, **ESLint** + **Prettier** for style, and **TypeScript** for type-checking. - Before pushing, run the full test/type/lint suite: ### Git Hooks with Husky @@ -436,16 +436,16 @@ npm test && npm run lint && npm run typecheck I have read the CLA Document and I hereby sign the CLA ``` - The CLA‑Assistant bot will turn the PR status green once all authors have signed. + The CLA-Assistant bot will turn the PR status green once all authors have signed. ```bash -# Watch mode (tests rerun on change) +# Watch mode (tests rerun on change) pnpm test:watch -# Type‑check without emitting files +# Type-check without emitting files pnpm typecheck -# Automatically fix lint + prettier issues +# Automatically fix lint + prettier issues pnpm lint:fix pnpm format:fix ``` @@ -475,35 +475,35 @@ Run the CLI via the flake app: nix run .#codex ``` -### Writing high‑impact code changes +### Writing high-impact code changes 1. **Start with an issue.** Open a new one or comment on an existing discussion so we can agree on the solution before code is written. -2. **Add or update tests.** Every new feature or bug‑fix should come with test coverage that fails before your change and passes afterwards. 100 % coverage is not required, but aim for meaningful assertions. -3. **Document behaviour.** If your change affects user‑facing behaviour, update the README, inline help (`codex --help`), or relevant example projects. +2. **Add or update tests.** Every new feature or bug-fix should come with test coverage that fails before your change and passes afterwards. 100 % coverage is not required, but aim for meaningful assertions. +3. **Document behaviour.** If your change affects user-facing behaviour, update the README, inline help (`codex --help`), or relevant example projects. 4. **Keep commits atomic.** Each commit should compile and the tests should pass. This makes reviews and potential rollbacks easier. ### Opening a pull request -- Fill in the PR template (or include similar information) – **What? Why? How?** +- Fill in the PR template (or include similar information) - **What? Why? How?** - Run **all** checks locally (`npm test && npm run lint && npm run typecheck`). CI failures that could have been caught locally slow down the process. -- Make sure your branch is up‑to‑date with `main` and that you have resolved merge conflicts. -- Mark the PR as **Ready for review** only when you believe it is in a merge‑able state. +- Make sure your branch is up-to-date with `main` and that you have resolved merge conflicts. +- Mark the PR as **Ready for review** only when you believe it is in a merge-able state. ### Review process 1. One maintainer will be assigned as a primary reviewer. -2. We may ask for changes – please do not take this personally. We value the work, we just also value consistency and long‑term maintainability. -3. When there is consensus that the PR meets the bar, a maintainer will squash‑and‑merge. +2. We may ask for changes - please do not take this personally. We value the work, we just also value consistency and long-term maintainability. +3. When there is consensus that the PR meets the bar, a maintainer will squash-and-merge. ### Community values - **Be kind and inclusive.** Treat others with respect; we follow the [Contributor Covenant](https://www.contributor-covenant.org/). -- **Assume good intent.** Written communication is hard – err on the side of generosity. +- **Assume good intent.** Written communication is hard - err on the side of generosity. - **Teach & learn.** If you spot something confusing, open an issue or PR with improvements. ### Getting help -If you run into problems setting up the project, would like feedback on an idea, or just want to say _hi_ – please open a Discussion or jump into the relevant issue. We are happy to help. +If you run into problems setting up the project, would like feedback on an idea, or just want to say _hi_ - please open a Discussion or jump into the relevant issue. We are happy to help. Together we can make Codex CLI an incredible tool. **Happy hacking!** :rocket: @@ -512,13 +512,13 @@ Together we can make Codex CLI an incredible tool. **Happy hacking!** :rocket: All contributors **must** accept the CLA. The process is lightweight: 1. Open your pull request. -2. Paste the following comment (or reply `recheck` if you’ve signed before): +2. Paste the following comment (or reply `recheck` if you've signed before): ```text I have read the CLA Document and I hereby sign the CLA ``` -3. The CLA‑Assistant bot records your signature in the repo and marks the status check as passed. +3. The CLA-Assistant bot records your signature in the repo and marks the status check as passed. No special Git commands, email attachments, or commit footers required. @@ -527,7 +527,6 @@ No special Git commands, email attachments, or commit footers required. | Scenario | Command | | ----------------- | ----------------------------------------------------------------------------------------- | | Amend last commit | `git commit --amend -s --no-edit && git push -f` | -| GitHub UI only | Edit the commit message in the PR → add
    `Signed-off-by: Your Name ` | The **DCO check** blocks merges until every commit in the PR carries the footer (with squash this is just the one). @@ -548,12 +547,12 @@ To publish a new version of the CLI, run the release scripts defined in `codex-c --- -## Security & Responsible AI +## Security & Responsible AI -Have you discovered a vulnerability or have concerns about model output? Please e‑mail **security@openai.com** and we will respond promptly. +Have you discovered a vulnerability or have concerns about model output? Please e-mail **security@openai.com** and we will respond promptly. --- ## License -This repository is licensed under the [Apache-2.0 License](LICENSE). +This repository is licensed under the [Apache-2.0 License](LICENSE). diff --git a/scripts/asciicheck.py b/scripts/asciicheck.py new file mode 100755 index 0000000000..812d1c6b69 --- /dev/null +++ b/scripts/asciicheck.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 + +import argparse +import sys +from pathlib import Path + +""" +Utility script that takes a list of files and returns non-zero if any of them +contain non-ASCII characters other than those in the allowed list. + +If --fix is used, it will attempt to replace non-ASCII characters with ASCII +equivalents. + +The motivation behind this script is that characters like U+00A0 (non-breaking +space) can cause regexes not to match and can result in surprising anchor +values for headings when GitHub renders Markdown as HTML. +""" + + +""" +When --fix is used, perform the following substitutions. +""" +substitutions: dict[int, str] = { + 0x00A0: " ", # non-breaking space + 0x2011: "-", # non-breaking hyphen + 0x2013: "-", # en dash + 0x2014: "-", # em dash + 0x2018: "'", # left single quote + 0x2019: "'", # right single quote + 0x201C: '"', # left double quote + 0x201D: '"', # right double quote + 0x2026: "...", # ellipsis + 0x202F: " ", # narrow non-breaking space +} + +""" +Unicode codepoints that are allowed in addition to ASCII. +Be conservative with this list. + +Note that it is always an option to use the hex HTML representation +instead of the character itself so the source code is ASCII-only. +For example, U+2728 (sparkles) can be written as `✨`. +""" +allowed_unicode_codepoints = { + 0x2728, # sparkles +} + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Check for non-ASCII characters in files." + ) + parser.add_argument( + "--fix", + action="store_true", + help="Rewrite files, replacing non-ASCII characters with ASCII equivalents, where possible.", + ) + parser.add_argument( + "files", + nargs="+", + help="Files to check for non-ASCII characters.", + ) + args = parser.parse_args() + + has_errors = False + for filename in args.files: + path = Path(filename) + has_errors |= lint_utf8_ascii(path, fix=args.fix) + return 1 if has_errors else 0 + + +def lint_utf8_ascii(filename: Path, fix: bool) -> bool: + """Returns True if an error was printed.""" + try: + with open(filename, "rb") as f: + raw = f.read() + text = raw.decode("utf-8") + except UnicodeDecodeError as e: + print("UTF-8 decoding error:") + print(f" byte offset: {e.start}") + print(f" reason: {e.reason}") + # Attempt to find line/column + partial = raw[: e.start] + line = partial.count(b"\n") + 1 + col = e.start - (partial.rfind(b"\n") if b"\n" in partial else -1) + print(f" location: line {line}, column {col}") + return True + + errors = [] + for lineno, line in enumerate(text.splitlines(keepends=True), 1): + for colno, char in enumerate(line, 1): + codepoint = ord(char) + if char == "\n": + continue + if ( + not (0x20 <= codepoint <= 0x7E) + and codepoint not in allowed_unicode_codepoints + ): + errors.append((lineno, colno, char, codepoint)) + + if errors: + for lineno, colno, char, codepoint in errors: + safe_char = repr(char)[1:-1] # nicely escape things like \u202f + print( + f"Invalid character at line {lineno}, column {colno}: U+{codepoint:04X} ({safe_char})" + ) + + if errors and fix: + print(f"Attempting to fix {filename}...") + num_replacements = 0 + new_contents = "" + for char in text: + codepoint = ord(char) + if codepoint in substitutions: + num_replacements += 1 + new_contents += substitutions[codepoint] + else: + new_contents += char + with open(filename, "w", encoding="utf-8") as f: + f.write(new_contents) + print(f"Fixed {num_replacements} of {len(errors)} errors in {filename}.") + + return bool(errors) + + +if __name__ == "__main__": + sys.exit(main()) From 835501ad652ceb85a25fe7126ebb3c5c7810207c Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 21 Apr 2025 23:17:11 -0700 Subject: [PATCH 40/41] Enforce ASCII in README.md --- .github/workflows/ci.yml | 3 + README.md | 163 +++++++++++++++++++-------------------- scripts/asciicheck.py | 127 ++++++++++++++++++++++++++++++ 3 files changed, 211 insertions(+), 82 deletions(-) create mode 100755 scripts/asciicheck.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e99c2daa04..1f5dd0d31a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,3 +67,6 @@ jobs: - name: Build run: pnpm run build + + - name: Ensure README.md contains only ASCII and certain Unicode code points + run: ./scripts/asciicheck.py README.md diff --git a/README.md b/README.md index c9fcc93c80..83d5d2e16a 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,13 @@ - [Experimental Technology Disclaimer](#experimental-technology-disclaimer) - [Quickstart](#quickstart) -- [Why Codex?](#whycodex) -- [Security Model \& Permissions](#securitymodelpermissions) +- [Why Codex?](#whycodex) +- [Security Model \& Permissions](#securitymodelpermissions) - [Platform sandboxing details](#platform-sandboxing-details) -- [System Requirements](#systemrequirements) -- [CLI Reference](#clireference) -- [Memory \& Project Docs](#memoryprojectdocs) -- [Non‑interactive / CI mode](#noninteractivecimode) +- [System Requirements](#systemrequirements) +- [CLI Reference](#clireference) +- [Memory \& Project Docs](#memoryprojectdocs) +- [Non-interactive / CI mode](#noninteractivecimode) - [Recipes](#recipes) - [Installation](#installation) - [Configuration](#configuration) @@ -27,7 +27,7 @@ - [Contributing](#contributing) - [Development workflow](#development-workflow) - [Nix Flake Development](#nix-flake-development) - - [Writing high‑impact code changes](#writing-highimpact-code-changes) + - [Writing high-impact code changes](#writing-highimpact-code-changes) - [Opening a pull request](#opening-a-pull-request) - [Review process](#review-process) - [Community values](#community-values) @@ -35,7 +35,7 @@ - [Contributor License Agreement (CLA)](#contributor-license-agreement-cla) - [Quick fixes](#quick-fixes) - [Releasing `codex`](#releasing-codex) -- [Security \& Responsible AI](#securityresponsibleai) +- [Security \& Responsible AI](#securityresponsibleai) - [License](#license) - [Zero Data Retention (ZDR) Organization Limitation](#zero-data-retention-zdr-organization-limitation) @@ -45,7 +45,7 @@ ## Experimental Technology Disclaimer -Codex CLI is an experimental project under active development. It is not yet stable, may contain bugs, incomplete features, or undergo breaking changes. We’re building it in the open with the community and welcome: +Codex CLI is an experimental project under active development. It is not yet stable, may contain bugs, incomplete features, or undergo breaking changes. We're building it in the open with the community and welcome: - Bug reports - Feature requests @@ -115,59 +115,59 @@ codex "explain this codebase to me" codex --approval-mode full-auto "create the fanciest todo-list app" ``` -That’s it – Codex will scaffold a file, run it inside a sandbox, install any +That's it - Codex will scaffold a file, run it inside a sandbox, install any missing dependencies, and show you the live result. Approve the changes and -they’ll be committed to your working directory. +they'll be committed to your working directory. --- -## Why Codex? +## Why Codex? Codex CLI is built for developers who already **live in the terminal** and want -ChatGPT‑level reasoning **plus** the power to actually run code, manipulate -files, and iterate – all under version control. In short, it’s _chat‑driven +ChatGPT-level reasoning **plus** the power to actually run code, manipulate +files, and iterate - all under version control. In short, it's _chat-driven development_ that understands and executes your repo. -- **Zero setup** — bring your OpenAI API key and it just works! +- **Zero setup** - bring your OpenAI API key and it just works! - **Full auto-approval, while safe + secure** by running network-disabled and directory-sandboxed -- **Multimodal** — pass in screenshots or diagrams to implement features ✨ +- **Multimodal** - pass in screenshots or diagrams to implement features ✨ And it's **fully open-source** so you can see and contribute to how it develops! --- -## Security Model & Permissions +## Security Model & Permissions Codex lets you decide _how much autonomy_ the agent receives and auto-approval policy via the `--approval-mode` flag (or the interactive onboarding prompt): -| Mode | What the agent may do without asking | Still requires approval | -| ------------------------- | -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | -| **Suggest**
    (default) | • Read any file in the repo | • **All** file writes/patches
    • **Any** arbitrary shell commands (aside from reading files) | -| **Auto Edit** | • Read **and** apply‑patch writes to files | • **All** shell commands | -| **Full Auto** | • Read/write files
    • Execute shell commands (network disabled, writes limited to your workdir) | – | +| Mode | What the agent may do without asking | Still requires approval | +| ------------------------- | --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | +| **Suggest**
    (default) |
  • Read any file in the repo |
  • **All** file writes/patches
  • **Any** arbitrary shell commands (aside from reading files) | +| **Auto Edit** |
  • Read **and** apply-patch writes to files |
  • **All** shell commands | +| **Full Auto** |
  • Read/write files
  • Execute shell commands (network disabled, writes limited to your workdir) | - | -In **Full Auto** every command is run **network‑disabled** and confined to the -current working directory (plus temporary files) for defense‑in‑depth. Codex -will also show a warning/confirmation if you start in **auto‑edit** or -**full‑auto** while the directory is _not_ tracked by Git, so you always have a +In **Full Auto** every command is run **network-disabled** and confined to the +current working directory (plus temporary files) for defense-in-depth. Codex +will also show a warning/confirmation if you start in **auto-edit** or +**full-auto** while the directory is _not_ tracked by Git, so you always have a safety net. -Coming soon: you’ll be able to whitelist specific commands to auto‑execute with -the network enabled, once we’re confident in additional safeguards. +Coming soon: you'll be able to whitelist specific commands to auto-execute with +the network enabled, once we're confident in additional safeguards. ### Platform sandboxing details The hardening mechanism Codex uses depends on your OS: -- **macOS 12+** – commands are wrapped with **Apple Seatbelt** (`sandbox-exec`). +- **macOS 12+** - commands are wrapped with **Apple Seatbelt** (`sandbox-exec`). - - Everything is placed in a read‑only jail except for a small set of + - Everything is placed in a read-only jail except for a small set of writable roots (`$PWD`, `$TMPDIR`, `~/.codex`, etc.). - - Outbound network is _fully blocked_ by default – even if a child process + - Outbound network is _fully blocked_ by default - even if a child process tries to `curl` somewhere it will fail. -- **Linux** – there is no sandboxing by default. +- **Linux** - there is no sandboxing by default. We recommend using Docker for sandboxing, where Codex launches itself inside a **minimal container image** and mounts your repo _read/write_ at the same path. A custom `iptables`/`ipset` firewall script denies all egress except the @@ -176,47 +176,47 @@ The hardening mechanism Codex uses depends on your OS: --- -## System Requirements +## System Requirements | Requirement | Details | | --------------------------- | --------------------------------------------------------------- | -| Operating systems | macOS 12+, Ubuntu 20.04+/Debian 10+, or Windows 11 **via WSL2** | +| Operating systems | macOS 12+, Ubuntu 20.04+/Debian 10+, or Windows 11 **via WSL2** | | Node.js | **22 or newer** (LTS recommended) | -| Git (optional, recommended) | 2.23+ for built‑in PR helpers | -| RAM | 4‑GB minimum (8‑GB recommended) | +| Git (optional, recommended) | 2.23+ for built-in PR helpers | +| RAM | 4-GB minimum (8-GB recommended) | > Never run `sudo npm install -g`; fix npm permissions instead. --- -## CLI Reference +## CLI Reference | Command | Purpose | Example | | ------------------------------------ | ----------------------------------- | ------------------------------------ | | `codex` | Interactive REPL | `codex` | -| `codex "…"` | Initial prompt for interactive REPL | `codex "fix lint errors"` | -| `codex -q "…"` | Non‑interactive "quiet mode" | `codex -q --json "explain utils.ts"` | +| `codex "..."` | Initial prompt for interactive REPL | `codex "fix lint errors"` | +| `codex -q "..."` | Non-interactive "quiet mode" | `codex -q --json "explain utils.ts"` | | `codex completion ` | Print shell completion script | `codex completion bash` | Key flags: `--model/-m`, `--approval-mode/-a`, `--quiet/-q`, and `--notify`. --- -## Memory & Project Docs +## Memory & Project Docs Codex merges Markdown instructions in this order: -1. `~/.codex/instructions.md` – personal global guidance -2. `codex.md` at repo root – shared project notes -3. `codex.md` in cwd – sub‑package specifics +1. `~/.codex/instructions.md` - personal global guidance +2. `codex.md` at repo root - shared project notes +3. `codex.md` in cwd - sub-package specifics Disable with `--no-project-doc` or `CODEX_DISABLE_PROJECT_DOC=1`. --- -## Non‑interactive / CI mode +## Non-interactive / CI mode -Run Codex head‑less in pipelines. Example GitHub Action step: +Run Codex head-less in pipelines. Example GitHub Action step: ```yaml - name: Update changelog via Codex @@ -240,15 +240,15 @@ DEBUG=true codex ## Recipes -Below are a few bite‑size examples you can copy‑paste. Replace the text in quotes with your own task. See the [prompting guide](https://github.com/openai/codex/blob/main/codex-cli/examples/prompting_guide.md) for more tips and usage patterns. +Below are a few bite-size examples you can copy-paste. Replace the text in quotes with your own task. See the [prompting guide](https://github.com/openai/codex/blob/main/codex-cli/examples/prompting_guide.md) for more tips and usage patterns. | ✨ | What you type | What happens | | --- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | -| 1 | `codex "Refactor the Dashboard component to React Hooks"` | Codex rewrites the class component, runs `npm test`, and shows the diff. | +| 1 | `codex "Refactor the Dashboard component to React Hooks"` | Codex rewrites the class component, runs `npm test`, and shows the diff. | | 2 | `codex "Generate SQL migrations for adding a users table"` | Infers your ORM, creates migration files, and runs them in a sandboxed DB. | | 3 | `codex "Write unit tests for utils/date.ts"` | Generates tests, executes them, and iterates until they pass. | -| 4 | `codex "Bulk‑rename *.jpeg → *.jpg with git mv"` | Safely renames files and updates imports/usages. | -| 5 | `codex "Explain what this regex does: ^(?=.*[A-Z]).{8,}$"` | Outputs a step‑by‑step human explanation. | +| 4 | `codex "Bulk-rename *.jpeg -> *.jpg with git mv"` | Safely renames files and updates imports/usages. | +| 5 | `codex "Explain what this regex does: ^(?=.*[A-Z]).{8,}$"` | Outputs a step-by-step human explanation. | | 6 | `codex "Carefully review this repo, and propose 3 high impact well-scoped PRs"` | Suggests impactful PRs in the current codebase. | | 7 | `codex "Look for vulnerabilities and create a security review report"` | Finds and explains security bugs. | @@ -257,7 +257,7 @@ Below are a few bite‑size examples you can copy‑paste. Replace the text in q ## Installation
    -From npm (Recommended) +From npm (Recommended) ```bash npm install -g @openai/codex @@ -272,7 +272,7 @@ pnpm add -g @openai/codex
    -Build from source +Build from source ```bash # Clone the repository and navigate to the CLI package @@ -289,7 +289,7 @@ pnpm build # Get the usage and the options node ./dist/cli.js --help -# Run the locally‑built CLI directly +# Run the locally-built CLI directly node ./dist/cli.js # Or link the command globally for convenience @@ -363,7 +363,7 @@ Codex runs model-generated commands in a sandbox. If a proposed command or file
    Does it work on Windows? -Not directly. It requires [Windows Subsystem for Linux (WSL2)](https://learn.microsoft.com/en-us/windows/wsl/install) – Codex has been tested on macOS and Linux with Node ≥ 22. +Not directly. It requires [Windows Subsystem for Linux (WSL2)](https://learn.microsoft.com/en-us/windows/wsl/install) - Codex has been tested on macOS and Linux with Node 22.
    @@ -394,12 +394,12 @@ OpenAI rejected the request. Error details: Status: 400, Code: unsupported_param ## Funding Opportunity -We’re excited to launch a **$1 million initiative** supporting open source projects that use Codex CLI and other OpenAI models. +We're excited to launch a **$1 million initiative** supporting open source projects that use Codex CLI and other OpenAI models. - Grants are awarded in **$25,000** API credit increments. - Applications are reviewed **on a rolling basis**. -**Interested? [Apply here](https://openai.com/form/codex-open-source-fund/).** +**Interested? [Apply here](https://openai.com/form/codex-open-source-fund/).** --- @@ -407,14 +407,14 @@ We’re excited to launch a **$1 million initiative** supporting open source pr This project is under active development and the code will likely change pretty significantly. We'll update this message once that's complete! -More broadly we welcome contributions – whether you are opening your very first pull request or you’re a seasoned maintainer. At the same time we care about reliability and long‑term maintainability, so the bar for merging code is intentionally **high**. The guidelines below spell out what “high‑quality” means in practice and should make the whole process transparent and friendly. +More broadly we welcome contributions - whether you are opening your very first pull request or you're a seasoned maintainer. At the same time we care about reliability and long-term maintainability, so the bar for merging code is intentionally **high**. The guidelines below spell out what "high-quality" means in practice and should make the whole process transparent and friendly. ### Development workflow -- Create a _topic branch_ from `main` – e.g. `feat/interactive-prompt`. +- Create a _topic branch_ from `main` - e.g. `feat/interactive-prompt`. - Keep your changes focused. Multiple unrelated fixes should be opened as separate PRs. -- Use `pnpm test:watch` during development for super‑fast feedback. -- We use **Vitest** for unit tests, **ESLint** + **Prettier** for style, and **TypeScript** for type‑checking. +- Use `pnpm test:watch` during development for super-fast feedback. +- We use **Vitest** for unit tests, **ESLint** + **Prettier** for style, and **TypeScript** for type-checking. - Before pushing, run the full test/type/lint suite: ### Git Hooks with Husky @@ -436,16 +436,16 @@ npm test && npm run lint && npm run typecheck I have read the CLA Document and I hereby sign the CLA ``` - The CLA‑Assistant bot will turn the PR status green once all authors have signed. + The CLA-Assistant bot will turn the PR status green once all authors have signed. ```bash -# Watch mode (tests rerun on change) +# Watch mode (tests rerun on change) pnpm test:watch -# Type‑check without emitting files +# Type-check without emitting files pnpm typecheck -# Automatically fix lint + prettier issues +# Automatically fix lint + prettier issues pnpm lint:fix pnpm format:fix ``` @@ -475,35 +475,35 @@ Run the CLI via the flake app: nix run .#codex ``` -### Writing high‑impact code changes +### Writing high-impact code changes 1. **Start with an issue.** Open a new one or comment on an existing discussion so we can agree on the solution before code is written. -2. **Add or update tests.** Every new feature or bug‑fix should come with test coverage that fails before your change and passes afterwards. 100 % coverage is not required, but aim for meaningful assertions. -3. **Document behaviour.** If your change affects user‑facing behaviour, update the README, inline help (`codex --help`), or relevant example projects. +2. **Add or update tests.** Every new feature or bug-fix should come with test coverage that fails before your change and passes afterwards. 100 % coverage is not required, but aim for meaningful assertions. +3. **Document behaviour.** If your change affects user-facing behaviour, update the README, inline help (`codex --help`), or relevant example projects. 4. **Keep commits atomic.** Each commit should compile and the tests should pass. This makes reviews and potential rollbacks easier. ### Opening a pull request -- Fill in the PR template (or include similar information) – **What? Why? How?** +- Fill in the PR template (or include similar information) - **What? Why? How?** - Run **all** checks locally (`npm test && npm run lint && npm run typecheck`). CI failures that could have been caught locally slow down the process. -- Make sure your branch is up‑to‑date with `main` and that you have resolved merge conflicts. -- Mark the PR as **Ready for review** only when you believe it is in a merge‑able state. +- Make sure your branch is up-to-date with `main` and that you have resolved merge conflicts. +- Mark the PR as **Ready for review** only when you believe it is in a merge-able state. ### Review process 1. One maintainer will be assigned as a primary reviewer. -2. We may ask for changes – please do not take this personally. We value the work, we just also value consistency and long‑term maintainability. -3. When there is consensus that the PR meets the bar, a maintainer will squash‑and‑merge. +2. We may ask for changes - please do not take this personally. We value the work, we just also value consistency and long-term maintainability. +3. When there is consensus that the PR meets the bar, a maintainer will squash-and-merge. ### Community values - **Be kind and inclusive.** Treat others with respect; we follow the [Contributor Covenant](https://www.contributor-covenant.org/). -- **Assume good intent.** Written communication is hard – err on the side of generosity. +- **Assume good intent.** Written communication is hard - err on the side of generosity. - **Teach & learn.** If you spot something confusing, open an issue or PR with improvements. ### Getting help -If you run into problems setting up the project, would like feedback on an idea, or just want to say _hi_ – please open a Discussion or jump into the relevant issue. We are happy to help. +If you run into problems setting up the project, would like feedback on an idea, or just want to say _hi_ - please open a Discussion or jump into the relevant issue. We are happy to help. Together we can make Codex CLI an incredible tool. **Happy hacking!** :rocket: @@ -512,22 +512,21 @@ Together we can make Codex CLI an incredible tool. **Happy hacking!** :rocket: All contributors **must** accept the CLA. The process is lightweight: 1. Open your pull request. -2. Paste the following comment (or reply `recheck` if you’ve signed before): +2. Paste the following comment (or reply `recheck` if you've signed before): ```text I have read the CLA Document and I hereby sign the CLA ``` -3. The CLA‑Assistant bot records your signature in the repo and marks the status check as passed. +3. The CLA-Assistant bot records your signature in the repo and marks the status check as passed. No special Git commands, email attachments, or commit footers required. #### Quick fixes -| Scenario | Command | -| ----------------- | ----------------------------------------------------------------------------------------- | -| Amend last commit | `git commit --amend -s --no-edit && git push -f` | -| GitHub UI only | Edit the commit message in the PR → add
    `Signed-off-by: Your Name ` | +| Scenario | Command | +| ----------------- | ------------------------------------------------ | +| Amend last commit | `git commit --amend -s --no-edit && git push -f` | The **DCO check** blocks merges until every commit in the PR carries the footer (with squash this is just the one). @@ -548,12 +547,12 @@ To publish a new version of the CLI, run the release scripts defined in `codex-c --- -## Security & Responsible AI +## Security & Responsible AI -Have you discovered a vulnerability or have concerns about model output? Please e‑mail **security@openai.com** and we will respond promptly. +Have you discovered a vulnerability or have concerns about model output? Please e-mail **security@openai.com** and we will respond promptly. --- ## License -This repository is licensed under the [Apache-2.0 License](LICENSE). +This repository is licensed under the [Apache-2.0 License](LICENSE). diff --git a/scripts/asciicheck.py b/scripts/asciicheck.py new file mode 100755 index 0000000000..812d1c6b69 --- /dev/null +++ b/scripts/asciicheck.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 + +import argparse +import sys +from pathlib import Path + +""" +Utility script that takes a list of files and returns non-zero if any of them +contain non-ASCII characters other than those in the allowed list. + +If --fix is used, it will attempt to replace non-ASCII characters with ASCII +equivalents. + +The motivation behind this script is that characters like U+00A0 (non-breaking +space) can cause regexes not to match and can result in surprising anchor +values for headings when GitHub renders Markdown as HTML. +""" + + +""" +When --fix is used, perform the following substitutions. +""" +substitutions: dict[int, str] = { + 0x00A0: " ", # non-breaking space + 0x2011: "-", # non-breaking hyphen + 0x2013: "-", # en dash + 0x2014: "-", # em dash + 0x2018: "'", # left single quote + 0x2019: "'", # right single quote + 0x201C: '"', # left double quote + 0x201D: '"', # right double quote + 0x2026: "...", # ellipsis + 0x202F: " ", # narrow non-breaking space +} + +""" +Unicode codepoints that are allowed in addition to ASCII. +Be conservative with this list. + +Note that it is always an option to use the hex HTML representation +instead of the character itself so the source code is ASCII-only. +For example, U+2728 (sparkles) can be written as `✨`. +""" +allowed_unicode_codepoints = { + 0x2728, # sparkles +} + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Check for non-ASCII characters in files." + ) + parser.add_argument( + "--fix", + action="store_true", + help="Rewrite files, replacing non-ASCII characters with ASCII equivalents, where possible.", + ) + parser.add_argument( + "files", + nargs="+", + help="Files to check for non-ASCII characters.", + ) + args = parser.parse_args() + + has_errors = False + for filename in args.files: + path = Path(filename) + has_errors |= lint_utf8_ascii(path, fix=args.fix) + return 1 if has_errors else 0 + + +def lint_utf8_ascii(filename: Path, fix: bool) -> bool: + """Returns True if an error was printed.""" + try: + with open(filename, "rb") as f: + raw = f.read() + text = raw.decode("utf-8") + except UnicodeDecodeError as e: + print("UTF-8 decoding error:") + print(f" byte offset: {e.start}") + print(f" reason: {e.reason}") + # Attempt to find line/column + partial = raw[: e.start] + line = partial.count(b"\n") + 1 + col = e.start - (partial.rfind(b"\n") if b"\n" in partial else -1) + print(f" location: line {line}, column {col}") + return True + + errors = [] + for lineno, line in enumerate(text.splitlines(keepends=True), 1): + for colno, char in enumerate(line, 1): + codepoint = ord(char) + if char == "\n": + continue + if ( + not (0x20 <= codepoint <= 0x7E) + and codepoint not in allowed_unicode_codepoints + ): + errors.append((lineno, colno, char, codepoint)) + + if errors: + for lineno, colno, char, codepoint in errors: + safe_char = repr(char)[1:-1] # nicely escape things like \u202f + print( + f"Invalid character at line {lineno}, column {colno}: U+{codepoint:04X} ({safe_char})" + ) + + if errors and fix: + print(f"Attempting to fix {filename}...") + num_replacements = 0 + new_contents = "" + for char in text: + codepoint = ord(char) + if codepoint in substitutions: + num_replacements += 1 + new_contents += substitutions[codepoint] + else: + new_contents += char + with open(filename, "w", encoding="utf-8") as f: + f.write(new_contents) + print(f"Fixed {num_replacements} of {len(errors)} errors in {filename}.") + + return bool(errors) + + +if __name__ == "__main__": + sys.exit(main()) From 66d4d73e24703b3efc0bed7d76b4363ed1b51190 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Mon, 21 Apr 2025 23:34:05 -0700 Subject: [PATCH 41/41] Enforce ASCII in README.md --- .github/workflows/ci.yml | 3 + README.md | 163 +++++++++++++++++++-------------------- scripts/asciicheck.py | 127 ++++++++++++++++++++++++++++++ 3 files changed, 211 insertions(+), 82 deletions(-) create mode 100755 scripts/asciicheck.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e99c2daa04..1f5dd0d31a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,3 +67,6 @@ jobs: - name: Build run: pnpm run build + + - name: Ensure README.md contains only ASCII and certain Unicode code points + run: ./scripts/asciicheck.py README.md diff --git a/README.md b/README.md index c9fcc93c80..00ccead8cf 100644 --- a/README.md +++ b/README.md @@ -12,13 +12,13 @@ - [Experimental Technology Disclaimer](#experimental-technology-disclaimer) - [Quickstart](#quickstart) -- [Why Codex?](#whycodex) -- [Security Model \& Permissions](#securitymodelpermissions) +- [Why Codex?](#whycodex) +- [Security Model \& Permissions](#securitymodelpermissions) - [Platform sandboxing details](#platform-sandboxing-details) -- [System Requirements](#systemrequirements) -- [CLI Reference](#clireference) -- [Memory \& Project Docs](#memoryprojectdocs) -- [Non‑interactive / CI mode](#noninteractivecimode) +- [System Requirements](#systemrequirements) +- [CLI Reference](#clireference) +- [Memory \& Project Docs](#memoryprojectdocs) +- [Non-interactive / CI mode](#noninteractivecimode) - [Recipes](#recipes) - [Installation](#installation) - [Configuration](#configuration) @@ -27,7 +27,7 @@ - [Contributing](#contributing) - [Development workflow](#development-workflow) - [Nix Flake Development](#nix-flake-development) - - [Writing high‑impact code changes](#writing-highimpact-code-changes) + - [Writing high-impact code changes](#writing-highimpact-code-changes) - [Opening a pull request](#opening-a-pull-request) - [Review process](#review-process) - [Community values](#community-values) @@ -35,7 +35,7 @@ - [Contributor License Agreement (CLA)](#contributor-license-agreement-cla) - [Quick fixes](#quick-fixes) - [Releasing `codex`](#releasing-codex) -- [Security \& Responsible AI](#securityresponsibleai) +- [Security \& Responsible AI](#securityresponsibleai) - [License](#license) - [Zero Data Retention (ZDR) Organization Limitation](#zero-data-retention-zdr-organization-limitation) @@ -45,7 +45,7 @@ ## Experimental Technology Disclaimer -Codex CLI is an experimental project under active development. It is not yet stable, may contain bugs, incomplete features, or undergo breaking changes. We’re building it in the open with the community and welcome: +Codex CLI is an experimental project under active development. It is not yet stable, may contain bugs, incomplete features, or undergo breaking changes. We're building it in the open with the community and welcome: - Bug reports - Feature requests @@ -115,59 +115,59 @@ codex "explain this codebase to me" codex --approval-mode full-auto "create the fanciest todo-list app" ``` -That’s it – Codex will scaffold a file, run it inside a sandbox, install any +That's it - Codex will scaffold a file, run it inside a sandbox, install any missing dependencies, and show you the live result. Approve the changes and -they’ll be committed to your working directory. +they'll be committed to your working directory. --- -## Why Codex? +## Why Codex? Codex CLI is built for developers who already **live in the terminal** and want -ChatGPT‑level reasoning **plus** the power to actually run code, manipulate -files, and iterate – all under version control. In short, it’s _chat‑driven +ChatGPT-level reasoning **plus** the power to actually run code, manipulate +files, and iterate - all under version control. In short, it's _chat-driven development_ that understands and executes your repo. -- **Zero setup** — bring your OpenAI API key and it just works! +- **Zero setup** - bring your OpenAI API key and it just works! - **Full auto-approval, while safe + secure** by running network-disabled and directory-sandboxed -- **Multimodal** — pass in screenshots or diagrams to implement features ✨ +- **Multimodal** - pass in screenshots or diagrams to implement features ✨ And it's **fully open-source** so you can see and contribute to how it develops! --- -## Security Model & Permissions +## Security Model & Permissions Codex lets you decide _how much autonomy_ the agent receives and auto-approval policy via the `--approval-mode` flag (or the interactive onboarding prompt): -| Mode | What the agent may do without asking | Still requires approval | -| ------------------------- | -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | -| **Suggest**
    (default) | • Read any file in the repo | • **All** file writes/patches
    • **Any** arbitrary shell commands (aside from reading files) | -| **Auto Edit** | • Read **and** apply‑patch writes to files | • **All** shell commands | -| **Full Auto** | • Read/write files
    • Execute shell commands (network disabled, writes limited to your workdir) | – | +| Mode | What the agent may do without asking | Still requires approval | +| ------------------------- | --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | +| **Suggest**
    (default) |
  • Read any file in the repo |
  • **All** file writes/patches
  • **Any** arbitrary shell commands (aside from reading files) | +| **Auto Edit** |
  • Read **and** apply-patch writes to files |
  • **All** shell commands | +| **Full Auto** |
  • Read/write files
  • Execute shell commands (network disabled, writes limited to your workdir) | - | -In **Full Auto** every command is run **network‑disabled** and confined to the -current working directory (plus temporary files) for defense‑in‑depth. Codex -will also show a warning/confirmation if you start in **auto‑edit** or -**full‑auto** while the directory is _not_ tracked by Git, so you always have a +In **Full Auto** every command is run **network-disabled** and confined to the +current working directory (plus temporary files) for defense-in-depth. Codex +will also show a warning/confirmation if you start in **auto-edit** or +**full-auto** while the directory is _not_ tracked by Git, so you always have a safety net. -Coming soon: you’ll be able to whitelist specific commands to auto‑execute with -the network enabled, once we’re confident in additional safeguards. +Coming soon: you'll be able to whitelist specific commands to auto-execute with +the network enabled, once we're confident in additional safeguards. ### Platform sandboxing details The hardening mechanism Codex uses depends on your OS: -- **macOS 12+** – commands are wrapped with **Apple Seatbelt** (`sandbox-exec`). +- **macOS 12+** - commands are wrapped with **Apple Seatbelt** (`sandbox-exec`). - - Everything is placed in a read‑only jail except for a small set of + - Everything is placed in a read-only jail except for a small set of writable roots (`$PWD`, `$TMPDIR`, `~/.codex`, etc.). - - Outbound network is _fully blocked_ by default – even if a child process + - Outbound network is _fully blocked_ by default - even if a child process tries to `curl` somewhere it will fail. -- **Linux** – there is no sandboxing by default. +- **Linux** - there is no sandboxing by default. We recommend using Docker for sandboxing, where Codex launches itself inside a **minimal container image** and mounts your repo _read/write_ at the same path. A custom `iptables`/`ipset` firewall script denies all egress except the @@ -176,47 +176,47 @@ The hardening mechanism Codex uses depends on your OS: --- -## System Requirements +## System Requirements | Requirement | Details | | --------------------------- | --------------------------------------------------------------- | -| Operating systems | macOS 12+, Ubuntu 20.04+/Debian 10+, or Windows 11 **via WSL2** | +| Operating systems | macOS 12+, Ubuntu 20.04+/Debian 10+, or Windows 11 **via WSL2** | | Node.js | **22 or newer** (LTS recommended) | -| Git (optional, recommended) | 2.23+ for built‑in PR helpers | -| RAM | 4‑GB minimum (8‑GB recommended) | +| Git (optional, recommended) | 2.23+ for built-in PR helpers | +| RAM | 4-GB minimum (8-GB recommended) | > Never run `sudo npm install -g`; fix npm permissions instead. --- -## CLI Reference +## CLI Reference | Command | Purpose | Example | | ------------------------------------ | ----------------------------------- | ------------------------------------ | | `codex` | Interactive REPL | `codex` | -| `codex "…"` | Initial prompt for interactive REPL | `codex "fix lint errors"` | -| `codex -q "…"` | Non‑interactive "quiet mode" | `codex -q --json "explain utils.ts"` | +| `codex "..."` | Initial prompt for interactive REPL | `codex "fix lint errors"` | +| `codex -q "..."` | Non-interactive "quiet mode" | `codex -q --json "explain utils.ts"` | | `codex completion ` | Print shell completion script | `codex completion bash` | Key flags: `--model/-m`, `--approval-mode/-a`, `--quiet/-q`, and `--notify`. --- -## Memory & Project Docs +## Memory & Project Docs Codex merges Markdown instructions in this order: -1. `~/.codex/instructions.md` – personal global guidance -2. `codex.md` at repo root – shared project notes -3. `codex.md` in cwd – sub‑package specifics +1. `~/.codex/instructions.md` - personal global guidance +2. `codex.md` at repo root - shared project notes +3. `codex.md` in cwd - sub-package specifics Disable with `--no-project-doc` or `CODEX_DISABLE_PROJECT_DOC=1`. --- -## Non‑interactive / CI mode +## Non-interactive / CI mode -Run Codex head‑less in pipelines. Example GitHub Action step: +Run Codex head-less in pipelines. Example GitHub Action step: ```yaml - name: Update changelog via Codex @@ -240,15 +240,15 @@ DEBUG=true codex ## Recipes -Below are a few bite‑size examples you can copy‑paste. Replace the text in quotes with your own task. See the [prompting guide](https://github.com/openai/codex/blob/main/codex-cli/examples/prompting_guide.md) for more tips and usage patterns. +Below are a few bite-size examples you can copy-paste. Replace the text in quotes with your own task. See the [prompting guide](https://github.com/openai/codex/blob/main/codex-cli/examples/prompting_guide.md) for more tips and usage patterns. | ✨ | What you type | What happens | | --- | ------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | -| 1 | `codex "Refactor the Dashboard component to React Hooks"` | Codex rewrites the class component, runs `npm test`, and shows the diff. | +| 1 | `codex "Refactor the Dashboard component to React Hooks"` | Codex rewrites the class component, runs `npm test`, and shows the diff. | | 2 | `codex "Generate SQL migrations for adding a users table"` | Infers your ORM, creates migration files, and runs them in a sandboxed DB. | | 3 | `codex "Write unit tests for utils/date.ts"` | Generates tests, executes them, and iterates until they pass. | -| 4 | `codex "Bulk‑rename *.jpeg → *.jpg with git mv"` | Safely renames files and updates imports/usages. | -| 5 | `codex "Explain what this regex does: ^(?=.*[A-Z]).{8,}$"` | Outputs a step‑by‑step human explanation. | +| 4 | `codex "Bulk-rename *.jpeg -> *.jpg with git mv"` | Safely renames files and updates imports/usages. | +| 5 | `codex "Explain what this regex does: ^(?=.*[A-Z]).{8,}$"` | Outputs a step-by-step human explanation. | | 6 | `codex "Carefully review this repo, and propose 3 high impact well-scoped PRs"` | Suggests impactful PRs in the current codebase. | | 7 | `codex "Look for vulnerabilities and create a security review report"` | Finds and explains security bugs. | @@ -257,7 +257,7 @@ Below are a few bite‑size examples you can copy‑paste. Replace the text in q ## Installation
    -From npm (Recommended) +From npm (Recommended) ```bash npm install -g @openai/codex @@ -272,7 +272,7 @@ pnpm add -g @openai/codex
    -Build from source +Build from source ```bash # Clone the repository and navigate to the CLI package @@ -289,7 +289,7 @@ pnpm build # Get the usage and the options node ./dist/cli.js --help -# Run the locally‑built CLI directly +# Run the locally-built CLI directly node ./dist/cli.js # Or link the command globally for convenience @@ -363,7 +363,7 @@ Codex runs model-generated commands in a sandbox. If a proposed command or file
    Does it work on Windows? -Not directly. It requires [Windows Subsystem for Linux (WSL2)](https://learn.microsoft.com/en-us/windows/wsl/install) – Codex has been tested on macOS and Linux with Node ≥ 22. +Not directly. It requires [Windows Subsystem for Linux (WSL2)](https://learn.microsoft.com/en-us/windows/wsl/install) - Codex has been tested on macOS and Linux with Node 22.
    @@ -394,12 +394,12 @@ OpenAI rejected the request. Error details: Status: 400, Code: unsupported_param ## Funding Opportunity -We’re excited to launch a **$1 million initiative** supporting open source projects that use Codex CLI and other OpenAI models. +We're excited to launch a **$1 million initiative** supporting open source projects that use Codex CLI and other OpenAI models. - Grants are awarded in **$25,000** API credit increments. - Applications are reviewed **on a rolling basis**. -**Interested? [Apply here](https://openai.com/form/codex-open-source-fund/).** +**Interested? [Apply here](https://openai.com/form/codex-open-source-fund/).** --- @@ -407,14 +407,14 @@ We’re excited to launch a **$1 million initiative** supporting open source pr This project is under active development and the code will likely change pretty significantly. We'll update this message once that's complete! -More broadly we welcome contributions – whether you are opening your very first pull request or you’re a seasoned maintainer. At the same time we care about reliability and long‑term maintainability, so the bar for merging code is intentionally **high**. The guidelines below spell out what “high‑quality” means in practice and should make the whole process transparent and friendly. +More broadly we welcome contributions - whether you are opening your very first pull request or you're a seasoned maintainer. At the same time we care about reliability and long-term maintainability, so the bar for merging code is intentionally **high**. The guidelines below spell out what "high-quality" means in practice and should make the whole process transparent and friendly. ### Development workflow -- Create a _topic branch_ from `main` – e.g. `feat/interactive-prompt`. +- Create a _topic branch_ from `main` - e.g. `feat/interactive-prompt`. - Keep your changes focused. Multiple unrelated fixes should be opened as separate PRs. -- Use `pnpm test:watch` during development for super‑fast feedback. -- We use **Vitest** for unit tests, **ESLint** + **Prettier** for style, and **TypeScript** for type‑checking. +- Use `pnpm test:watch` during development for super-fast feedback. +- We use **Vitest** for unit tests, **ESLint** + **Prettier** for style, and **TypeScript** for type-checking. - Before pushing, run the full test/type/lint suite: ### Git Hooks with Husky @@ -436,16 +436,16 @@ npm test && npm run lint && npm run typecheck I have read the CLA Document and I hereby sign the CLA ``` - The CLA‑Assistant bot will turn the PR status green once all authors have signed. + The CLA-Assistant bot will turn the PR status green once all authors have signed. ```bash -# Watch mode (tests rerun on change) +# Watch mode (tests rerun on change) pnpm test:watch -# Type‑check without emitting files +# Type-check without emitting files pnpm typecheck -# Automatically fix lint + prettier issues +# Automatically fix lint + prettier issues pnpm lint:fix pnpm format:fix ``` @@ -475,35 +475,35 @@ Run the CLI via the flake app: nix run .#codex ``` -### Writing high‑impact code changes +### Writing high-impact code changes 1. **Start with an issue.** Open a new one or comment on an existing discussion so we can agree on the solution before code is written. -2. **Add or update tests.** Every new feature or bug‑fix should come with test coverage that fails before your change and passes afterwards. 100 % coverage is not required, but aim for meaningful assertions. -3. **Document behaviour.** If your change affects user‑facing behaviour, update the README, inline help (`codex --help`), or relevant example projects. +2. **Add or update tests.** Every new feature or bug-fix should come with test coverage that fails before your change and passes afterwards. 100% coverage is not required, but aim for meaningful assertions. +3. **Document behaviour.** If your change affects user-facing behaviour, update the README, inline help (`codex --help`), or relevant example projects. 4. **Keep commits atomic.** Each commit should compile and the tests should pass. This makes reviews and potential rollbacks easier. ### Opening a pull request -- Fill in the PR template (or include similar information) – **What? Why? How?** +- Fill in the PR template (or include similar information) - **What? Why? How?** - Run **all** checks locally (`npm test && npm run lint && npm run typecheck`). CI failures that could have been caught locally slow down the process. -- Make sure your branch is up‑to‑date with `main` and that you have resolved merge conflicts. -- Mark the PR as **Ready for review** only when you believe it is in a merge‑able state. +- Make sure your branch is up-to-date with `main` and that you have resolved merge conflicts. +- Mark the PR as **Ready for review** only when you believe it is in a merge-able state. ### Review process 1. One maintainer will be assigned as a primary reviewer. -2. We may ask for changes – please do not take this personally. We value the work, we just also value consistency and long‑term maintainability. -3. When there is consensus that the PR meets the bar, a maintainer will squash‑and‑merge. +2. We may ask for changes - please do not take this personally. We value the work, we just also value consistency and long-term maintainability. +3. When there is consensus that the PR meets the bar, a maintainer will squash-and-merge. ### Community values - **Be kind and inclusive.** Treat others with respect; we follow the [Contributor Covenant](https://www.contributor-covenant.org/). -- **Assume good intent.** Written communication is hard – err on the side of generosity. +- **Assume good intent.** Written communication is hard - err on the side of generosity. - **Teach & learn.** If you spot something confusing, open an issue or PR with improvements. ### Getting help -If you run into problems setting up the project, would like feedback on an idea, or just want to say _hi_ – please open a Discussion or jump into the relevant issue. We are happy to help. +If you run into problems setting up the project, would like feedback on an idea, or just want to say _hi_ - please open a Discussion or jump into the relevant issue. We are happy to help. Together we can make Codex CLI an incredible tool. **Happy hacking!** :rocket: @@ -512,22 +512,21 @@ Together we can make Codex CLI an incredible tool. **Happy hacking!** :rocket: All contributors **must** accept the CLA. The process is lightweight: 1. Open your pull request. -2. Paste the following comment (or reply `recheck` if you’ve signed before): +2. Paste the following comment (or reply `recheck` if you've signed before): ```text I have read the CLA Document and I hereby sign the CLA ``` -3. The CLA‑Assistant bot records your signature in the repo and marks the status check as passed. +3. The CLA-Assistant bot records your signature in the repo and marks the status check as passed. No special Git commands, email attachments, or commit footers required. #### Quick fixes -| Scenario | Command | -| ----------------- | ----------------------------------------------------------------------------------------- | -| Amend last commit | `git commit --amend -s --no-edit && git push -f` | -| GitHub UI only | Edit the commit message in the PR → add
    `Signed-off-by: Your Name ` | +| Scenario | Command | +| ----------------- | ------------------------------------------------ | +| Amend last commit | `git commit --amend -s --no-edit && git push -f` | The **DCO check** blocks merges until every commit in the PR carries the footer (with squash this is just the one). @@ -548,12 +547,12 @@ To publish a new version of the CLI, run the release scripts defined in `codex-c --- -## Security & Responsible AI +## Security & Responsible AI -Have you discovered a vulnerability or have concerns about model output? Please e‑mail **security@openai.com** and we will respond promptly. +Have you discovered a vulnerability or have concerns about model output? Please e-mail **security@openai.com** and we will respond promptly. --- ## License -This repository is licensed under the [Apache-2.0 License](LICENSE). +This repository is licensed under the [Apache-2.0 License](LICENSE). diff --git a/scripts/asciicheck.py b/scripts/asciicheck.py new file mode 100755 index 0000000000..812d1c6b69 --- /dev/null +++ b/scripts/asciicheck.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 + +import argparse +import sys +from pathlib import Path + +""" +Utility script that takes a list of files and returns non-zero if any of them +contain non-ASCII characters other than those in the allowed list. + +If --fix is used, it will attempt to replace non-ASCII characters with ASCII +equivalents. + +The motivation behind this script is that characters like U+00A0 (non-breaking +space) can cause regexes not to match and can result in surprising anchor +values for headings when GitHub renders Markdown as HTML. +""" + + +""" +When --fix is used, perform the following substitutions. +""" +substitutions: dict[int, str] = { + 0x00A0: " ", # non-breaking space + 0x2011: "-", # non-breaking hyphen + 0x2013: "-", # en dash + 0x2014: "-", # em dash + 0x2018: "'", # left single quote + 0x2019: "'", # right single quote + 0x201C: '"', # left double quote + 0x201D: '"', # right double quote + 0x2026: "...", # ellipsis + 0x202F: " ", # narrow non-breaking space +} + +""" +Unicode codepoints that are allowed in addition to ASCII. +Be conservative with this list. + +Note that it is always an option to use the hex HTML representation +instead of the character itself so the source code is ASCII-only. +For example, U+2728 (sparkles) can be written as `✨`. +""" +allowed_unicode_codepoints = { + 0x2728, # sparkles +} + + +def main() -> int: + parser = argparse.ArgumentParser( + description="Check for non-ASCII characters in files." + ) + parser.add_argument( + "--fix", + action="store_true", + help="Rewrite files, replacing non-ASCII characters with ASCII equivalents, where possible.", + ) + parser.add_argument( + "files", + nargs="+", + help="Files to check for non-ASCII characters.", + ) + args = parser.parse_args() + + has_errors = False + for filename in args.files: + path = Path(filename) + has_errors |= lint_utf8_ascii(path, fix=args.fix) + return 1 if has_errors else 0 + + +def lint_utf8_ascii(filename: Path, fix: bool) -> bool: + """Returns True if an error was printed.""" + try: + with open(filename, "rb") as f: + raw = f.read() + text = raw.decode("utf-8") + except UnicodeDecodeError as e: + print("UTF-8 decoding error:") + print(f" byte offset: {e.start}") + print(f" reason: {e.reason}") + # Attempt to find line/column + partial = raw[: e.start] + line = partial.count(b"\n") + 1 + col = e.start - (partial.rfind(b"\n") if b"\n" in partial else -1) + print(f" location: line {line}, column {col}") + return True + + errors = [] + for lineno, line in enumerate(text.splitlines(keepends=True), 1): + for colno, char in enumerate(line, 1): + codepoint = ord(char) + if char == "\n": + continue + if ( + not (0x20 <= codepoint <= 0x7E) + and codepoint not in allowed_unicode_codepoints + ): + errors.append((lineno, colno, char, codepoint)) + + if errors: + for lineno, colno, char, codepoint in errors: + safe_char = repr(char)[1:-1] # nicely escape things like \u202f + print( + f"Invalid character at line {lineno}, column {colno}: U+{codepoint:04X} ({safe_char})" + ) + + if errors and fix: + print(f"Attempting to fix {filename}...") + num_replacements = 0 + new_contents = "" + for char in text: + codepoint = ord(char) + if codepoint in substitutions: + num_replacements += 1 + new_contents += substitutions[codepoint] + else: + new_contents += char + with open(filename, "w", encoding="utf-8") as f: + f.write(new_contents) + print(f"Fixed {num_replacements} of {len(errors)} errors in {filename}.") + + return bool(errors) + + +if __name__ == "__main__": + sys.exit(main())