From 7d21ebea4ae556b61a44c99781f70ca09a51aa01 Mon Sep 17 00:00:00 2001 From: Fouad Matin <169186268+fouad-openai@users.noreply.github.com> Date: Wed, 16 Apr 2025 15:11:02 -0700 Subject: [PATCH 1/6] bump(version): 0.1.04161352 (#125) Signed-off-by: Fouad Matin --- codex-cli/package.json | 4 ++-- codex-cli/src/utils/session.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/codex-cli/package.json b/codex-cli/package.json index b9f0d9e157..38850e6d72 100644 --- a/codex-cli/package.json +++ b/codex-cli/package.json @@ -1,6 +1,6 @@ { "name": "@openai/codex", - "version": "0.1.04161241", + "version": "0.1.04161352", "license": "Apache-2.0", "bin": { "codex": "dist/cli.js" @@ -22,7 +22,7 @@ "build:dev": "NODE_ENV=development node build.mjs --dev && NODE_OPTIONS=--enable-source-maps node dist/cli-dev.js", "release:readme": "cp ../README.md ./README.md", "release:version": "codex -a full-auto 'update the CLI_VERSION in codex-cli/src/utils/session.ts and the version in package.json to use the current timestamp (MMDDHHmm format)'", - "release": "npm run release:readme && npm run build && npm publish" + "release": "npm run release:readme && npm run release:version && npm run build && npm publish" }, "files": [ "README.md", diff --git a/codex-cli/src/utils/session.ts b/codex-cli/src/utils/session.ts index 287d0d7ed6..3c96b9c1a8 100644 --- a/codex-cli/src/utils/session.ts +++ b/codex-cli/src/utils/session.ts @@ -1,4 +1,4 @@ -export const CLI_VERSION = "0.1.04161241"; // Must be in sync with package.json. +export const CLI_VERSION = "0.1.04161352"; // Must be in sync with package.json. export const ORIGIN = "codex_cli_ts"; export type TerminalChatSession = { From 50925c0d6cf0e1d0a6f660c1b754fca742052641 Mon Sep 17 00:00:00 2001 From: Ilan Bigio Date: Wed, 16 Apr 2025 18:24:31 -0400 Subject: [PATCH 2/6] Set up CLA process and remove DCO (#129) Signed-off-by: Ilan Bigio --- .github/workflows/cla.yml | 29 ++++++++++++++++++++++ .github/workflows/dco.yml | 43 -------------------------------- README.md | 52 ++++++++++++++++++++++----------------- docs/CLA.md | 44 +++++++++++++++++++++++++++++++++ 4 files changed, 102 insertions(+), 66 deletions(-) create mode 100644 .github/workflows/cla.yml delete mode 100644 .github/workflows/dco.yml create mode 100644 docs/CLA.md diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml new file mode 100644 index 0000000000..a7f6c62865 --- /dev/null +++ b/.github/workflows/cla.yml @@ -0,0 +1,29 @@ +name: CLA Assistant +on: + issue_comment: + types: [created] + pull_request_target: + types: [opened, closed, synchronize] + +permissions: + actions: write + contents: write + pull-requests: write + statuses: write + +jobs: + cla: + runs-on: ubuntu-latest + steps: + - uses: contributor-assistant/github-action@v2.6.1 + if: | + github.event_name == 'pull_request_target' || + github.event.comment.body == 'recheck' || + github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + path-to-document: docs/CLA.md + path-to-signatures: signatures/cla.json + branch: cla-signatures + allowlist: dependabot[bot] diff --git a/.github/workflows/dco.yml b/.github/workflows/dco.yml deleted file mode 100644 index 7ed7ba5acf..0000000000 --- a/.github/workflows/dco.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: dco -on: [pull_request] - -jobs: - check: - runs-on: ubuntu-latest - permissions: - contents: read # minimum needed - - steps: - - uses: actions/checkout@v4 - with: { fetch-depth: 0 } # make sure base SHA exists - - - name: Verify Signed‑off‑by lines - shell: bash - run: | - base="${{ github.event.pull_request.base.sha }}" - head="${{ github.event.pull_request.head.sha }}" - - unsigned=$(git log --format='%h %s' "$base..$head" | while read sha _; do - git show -s --format='%B' "$sha" | grep -qi '^Signed-off-by:' || echo "$sha" - done) - - if [ -n "$unsigned" ]; then - echo "::error ::❌ DCO check failed." - echo "" - echo "Commits missing the 'Signed-off-by:' footer:" - echo "$unsigned" - echo "" - echo "🛠 **Quick fix (make ONE signed commit):**" - echo " git fetch origin" - echo " git reset --soft origin/${GITHUB_BASE_REF}" - echo " git commit -s -m \"\"" - echo " git push --force-with-lease" - echo "" - echo "🔧 **Fix individual commits:**" - echo " git rebase -i origin/${GITHUB_BASE_REF} --exec \"git commit --amend -s --no-edit\"" - echo " git push --force-with-lease" - echo "" - echo "💡 Or edit the commit message in GitHub UI and add:" - echo " Signed-off-by: Your Name " - exit 1 - fi diff --git a/README.md b/README.md index 289fdf9524..cfcabb2414 100644 --- a/README.md +++ b/README.md @@ -294,7 +294,7 @@ Any model available with [Responses API](https://platform.openai.com/docs/api-re 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 @@ -302,7 +302,19 @@ More broadly We welcome contributions – whether you are opening your very firs - Keep your changes focused. Multiple unrelated fixes should be opened as separate PRs. - Use `npm run test:watch` during development for super‑fast feedback. - We use **Vitest** for unit tests, **ESLint** + **Prettier** for style, and **TypeScript** for type‑checking. -- Make sure all your commits are signed off with `git commit -s ...`, see [Developer Certificate of Origin (DCO)](#developer-certificate-of-origin-dco) for more details. +- Before pushing, run the full test/type/lint suite: + + ```bash + npm test && npm run lint && npm run typecheck + ``` + +- If you have **not** yet signed the Contributor License Agreement (CLA), add a PR comment containing the exact text + + ```text + 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. ```bash # Watch mode (tests rerun on change) @@ -318,20 +330,15 @@ npm run format:fix ### 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. -4. **Keep commits atomic.** - Each commit should compile and the tests should pass. This makes reviews and potential rollbacks easier. +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. +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?** -- Run **all** checks locally (`npm test && npm run lint && npm run typecheck`). - CI failures that could have been caught locally slow down the process. +- 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. @@ -353,21 +360,20 @@ If you run into problems setting up the project, would like feedback on an idea, Together we can make Codex CLI an incredible tool. **Happy hacking!** :rocket: -### Developer Certificate of Origin (DCO) +### Contributor License Agreement (CLA) -All commits **must** include a `Signed‑off‑by:` footer. -This one‑line self‑certification tells us you wrote the code and can contribute it under the repo’s license. +All contributors **must** accept the CLA. The process is lightweight: -#### How to sign (recommended flow) +1. Open your pull request. +2. Paste the following comment (or reply `recheck` if you’ve signed before): -```bash -# squash your work into ONE signed commit -git reset --soft origin/main # stage all changes -git commit -s -m "Your concise message" -git push --force-with-lease # updates the PR -``` + ```text + I have read the CLA Document and I hereby sign the CLA + ``` -> We enforce **squash‑and‑merge only**, so a single signed commit is enough for the whole PR. +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 diff --git a/docs/CLA.md b/docs/CLA.md new file mode 100644 index 0000000000..f045598813 --- /dev/null +++ b/docs/CLA.md @@ -0,0 +1,44 @@ +# Individual Contributor License Agreement (v1.0, OpenAI) + +_Based on the Apache Software Foundation Individual CLA v 2.2._ + +By commenting **“I have read the CLA Document and I hereby sign the CLA”** +on a Pull Request, **you (“Contributor”) agree to the following terms** for any +past and future “Contributions” submitted to the **OpenAI Codex CLI project +(the “Project”)**. + +--- + +## 1. Definitions +- **“Contribution”** – any original work of authorship submitted to the Project + (code, documentation, designs, etc.). +- **“You” / “Your”** – the individual (or legal entity) posting the acceptance + comment. + +## 2. Copyright License +You grant **OpenAI, Inc.** and all recipients of software distributed by the +Project a perpetual, worldwide, non‑exclusive, royalty‑free, irrevocable +license to reproduce, prepare derivative works of, publicly display, publicly +perform, sublicense, and distribute Your Contributions and derivative works. + +## 3. Patent License +You grant **OpenAI, Inc.** and all recipients of the Project a perpetual, +worldwide, non‑exclusive, royalty‑free, irrevocable (except as below) patent +license to make, have made, use, sell, offer to sell, import, and otherwise +transfer Your Contributions alone or in combination with the Project. + +If any entity brings patent litigation alleging that the Project or a +Contribution infringes a patent, the patent licenses granted by You to that +entity under this CLA terminate. + +## 4. Representations +1. You are legally entitled to grant the licenses above. +2. Each Contribution is either Your original creation or You have authority to + submit it under this CLA. +3. Your Contributions are provided **“AS IS”** without warranties of any kind. +4. You will notify the Project if any statement above becomes inaccurate. + +## 5. Miscellany +This Agreement is governed by the laws of the **State of California**, USA, +excluding its conflict‑of‑laws rules. If any provision is held unenforceable, +the remaining provisions remain in force. From 05c458b51e8175f595b307a120c751b28c865004 Mon Sep 17 00:00:00 2001 From: Jon Church Date: Wed, 16 Apr 2025 18:25:32 -0400 Subject: [PATCH 3/6] fix: silence deprecation warnings without NODE_OPTIONS (#80) Signed-off-by: Jon Church --- codex-cli/src/cli.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/codex-cli/src/cli.tsx b/codex-cli/src/cli.tsx index 0af421a431..df72136e0c 100644 --- a/codex-cli/src/cli.tsx +++ b/codex-cli/src/cli.tsx @@ -1,5 +1,9 @@ #!/usr/bin/env node +// Hack to suppress deprecation warnings (punycode) +// eslint-disable-next-line @typescript-eslint/no-explicit-any +(process as any).noDeprecation = true; + import type { AppRollout } from "./app"; import type { ApprovalPolicy } from "./approvals"; import type { CommandConfirmation } from "./utils/agent/agent-loop"; From 40266beebe55d5bb93af3fbc15269bf9e96f3911 Mon Sep 17 00:00:00 2001 From: Aron Jones Date: Wed, 16 Apr 2025 18:33:12 -0400 Subject: [PATCH 4/6] dotenv support (#122) Signed-off-by: Aron Jones --- codex-cli/package-lock.json | 12 ++++++++++++ codex-cli/package.json | 1 + codex-cli/src/cli.tsx | 1 + 3 files changed, 14 insertions(+) diff --git a/codex-cli/package-lock.json b/codex-cli/package-lock.json index 7431719286..eb398389d7 100644 --- a/codex-cli/package-lock.json +++ b/codex-cli/package-lock.json @@ -12,6 +12,7 @@ "@inkjs/ui": "^2.0.0", "chalk": "^5.2.0", "diff": "^7.0.0", + "dotenv": "^16.1.4", "file-type": "^20.1.0", "ink": "^5.2.0", "ink-select-input": "^6.0.0", @@ -2435,6 +2436,17 @@ "node": ">=6.0.0" } }, + "node_modules/dotenv": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", diff --git a/codex-cli/package.json b/codex-cli/package.json index 38850e6d72..728485b744 100644 --- a/codex-cli/package.json +++ b/codex-cli/package.json @@ -33,6 +33,7 @@ "@inkjs/ui": "^2.0.0", "chalk": "^5.2.0", "diff": "^7.0.0", + "dotenv": "^16.1.4", "file-type": "^20.1.0", "ink": "^5.2.0", "ink-select-input": "^6.0.0", diff --git a/codex-cli/src/cli.tsx b/codex-cli/src/cli.tsx index df72136e0c..d4b661f641 100644 --- a/codex-cli/src/cli.tsx +++ b/codex-cli/src/cli.tsx @@ -1,4 +1,5 @@ #!/usr/bin/env node +import "dotenv/config"; // Hack to suppress deprecation warnings (punycode) // eslint-disable-next-line @typescript-eslint/no-explicit-any From 72d0230b332a1e0cb4d893cedbf4350559b9cb38 Mon Sep 17 00:00:00 2001 From: Ilan Bigio Date: Wed, 16 Apr 2025 18:33:42 -0400 Subject: [PATCH 5/6] Change bash to shell in README (#132) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cfcabb2414..3ee12fbf47 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ npm install -g @openai/codex Next, set your OpenAI API key as an environment variable: -```bash +```shell export OPENAI_API_KEY="your-api-key-here" ``` From 08d209f8fa49242bfa7979b3861ff6302550b6f3 Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Wed, 16 Apr 2025 15:36:25 -0700 Subject: [PATCH 6/6] 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; -}