Skip to content

Commit 3945241

Browse files
Merge branch 'main' into feat/watch-mode
2 parents 5200bb4 + 750d97e commit 3945241

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+1977
-602
lines changed

.github/workflows/ci.yml

+5
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,8 @@ jobs:
6767

6868
- name: Build
6969
run: pnpm run build
70+
71+
- name: Ensure README.md contains only ASCII and certain Unicode code points
72+
run: ./scripts/asciicheck.py README.md
73+
- name: Check README ToC
74+
run: python3 scripts/readme_toc.py README.md

CHANGELOG.md

+55
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,61 @@
22

33
You can install any of these versions: `npm install -g codex@version`
44

5+
## `0.1.2504221401`
6+
7+
### 🚀 Features
8+
9+
- Show actionable errors when api keys are missing (#523)
10+
- Add CLI `--version` flag (#492)
11+
12+
### 🐛 Bug Fixes
13+
14+
- Agent loop for ZDR (`disableResponseStorage`) (#543)
15+
- Fix relative `workdir` check for `apply_patch` (#556)
16+
- Minimal mid-stream #429 retry loop using existing back-off (#506)
17+
- Inconsistent usage of base URL and API key (#507)
18+
- Remove requirement for api key for ollama (#546)
19+
- Support `[provider]_BASE_URL` (#542)
20+
21+
## `0.1.2504220136`
22+
23+
### 🚀 Features
24+
25+
- Add support for ZDR orgs (#481)
26+
- Include fractional portion of chunk that exceeds stdout/stderr limit (#497)
27+
28+
## `0.1.2504211509`
29+
30+
### 🚀 Features
31+
32+
- Support multiple providers via Responses-Completion transformation (#247)
33+
- Add user-defined safe commands configuration and approval logic #380 (#386)
34+
- Allow switching approval modes when prompted to approve an edit/command (#400)
35+
- Add support for `/diff` command autocomplete in TerminalChatInput (#431)
36+
- Auto-open model selector if user selects deprecated model (#427)
37+
- Read approvalMode from config file (#298)
38+
- `/diff` command to view git diff (#426)
39+
- Tab completions for file paths (#279)
40+
- Add /command autocomplete (#317)
41+
- Allow multi-line input (#438)
42+
43+
### 🐛 Bug Fixes
44+
45+
- `full-auto` support in quiet mode (#374)
46+
- Enable shell option for child process execution (#391)
47+
- Configure husky and lint-staged for pnpm monorepo (#384)
48+
- Command pipe execution by improving shell detection (#437)
49+
- Name of the file not matching the name of the component (#354)
50+
- Allow proper exit from new Switch approval mode dialog (#453)
51+
- Ensure /clear resets context and exclude system messages from approximateTokenUsed count (#443)
52+
- `/clear` now clears terminal screen and resets context left indicator (#425)
53+
- Correct fish completion function name in CLI script (#485)
54+
- Auto-open model-selector when model is not found (#448)
55+
- Remove unnecessary isLoggingEnabled() checks (#420)
56+
- Improve test reliability for `raw-exec` (#434)
57+
- Unintended tear down of agent loop (#483)
58+
- Remove extraneous type casts (#462)
59+
560
## `0.1.2504181820`
661

762
### 🚀 Features

README.md

+150-114
Large diffs are not rendered by default.

cliff.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ commit_parsers = [
3838
{ message = "^fix", group = "<!-- 1 -->🐛 Bug Fixes" },
3939
{ message = "^bump", group = "<!-- 6 -->🛳️ Release" },
4040
# Fallback – skip anything that didn't match the above rules.
41-
{ message = ".*", group = "<!-- 10 -->💼 Other", skip = true },
41+
{ message = ".*", group = "<!-- 10 -->💼 Other" },
4242
]
4343

4444
filter_unconventional = false

codex-cli/build.mjs

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import * as esbuild from "esbuild";
22
import * as fs from "fs";
33
import * as path from "path";
4+
5+
const OUT_DIR = 'dist'
46
/**
57
* ink attempts to import react-devtools-core in an ESM-unfriendly way:
68
*
@@ -39,6 +41,11 @@ const isDevBuild =
3941

4042
const plugins = [ignoreReactDevToolsPlugin];
4143

44+
// Build Hygiene, ensure we drop previous dist dir and any leftover files
45+
const outPath = path.resolve(OUT_DIR);
46+
if (fs.existsSync(outPath)) {
47+
fs.rmSync(outPath, { recursive: true, force: true });
48+
}
4249

4350
// Add a shebang that enables source‑map support for dev builds so that stack
4451
// traces point to the original TypeScript lines without requiring callers to
@@ -50,7 +57,7 @@ if (isDevBuild) {
5057
name: "dev-shebang",
5158
setup(build) {
5259
build.onEnd(async () => {
53-
const outFile = path.resolve(isDevBuild ? "dist/cli-dev.js" : "dist/cli.js");
60+
const outFile = path.resolve(isDevBuild ? `${OUT_DIR}/cli-dev.js` : `${OUT_DIR}/cli.js`);
5461
let code = await fs.promises.readFile(outFile, "utf8");
5562
if (code.startsWith("#!")) {
5663
code = code.replace(/^#!.*\n/, devShebangLine);
@@ -69,7 +76,7 @@ esbuild
6976
format: "esm",
7077
platform: "node",
7178
tsconfig: "tsconfig.json",
72-
outfile: isDevBuild ? "dist/cli-dev.js" : "dist/cli.js",
79+
outfile: isDevBuild ? `${OUT_DIR}/cli-dev.js` : `${OUT_DIR}/cli.js`,
7380
minify: !isDevBuild,
7481
sourcemap: isDevBuild ? "inline" : true,
7582
plugins,

codex-cli/package.json

+2-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@openai/codex",
3-
"version": "0.1.2504181820",
3+
"version": "0.1.2504221401",
44
"license": "Apache-2.0",
55
"bin": {
66
"codex": "bin/codex.js"
@@ -26,8 +26,7 @@
2626
"release": "pnpm run release:readme && pnpm run release:version && pnpm install && pnpm run release:build-and-publish"
2727
},
2828
"files": [
29-
"dist",
30-
"src"
29+
"dist"
3130
],
3231
"dependencies": {
3332
"@inkjs/ui": "^2.0.0",

codex-cli/src/approvals.ts

+80-36
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import {
44
identify_files_added,
55
identify_files_needed,
66
} from "./utils/agent/apply-patch";
7-
import { loadConfig } from "./utils/config";
87
import * as path from "path";
98
import { parse } from "shell-quote";
109

@@ -72,13 +71,14 @@ export type ApprovalPolicy =
7271
*/
7372
export function canAutoApprove(
7473
command: ReadonlyArray<string>,
74+
workdir: string | undefined,
7575
policy: ApprovalPolicy,
7676
writableRoots: ReadonlyArray<string>,
7777
env: NodeJS.ProcessEnv = process.env,
7878
): SafetyAssessment {
7979
if (command[0] === "apply_patch") {
8080
return command.length === 2 && typeof command[1] === "string"
81-
? canAutoApproveApplyPatch(command[1], writableRoots, policy)
81+
? canAutoApproveApplyPatch(command[1], workdir, writableRoots, policy)
8282
: {
8383
type: "reject",
8484
reason: "Invalid apply_patch command",
@@ -104,7 +104,12 @@ export function canAutoApprove(
104104
) {
105105
const applyPatchArg = tryParseApplyPatch(command[2]);
106106
if (applyPatchArg != null) {
107-
return canAutoApproveApplyPatch(applyPatchArg, writableRoots, policy);
107+
return canAutoApproveApplyPatch(
108+
applyPatchArg,
109+
workdir,
110+
writableRoots,
111+
policy,
112+
);
108113
}
109114

110115
let bashCmd;
@@ -136,8 +141,8 @@ export function canAutoApprove(
136141
// bashCmd could be a mix of strings and operators, e.g.:
137142
// "ls || (true && pwd)" => [ 'ls', { op: '||' }, '(', 'true', { op: '&&' }, 'pwd', ')' ]
138143
// We try to ensure that *every* command segment is deemed safe and that
139-
// all operators belong to an allowlist. If so, the entire expression is
140-
// considered autoapprovable.
144+
// all operators belong to an allow-list. If so, the entire expression is
145+
// considered auto-approvable.
141146

142147
const shellSafe = isEntireShellExpressionSafe(bashCmd);
143148
if (shellSafe != null) {
@@ -163,6 +168,7 @@ export function canAutoApprove(
163168

164169
function canAutoApproveApplyPatch(
165170
applyPatchArg: string,
171+
workdir: string | undefined,
166172
writableRoots: ReadonlyArray<string>,
167173
policy: ApprovalPolicy,
168174
): SafetyAssessment {
@@ -180,7 +186,13 @@ function canAutoApproveApplyPatch(
180186
break;
181187
}
182188

183-
if (isWritePatchConstrainedToWritablePaths(applyPatchArg, writableRoots)) {
189+
if (
190+
isWritePatchConstrainedToWritablePaths(
191+
applyPatchArg,
192+
workdir,
193+
writableRoots,
194+
)
195+
) {
184196
return {
185197
type: "auto-approve",
186198
reason: "apply_patch command is constrained to writable paths",
@@ -209,6 +221,7 @@ function canAutoApproveApplyPatch(
209221
*/
210222
function isWritePatchConstrainedToWritablePaths(
211223
applyPatchArg: string,
224+
workdir: string | undefined,
212225
writableRoots: ReadonlyArray<string>,
213226
): boolean {
214227
// `identify_files_needed()` returns a list of files that will be modified or
@@ -223,35 +236,60 @@ function isWritePatchConstrainedToWritablePaths(
223236
return (
224237
allPathsConstrainedTowritablePaths(
225238
identify_files_needed(applyPatchArg),
239+
workdir,
226240
writableRoots,
227241
) &&
228242
allPathsConstrainedTowritablePaths(
229243
identify_files_added(applyPatchArg),
244+
workdir,
230245
writableRoots,
231246
)
232247
);
233248
}
234249

235250
function allPathsConstrainedTowritablePaths(
236251
candidatePaths: ReadonlyArray<string>,
252+
workdir: string | undefined,
237253
writableRoots: ReadonlyArray<string>,
238254
): boolean {
239255
return candidatePaths.every((candidatePath) =>
240-
isPathConstrainedTowritablePaths(candidatePath, writableRoots),
256+
isPathConstrainedTowritablePaths(candidatePath, workdir, writableRoots),
241257
);
242258
}
243259

244260
/** If candidatePath is relative, it will be resolved against cwd. */
245261
function isPathConstrainedTowritablePaths(
246262
candidatePath: string,
263+
workdir: string | undefined,
247264
writableRoots: ReadonlyArray<string>,
248265
): boolean {
249-
const candidateAbsolutePath = path.resolve(candidatePath);
266+
const candidateAbsolutePath = resolvePathAgainstWorkdir(
267+
candidatePath,
268+
workdir,
269+
);
270+
250271
return writableRoots.some((writablePath) =>
251272
pathContains(writablePath, candidateAbsolutePath),
252273
);
253274
}
254275

276+
/**
277+
* If not already an absolute path, resolves `candidatePath` against `workdir`
278+
* if specified; otherwise, against `process.cwd()`.
279+
*/
280+
export function resolvePathAgainstWorkdir(
281+
candidatePath: string,
282+
workdir: string | undefined,
283+
): string {
284+
if (path.isAbsolute(candidatePath)) {
285+
return candidatePath;
286+
} else if (workdir != null) {
287+
return path.resolve(workdir, candidatePath);
288+
} else {
289+
return path.resolve(candidatePath);
290+
}
291+
}
292+
255293
/** Both `parent` and `child` must be absolute paths. */
256294
function pathContains(parent: string, child: string): boolean {
257295
const relative = path.relative(parent, child);
@@ -297,24 +335,6 @@ export function isSafeCommand(
297335
): SafeCommandReason | null {
298336
const [cmd0, cmd1, cmd2, cmd3] = command;
299337

300-
const config = loadConfig();
301-
if (config.safeCommands && Array.isArray(config.safeCommands)) {
302-
for (const safe of config.safeCommands) {
303-
// safe: "npm test" → ["npm", "test"]
304-
const safeArr = typeof safe === "string" ? safe.trim().split(/\s+/) : [];
305-
if (
306-
safeArr.length > 0 &&
307-
safeArr.length <= command.length &&
308-
safeArr.every((v, i) => v === command[i])
309-
) {
310-
return {
311-
reason: "User-defined safe command",
312-
group: "User config",
313-
};
314-
}
315-
}
316-
}
317-
318338
switch (cmd0) {
319339
case "cd":
320340
return {
@@ -333,7 +353,7 @@ export function isSafeCommand(
333353
};
334354
case "true":
335355
return {
336-
reason: "Noop (true)",
356+
reason: "No-op (true)",
337357
group: "Utility",
338358
};
339359
case "echo":
@@ -348,11 +368,20 @@ export function isSafeCommand(
348368
reason: "Ripgrep search",
349369
group: "Searching",
350370
};
351-
case "find":
352-
return {
353-
reason: "Find files or directories",
354-
group: "Searching",
355-
};
371+
case "find": {
372+
// Certain options to `find` allow executing arbitrary processes, so we
373+
// cannot auto-approve them.
374+
if (
375+
command.some((arg: string) => UNSAFE_OPTIONS_FOR_FIND_COMMAND.has(arg))
376+
) {
377+
break;
378+
} else {
379+
return {
380+
reason: "Find files or directories",
381+
group: "Searching",
382+
};
383+
}
384+
}
356385
case "grep":
357386
return {
358387
reason: "Text search (grep)",
@@ -440,12 +469,27 @@ function isValidSedNArg(arg: string | undefined): boolean {
440469
return arg != null && /^(\d+,)?\d+p$/.test(arg);
441470
}
442471

472+
const UNSAFE_OPTIONS_FOR_FIND_COMMAND: ReadonlySet<string> = new Set([
473+
// Options that can execute arbitrary commands.
474+
"-exec",
475+
"-execdir",
476+
"-ok",
477+
"-okdir",
478+
// Option that deletes matching files.
479+
"-delete",
480+
// Options that write pathnames to a file.
481+
"-fls",
482+
"-fprint",
483+
"-fprint0",
484+
"-fprintf",
485+
]);
486+
443487
// ---------------- Helper utilities for complex shell expressions -----------------
444488

445-
// A conservative allowlist of bash operators that do not, on their own, cause
489+
// A conservative allow-list of bash operators that do not, on their own, cause
446490
// side effects. Redirections (>, >>, <, etc.) and command substitution `$()`
447491
// are intentionally excluded. Parentheses used for grouping are treated as
448-
// strings by `shellquote`, so we do not add them here. Reference:
492+
// strings by `shell-quote`, so we do not add them here. Reference:
449493
// https://github.com/substack/node-shell-quote#parsecmd-opts
450494
const SAFE_SHELL_OPERATORS: ReadonlySet<string> = new Set([
451495
"&&", // logical AND
@@ -471,7 +515,7 @@ function isEntireShellExpressionSafe(
471515
}
472516

473517
try {
474-
// Collect command segments delimited by operators. `shellquote` represents
518+
// Collect command segments delimited by operators. `shell-quote` represents
475519
// subshell grouping parentheses as literal strings "(" and ")"; treat them
476520
// as unsafe to keep the logic simple (since subshells could introduce
477521
// unexpected scope changes).
@@ -539,7 +583,7 @@ function isParseEntryWithOp(
539583
return (
540584
typeof entry === "object" &&
541585
entry != null &&
542-
// Using the safe `in` operator keeps the check propertysafe even when
586+
// Using the safe `in` operator keeps the check property-safe even when
543587
// `entry` is a `string`.
544588
"op" in entry &&
545589
typeof (entry as { op?: unknown }).op === "string"

0 commit comments

Comments
 (0)