Skip to content

Commit 45b266e

Browse files
committed
propagate shell config from config file to exec command
- Introduced DEFAULT_SHELL_MAX_BYTES and DEFAULT_SHELL_MAX_LINES for shell output limits. - Updated loadConfig and saveConfig to handle new shell settings. - Modified exec functions to utilize shell output limits from config. - Added tests to verify loading and saving of custom shell configurations.
1 parent bfe6fac commit 45b266e

File tree

7 files changed

+154
-9
lines changed

7 files changed

+154
-9
lines changed

codex-cli/src/utils/agent/exec.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { AppConfig } from "../config.js";
12
import type { ExecInput, ExecResult } from "./sandbox/interface.js";
23
import type { SpawnOptions } from "child_process";
34
import type { ParseEntry } from "shell-quote";
@@ -41,6 +42,7 @@ export function exec(
4142
}: ExecInput & { additionalWritableRoots: ReadonlyArray<string> },
4243
sandbox: SandboxType,
4344
abortSignal?: AbortSignal,
45+
config?: AppConfig,
4446
): Promise<ExecResult> {
4547
// This is a temporary measure to understand what are the common base commands
4648
// until we start persisting and uploading rollouts
@@ -59,7 +61,7 @@ export function exec(
5961
os.tmpdir(),
6062
...additionalWritableRoots,
6163
];
62-
return execForSandbox(cmd, opts, writableRoots, abortSignal);
64+
return execForSandbox(cmd, opts, writableRoots, abortSignal, config);
6365
}
6466

6567
export function execApplyPatch(

codex-cli/src/utils/agent/handle-exec-command.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ export async function handleExecCommand(
9494
/* runInSandbox */ false,
9595
additionalWritableRoots,
9696
abortSignal,
97+
config,
9798
).then(convertSummaryToResult);
9899
}
99100

@@ -142,6 +143,7 @@ export async function handleExecCommand(
142143
runInSandbox,
143144
additionalWritableRoots,
144145
abortSignal,
146+
config,
145147
);
146148
// If the operation was aborted in the meantime, propagate the cancellation
147149
// upward by returning an empty (no-op) result so that the agent loop will
@@ -179,6 +181,7 @@ export async function handleExecCommand(
179181
false,
180182
additionalWritableRoots,
181183
abortSignal,
184+
config,
182185
);
183186
return convertSummaryToResult(summary);
184187
}
@@ -213,6 +216,7 @@ async function execCommand(
213216
runInSandbox: boolean,
214217
additionalWritableRoots: ReadonlyArray<string>,
215218
abortSignal?: AbortSignal,
219+
config?: AppConfig,
216220
): Promise<ExecCommandSummary> {
217221
let { workdir } = execInput;
218222
if (workdir) {
@@ -252,6 +256,7 @@ async function execCommand(
252256
{ ...execInput, additionalWritableRoots },
253257
await getSandbox(runInSandbox),
254258
abortSignal,
259+
config,
255260
);
256261
const duration = Date.now() - start;
257262
const { stdout, stderr, exitCode } = execResult;

codex-cli/src/utils/agent/sandbox/create-truncating-collector.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Maximum output cap: either MAX_OUTPUT_LINES lines or MAX_OUTPUT_BYTES bytes,
22
// whichever limit is reached first.
3-
const MAX_OUTPUT_BYTES = 1024 * 10; // 10 KB
4-
const MAX_OUTPUT_LINES = 256;
3+
import { DEFAULT_SHELL_MAX_BYTES, DEFAULT_SHELL_MAX_LINES } from "../../config";
54

65
/**
76
* Creates a collector that accumulates data Buffers from a stream up to
@@ -10,8 +9,8 @@ const MAX_OUTPUT_LINES = 256;
109
*/
1110
export function createTruncatingCollector(
1211
stream: NodeJS.ReadableStream,
13-
byteLimit: number = MAX_OUTPUT_BYTES,
14-
lineLimit: number = MAX_OUTPUT_LINES,
12+
byteLimit: number = DEFAULT_SHELL_MAX_BYTES,
13+
lineLimit: number = DEFAULT_SHELL_MAX_LINES,
1514
): {
1615
getString: () => string;
1716
hit: boolean;

codex-cli/src/utils/agent/sandbox/macos-seatbelt.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { ExecResult } from "./interface.js";
2+
import type { AppConfig } from "../../config.js";
23
import type { SpawnOptions } from "child_process";
34

45
import { exec } from "./raw-exec.js";
@@ -17,6 +18,7 @@ export function execWithSeatbelt(
1718
opts: SpawnOptions,
1819
writableRoots: ReadonlyArray<string>,
1920
abortSignal?: AbortSignal,
21+
config?: AppConfig,
2022
): Promise<ExecResult> {
2123
let scopedWritePolicy: string;
2224
let policyTemplateParams: Array<string>;
@@ -64,7 +66,7 @@ export function execWithSeatbelt(
6466
"--",
6567
...cmd,
6668
];
67-
return exec(fullCommand, opts, writableRoots, abortSignal);
69+
return exec(fullCommand, opts, writableRoots, abortSignal, config);
6870
}
6971

7072
const READ_ONLY_SEATBELT_POLICY = `

codex-cli/src/utils/agent/sandbox/raw-exec.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { ExecResult } from "./interface";
2+
import type { AppConfig } from "../../config";
23
import type {
34
ChildProcess,
45
SpawnOptions,
@@ -22,6 +23,7 @@ export function exec(
2223
options: SpawnOptions,
2324
_writableRoots: ReadonlyArray<string>,
2425
abortSignal?: AbortSignal,
26+
config?: AppConfig,
2527
): Promise<ExecResult> {
2628
// Adapt command for the current platform (e.g., convert 'ls' to 'dir' on Windows)
2729
const adaptedCommand = adaptCommandForPlatform(command);
@@ -143,9 +145,21 @@ export function exec(
143145
// ExecResult object so the rest of the agent loop can carry on gracefully.
144146

145147
return new Promise<ExecResult>((resolve) => {
148+
// Get shell output limits from config if available
149+
const maxBytes = config?.tools?.shell?.maxBytes;
150+
const maxLines = config?.tools?.shell?.maxLines;
151+
146152
// Collect stdout and stderr up to configured limits.
147-
const stdoutCollector = createTruncatingCollector(child.stdout!);
148-
const stderrCollector = createTruncatingCollector(child.stderr!);
153+
const stdoutCollector = createTruncatingCollector(
154+
child.stdout!,
155+
maxBytes,
156+
maxLines,
157+
);
158+
const stderrCollector = createTruncatingCollector(
159+
child.stderr!,
160+
maxBytes,
161+
maxLines,
162+
);
149163

150164
child.on("exit", (code, signal) => {
151165
const stdout = stdoutCollector.getString();

codex-cli/src/utils/config.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ export const DEFAULT_FULL_CONTEXT_MODEL = "gpt-4.1";
2121
export const DEFAULT_APPROVAL_MODE = AutoApprovalMode.SUGGEST;
2222
export const DEFAULT_INSTRUCTIONS = "";
2323

24+
// Default shell output limits
25+
export const DEFAULT_SHELL_MAX_BYTES = 1024 * 10; // 10 KB
26+
export const DEFAULT_SHELL_MAX_LINES = 256;
27+
2428
export const CONFIG_DIR = join(homedir(), ".codex");
2529
export const CONFIG_JSON_FILEPATH = join(CONFIG_DIR, "config.json");
2630
export const CONFIG_YAML_FILEPATH = join(CONFIG_DIR, "config.yaml");
@@ -108,6 +112,12 @@ export type StoredConfig = {
108112
saveHistory?: boolean;
109113
sensitivePatterns?: Array<string>;
110114
};
115+
tools?: {
116+
shell?: {
117+
maxBytes?: number;
118+
maxLines?: number;
119+
};
120+
};
111121
};
112122

113123
// Minimal config written on first run. An *empty* model string ensures that
@@ -145,6 +155,12 @@ export type AppConfig = {
145155
saveHistory: boolean;
146156
sensitivePatterns: Array<string>;
147157
};
158+
tools?: {
159+
shell?: {
160+
maxBytes: number;
161+
maxLines: number;
162+
};
163+
};
148164
};
149165

150166
// Formatting (quiet mode-only).
@@ -332,6 +348,15 @@ export const loadConfig = (
332348
notify: storedConfig.notify === true,
333349
approvalMode: storedConfig.approvalMode,
334350
disableResponseStorage: storedConfig.disableResponseStorage ?? false,
351+
// Add default tools config
352+
tools: {
353+
shell: {
354+
maxBytes:
355+
storedConfig.tools?.shell?.maxBytes ?? DEFAULT_SHELL_MAX_BYTES,
356+
maxLines:
357+
storedConfig.tools?.shell?.maxLines ?? DEFAULT_SHELL_MAX_LINES,
358+
},
359+
},
335360
};
336361

337362
// -----------------------------------------------------------------------
@@ -457,6 +482,18 @@ export const saveConfig = (
457482
};
458483
}
459484

485+
// Add tools settings if they exist
486+
if (config.tools) {
487+
configToSave.tools = {
488+
shell: config.tools.shell
489+
? {
490+
maxBytes: config.tools.shell.maxBytes,
491+
maxLines: config.tools.shell.maxLines,
492+
}
493+
: undefined,
494+
};
495+
}
496+
460497
if (ext === ".yaml" || ext === ".yml") {
461498
writeFileSync(targetPath, dumpYaml(configToSave), "utf-8");
462499
} else {

codex-cli/tests/config.test.tsx

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import type * as fsType from "fs";
22

3-
import { loadConfig, saveConfig } from "../src/utils/config.js"; // parent import first
3+
import {
4+
loadConfig,
5+
saveConfig,
6+
DEFAULT_SHELL_MAX_BYTES,
7+
DEFAULT_SHELL_MAX_LINES,
8+
} from "../src/utils/config.js"; // parent import first
49
import { AutoApprovalMode } from "../src/utils/auto-approval-mode.js";
510
import { tmpdir } from "os";
611
import { join } from "path";
@@ -275,3 +280,84 @@ test("handles empty user instructions when saving with project doc separator", (
275280
});
276281
expect(loadedConfig.instructions).toBe("");
277282
});
283+
284+
test("loads default shell config when not specified", () => {
285+
// Setup config without shell settings
286+
memfs[testConfigPath] = JSON.stringify(
287+
{
288+
model: "mymodel",
289+
},
290+
null,
291+
2,
292+
);
293+
memfs[testInstructionsPath] = "test instructions";
294+
295+
// Load config and verify default shell settings
296+
const loadedConfig = loadConfig(testConfigPath, testInstructionsPath, {
297+
disableProjectDoc: true,
298+
});
299+
300+
// Check shell settings were loaded with defaults
301+
expect(loadedConfig.tools).toBeDefined();
302+
expect(loadedConfig.tools?.shell).toBeDefined();
303+
expect(loadedConfig.tools?.shell?.maxBytes).toBe(DEFAULT_SHELL_MAX_BYTES);
304+
expect(loadedConfig.tools?.shell?.maxLines).toBe(DEFAULT_SHELL_MAX_LINES);
305+
});
306+
307+
test("loads and saves custom shell config", () => {
308+
// Setup config with custom shell settings
309+
const customMaxBytes = 12410;
310+
const customMaxLines = 500;
311+
312+
memfs[testConfigPath] = JSON.stringify(
313+
{
314+
model: "mymodel",
315+
tools: {
316+
shell: {
317+
maxBytes: customMaxBytes,
318+
maxLines: customMaxLines,
319+
},
320+
},
321+
},
322+
null,
323+
2,
324+
);
325+
memfs[testInstructionsPath] = "test instructions";
326+
327+
// Load config and verify custom shell settings
328+
const loadedConfig = loadConfig(testConfigPath, testInstructionsPath, {
329+
disableProjectDoc: true,
330+
});
331+
332+
// Check shell settings were loaded correctly
333+
expect(loadedConfig.tools?.shell?.maxBytes).toBe(customMaxBytes);
334+
expect(loadedConfig.tools?.shell?.maxLines).toBe(customMaxLines);
335+
336+
// Modify shell settings and save
337+
const updatedMaxBytes = 20000;
338+
const updatedMaxLines = 1000;
339+
340+
const updatedConfig = {
341+
...loadedConfig,
342+
tools: {
343+
shell: {
344+
maxBytes: updatedMaxBytes,
345+
maxLines: updatedMaxLines,
346+
},
347+
},
348+
};
349+
350+
saveConfig(updatedConfig, testConfigPath, testInstructionsPath);
351+
352+
// Verify saved config contains updated shell settings
353+
expect(memfs[testConfigPath]).toContain(`"maxBytes": ${updatedMaxBytes}`);
354+
expect(memfs[testConfigPath]).toContain(`"maxLines": ${updatedMaxLines}`);
355+
356+
// Load again and verify updated values
357+
const reloadedConfig = loadConfig(testConfigPath, testInstructionsPath, {
358+
disableProjectDoc: true,
359+
});
360+
361+
expect(reloadedConfig.tools?.shell?.maxBytes).toBe(updatedMaxBytes);
362+
expect(reloadedConfig.tools?.shell?.maxLines).toBe(updatedMaxLines);
363+
});

0 commit comments

Comments
 (0)