add support for -w,--writable-root to add more writable roots for sandbox (#263)
This adds support for a new flag, `-w,--writable-root`, that can be specified multiple times to _amend_ the list of folders that should be configured as "writable roots" by the sandbox used in `full-auto` mode. Values that are passed as relative paths will be resolved to absolute paths. Incidentally, this required updating a number of the `agent*.test.ts` files: it feels like some of the setup logic across those tests could be consolidated. In my testing, it seems that this might be slightly out of distribution for the model, as I had to explicitly tell it to run `apply_patch` and that it had the permissions to write those files (initially, it just showed me a diff and told me to apply it myself). Nevertheless, I think this is a good starting point.
This commit is contained in:
@@ -22,6 +22,7 @@ type Props = {
|
||||
imagePaths?: Array<string>;
|
||||
rollout?: AppRollout;
|
||||
approvalPolicy: ApprovalPolicy;
|
||||
additionalWritableRoots: ReadonlyArray<string>;
|
||||
fullStdout: boolean;
|
||||
};
|
||||
|
||||
@@ -31,6 +32,7 @@ export default function App({
|
||||
rollout,
|
||||
imagePaths,
|
||||
approvalPolicy,
|
||||
additionalWritableRoots,
|
||||
fullStdout,
|
||||
}: Props): JSX.Element {
|
||||
const app = useApp();
|
||||
@@ -97,6 +99,7 @@ export default function App({
|
||||
prompt={prompt}
|
||||
imagePaths={imagePaths}
|
||||
approvalPolicy={approvalPolicy}
|
||||
additionalWritableRoots={additionalWritableRoots}
|
||||
fullStdout={fullStdout}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -53,13 +53,14 @@ const cli = meow(
|
||||
$ codex completion <bash|zsh|fish>
|
||||
|
||||
Options
|
||||
-h, --help Show usage and exit
|
||||
-m, --model <model> Model to use for completions (default: o4-mini)
|
||||
-i, --image <path> Path(s) to image files to include as input
|
||||
-v, --view <rollout> Inspect a previously saved rollout instead of starting a session
|
||||
-q, --quiet Non-interactive mode that only prints the assistant's final output
|
||||
-c, --config Open the instructions file in your editor
|
||||
-a, --approval-mode <mode> Override the approval policy: 'suggest', 'auto-edit', or 'full-auto'
|
||||
-h, --help Show usage and exit
|
||||
-m, --model <model> Model to use for completions (default: o4-mini)
|
||||
-i, --image <path> Path(s) to image files to include as input
|
||||
-v, --view <rollout> Inspect a previously saved rollout instead of starting a session
|
||||
-q, --quiet Non-interactive mode that only prints the assistant's final output
|
||||
-c, --config Open the instructions file in your editor
|
||||
-w, --writable-root <path> Writable folder for sandbox in full-auto mode (can be specified multiple times)
|
||||
-a, --approval-mode <mode> Override the approval policy: 'suggest', 'auto-edit', or 'full-auto'
|
||||
|
||||
--auto-edit Automatically approve file edits; still prompt for commands
|
||||
--full-auto Automatically approve edits and commands when executed in the sandbox
|
||||
@@ -122,6 +123,13 @@ const cli = meow(
|
||||
description:
|
||||
"Determine the approval mode for Codex (default: suggest) Values: suggest, auto-edit, full-auto",
|
||||
},
|
||||
writableRoot: {
|
||||
type: "string",
|
||||
isMultiple: true,
|
||||
aliases: ["w"],
|
||||
description:
|
||||
"Writable folder for sandbox in full-auto mode (can be specified multiple times)",
|
||||
},
|
||||
noProjectDoc: {
|
||||
type: "boolean",
|
||||
description: "Disable automatic inclusion of project‑level codex.md",
|
||||
@@ -276,6 +284,11 @@ if (fullContextMode) {
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Ensure that all values in additionalWritableRoots are absolute paths.
|
||||
const additionalWritableRoots: ReadonlyArray<string> = (
|
||||
cli.flags.writableRoot ?? []
|
||||
).map((p) => path.resolve(p));
|
||||
|
||||
// If we are running in --quiet mode, do that and exit.
|
||||
const quietMode = Boolean(cli.flags.quiet);
|
||||
const autoApproveEverything = Boolean(
|
||||
@@ -298,6 +311,7 @@ if (quietMode) {
|
||||
approvalPolicy: autoApproveEverything
|
||||
? AutoApprovalMode.FULL_AUTO
|
||||
: AutoApprovalMode.SUGGEST,
|
||||
additionalWritableRoots,
|
||||
config,
|
||||
});
|
||||
onExit();
|
||||
@@ -332,6 +346,7 @@ const instance = render(
|
||||
rollout={rollout}
|
||||
imagePaths={imagePaths}
|
||||
approvalPolicy={approvalPolicy}
|
||||
additionalWritableRoots={additionalWritableRoots}
|
||||
fullStdout={fullStdout}
|
||||
/>,
|
||||
{
|
||||
@@ -393,11 +408,13 @@ async function runQuietMode({
|
||||
prompt,
|
||||
imagePaths,
|
||||
approvalPolicy,
|
||||
additionalWritableRoots,
|
||||
config,
|
||||
}: {
|
||||
prompt: string;
|
||||
imagePaths: Array<string>;
|
||||
approvalPolicy: ApprovalPolicy;
|
||||
additionalWritableRoots: ReadonlyArray<string>;
|
||||
config: AppConfig;
|
||||
}): Promise<void> {
|
||||
const agent = new AgentLoop({
|
||||
@@ -405,6 +422,7 @@ async function runQuietMode({
|
||||
config: config,
|
||||
instructions: config.instructions,
|
||||
approvalPolicy,
|
||||
additionalWritableRoots,
|
||||
onItem: (item: ResponseItem) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(formatResponseItemForQuietMode(item));
|
||||
|
||||
@@ -37,6 +37,7 @@ type Props = {
|
||||
prompt?: string;
|
||||
imagePaths?: Array<string>;
|
||||
approvalPolicy: ApprovalPolicy;
|
||||
additionalWritableRoots: ReadonlyArray<string>;
|
||||
fullStdout: boolean;
|
||||
};
|
||||
|
||||
@@ -122,6 +123,7 @@ export default function TerminalChat({
|
||||
prompt: _initialPrompt,
|
||||
imagePaths: _initialImagePaths,
|
||||
approvalPolicy: initialApprovalPolicy,
|
||||
additionalWritableRoots,
|
||||
fullStdout,
|
||||
}: Props): React.ReactElement {
|
||||
const [model, setModel] = useState<string>(config.model);
|
||||
@@ -183,6 +185,7 @@ export default function TerminalChat({
|
||||
config,
|
||||
instructions: config.instructions,
|
||||
approvalPolicy,
|
||||
additionalWritableRoots,
|
||||
onLastResponseId: setLastResponseId,
|
||||
onItem: (item) => {
|
||||
log(`onItem: ${JSON.stringify(item)}`);
|
||||
@@ -248,7 +251,13 @@ export default function TerminalChat({
|
||||
agentRef.current = undefined;
|
||||
forceUpdate(); // re‑render after teardown too
|
||||
};
|
||||
}, [model, config, approvalPolicy, requestConfirmation]);
|
||||
}, [
|
||||
model,
|
||||
config,
|
||||
approvalPolicy,
|
||||
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
|
||||
|
||||
@@ -45,6 +45,9 @@ type AgentLoopParams = {
|
||||
onItem: (item: ResponseItem) => void;
|
||||
onLoading: (loading: boolean) => void;
|
||||
|
||||
/** Extra writable roots to use with sandbox execution. */
|
||||
additionalWritableRoots: ReadonlyArray<string>;
|
||||
|
||||
/** Called when the command is not auto-approved to request explicit user review. */
|
||||
getCommandConfirmation: (
|
||||
command: Array<string>,
|
||||
@@ -58,6 +61,7 @@ export class AgentLoop {
|
||||
private instructions?: string;
|
||||
private approvalPolicy: ApprovalPolicy;
|
||||
private config: AppConfig;
|
||||
private additionalWritableRoots: ReadonlyArray<string>;
|
||||
|
||||
// Using `InstanceType<typeof OpenAI>` sidesteps typing issues with the OpenAI package under
|
||||
// the TS 5+ `moduleResolution=bundler` setup. OpenAI client instance. We keep the concrete
|
||||
@@ -213,6 +217,7 @@ export class AgentLoop {
|
||||
onLoading,
|
||||
getCommandConfirmation,
|
||||
onLastResponseId,
|
||||
additionalWritableRoots,
|
||||
}: AgentLoopParams & { config?: AppConfig }) {
|
||||
this.model = model;
|
||||
this.instructions = instructions;
|
||||
@@ -229,6 +234,7 @@ export class AgentLoop {
|
||||
model,
|
||||
instructions: instructions ?? "",
|
||||
} as AppConfig);
|
||||
this.additionalWritableRoots = additionalWritableRoots;
|
||||
this.onItem = onItem;
|
||||
this.onLoading = onLoading;
|
||||
this.getCommandConfirmation = getCommandConfirmation;
|
||||
@@ -358,6 +364,7 @@ export class AgentLoop {
|
||||
args,
|
||||
this.config,
|
||||
this.approvalPolicy,
|
||||
this.additionalWritableRoots,
|
||||
this.getCommandConfirmation,
|
||||
this.execAbortController?.signal,
|
||||
);
|
||||
|
||||
@@ -16,7 +16,12 @@ const DEFAULT_TIMEOUT_MS = 10_000; // 10 seconds
|
||||
* mapped to a non-zero exit code and the error message should be in stderr.
|
||||
*/
|
||||
export function exec(
|
||||
{ cmd, workdir, timeoutInMillis }: ExecInput,
|
||||
{
|
||||
cmd,
|
||||
workdir,
|
||||
timeoutInMillis,
|
||||
additionalWritableRoots,
|
||||
}: ExecInput & { additionalWritableRoots: ReadonlyArray<string> },
|
||||
sandbox: SandboxType,
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<ExecResult> {
|
||||
@@ -30,7 +35,12 @@ export function exec(
|
||||
timeout: timeoutInMillis || DEFAULT_TIMEOUT_MS,
|
||||
...(workdir ? { cwd: workdir } : {}),
|
||||
};
|
||||
const writableRoots = [process.cwd(), os.tmpdir()];
|
||||
// Merge default writable roots with any user-specified ones.
|
||||
const writableRoots = [
|
||||
process.cwd(),
|
||||
os.tmpdir(),
|
||||
...additionalWritableRoots,
|
||||
];
|
||||
return execForSandbox(cmd, opts, writableRoots, abortSignal);
|
||||
}
|
||||
|
||||
|
||||
@@ -74,6 +74,7 @@ export async function handleExecCommand(
|
||||
args: ExecInput,
|
||||
config: AppConfig,
|
||||
policy: ApprovalPolicy,
|
||||
additionalWritableRoots: ReadonlyArray<string>,
|
||||
getCommandConfirmation: (
|
||||
command: Array<string>,
|
||||
applyPatch: ApplyPatchCommand | undefined,
|
||||
@@ -91,6 +92,7 @@ export async function handleExecCommand(
|
||||
args,
|
||||
/* applyPatch */ undefined,
|
||||
/* runInSandbox */ false,
|
||||
additionalWritableRoots,
|
||||
abortSignal,
|
||||
).then(convertSummaryToResult);
|
||||
}
|
||||
@@ -138,6 +140,7 @@ export async function handleExecCommand(
|
||||
args,
|
||||
applyPatch,
|
||||
runInSandbox,
|
||||
additionalWritableRoots,
|
||||
abortSignal,
|
||||
);
|
||||
// If the operation was aborted in the meantime, propagate the cancellation
|
||||
@@ -170,7 +173,13 @@ export async function handleExecCommand(
|
||||
} else {
|
||||
// The user has approved the command, so we will run it outside of the
|
||||
// sandbox.
|
||||
const summary = await execCommand(args, applyPatch, false, abortSignal);
|
||||
const summary = await execCommand(
|
||||
args,
|
||||
applyPatch,
|
||||
false,
|
||||
additionalWritableRoots,
|
||||
abortSignal,
|
||||
);
|
||||
return convertSummaryToResult(summary);
|
||||
}
|
||||
} else {
|
||||
@@ -202,6 +211,7 @@ async function execCommand(
|
||||
execInput: ExecInput,
|
||||
applyPatchCommand: ApplyPatchCommand | undefined,
|
||||
runInSandbox: boolean,
|
||||
additionalWritableRoots: ReadonlyArray<string>,
|
||||
abortSignal?: AbortSignal,
|
||||
): Promise<ExecCommandSummary> {
|
||||
let { workdir } = execInput;
|
||||
@@ -239,7 +249,11 @@ async function execCommand(
|
||||
const execResult =
|
||||
applyPatchCommand != null
|
||||
? execApplyPatch(applyPatchCommand.patch)
|
||||
: await exec(execInput, await getSandbox(runInSandbox), abortSignal);
|
||||
: await exec(
|
||||
{ ...execInput, additionalWritableRoots },
|
||||
await getSandbox(runInSandbox),
|
||||
abortSignal,
|
||||
);
|
||||
const duration = Date.now() - start;
|
||||
const { stdout, stderr, exitCode } = execResult;
|
||||
|
||||
|
||||
@@ -88,6 +88,7 @@ describe("cancel before first function_call", () => {
|
||||
const { _test } = (await import("openai")) as any;
|
||||
|
||||
const agent = new AgentLoop({
|
||||
additionalWritableRoots: [],
|
||||
model: "any",
|
||||
instructions: "",
|
||||
approvalPolicy: { mode: "auto" } as any,
|
||||
|
||||
@@ -99,6 +99,7 @@ describe("cancel clears previous_response_id", () => {
|
||||
model: "any",
|
||||
instructions: "",
|
||||
approvalPolicy: { mode: "auto" } as any,
|
||||
additionalWritableRoots: [],
|
||||
onItem: () => {},
|
||||
onLoading: () => {},
|
||||
getCommandConfirmation: async () => ({ review: "yes" } as any),
|
||||
|
||||
@@ -92,6 +92,7 @@ describe("Agent cancellation race", () => {
|
||||
const items: Array<any> = [];
|
||||
|
||||
const agent = new AgentLoop({
|
||||
additionalWritableRoots: [],
|
||||
model: "any",
|
||||
instructions: "",
|
||||
config: { model: "any", instructions: "" },
|
||||
|
||||
@@ -91,6 +91,7 @@ describe("Agent cancellation", () => {
|
||||
instructions: "",
|
||||
config: { model: "any", instructions: "" },
|
||||
approvalPolicy: { mode: "auto" } as any,
|
||||
additionalWritableRoots: [],
|
||||
onItem: (item) => {
|
||||
received.push(item);
|
||||
},
|
||||
@@ -136,6 +137,7 @@ describe("Agent cancellation", () => {
|
||||
const received: Array<any> = [];
|
||||
|
||||
const agent = new AgentLoop({
|
||||
additionalWritableRoots: [],
|
||||
model: "any",
|
||||
instructions: "",
|
||||
config: { model: "any", instructions: "" },
|
||||
|
||||
@@ -118,6 +118,7 @@ describe("function_call_output includes original call ID", () => {
|
||||
model: "any",
|
||||
instructions: "",
|
||||
approvalPolicy: { mode: "auto" } as any,
|
||||
additionalWritableRoots: [],
|
||||
onItem: () => {},
|
||||
onLoading: () => {},
|
||||
getCommandConfirmation: async () => ({ review: "yes" } as any),
|
||||
|
||||
@@ -56,6 +56,7 @@ describe("AgentLoop – generic network/server errors", () => {
|
||||
const received: Array<any> = [];
|
||||
|
||||
const agent = new AgentLoop({
|
||||
additionalWritableRoots: [],
|
||||
model: "any",
|
||||
instructions: "",
|
||||
approvalPolicy: { mode: "auto" } as any,
|
||||
@@ -99,6 +100,7 @@ describe("AgentLoop – generic network/server errors", () => {
|
||||
const received: Array<any> = [];
|
||||
|
||||
const agent = new AgentLoop({
|
||||
additionalWritableRoots: [],
|
||||
model: "any",
|
||||
instructions: "",
|
||||
approvalPolicy: { mode: "auto" } as any,
|
||||
|
||||
@@ -34,6 +34,7 @@ describe("Agent interrupt and continue", () => {
|
||||
|
||||
// Create the agent
|
||||
const agent = new AgentLoop({
|
||||
additionalWritableRoots: [],
|
||||
model: "test-model",
|
||||
instructions: "",
|
||||
approvalPolicy: { mode: "auto" } as any,
|
||||
|
||||
@@ -58,6 +58,7 @@ describe("AgentLoop – invalid request / 4xx errors", () => {
|
||||
model: "any",
|
||||
instructions: "",
|
||||
approvalPolicy: { mode: "auto" } as any,
|
||||
additionalWritableRoots: [],
|
||||
onItem: (i) => received.push(i),
|
||||
onLoading: () => {},
|
||||
getCommandConfirmation: async () => ({ review: "yes" } as any),
|
||||
|
||||
@@ -58,6 +58,7 @@ describe("AgentLoop – max_tokens too large error", () => {
|
||||
const received: Array<any> = [];
|
||||
|
||||
const agent = new AgentLoop({
|
||||
additionalWritableRoots: [],
|
||||
model: "any",
|
||||
instructions: "",
|
||||
approvalPolicy: { mode: "auto" } as any,
|
||||
|
||||
@@ -109,6 +109,7 @@ describe("AgentLoop – network resilience", () => {
|
||||
model: "any",
|
||||
instructions: "",
|
||||
approvalPolicy: { mode: "auto" } as any,
|
||||
additionalWritableRoots: [],
|
||||
onItem: (i) => received.push(i),
|
||||
onLoading: () => {},
|
||||
getCommandConfirmation: async () => ({ review: "yes" } as any),
|
||||
@@ -150,6 +151,7 @@ describe("AgentLoop – network resilience", () => {
|
||||
model: "any",
|
||||
instructions: "",
|
||||
approvalPolicy: { mode: "auto" } as any,
|
||||
additionalWritableRoots: [],
|
||||
onItem: (i) => received.push(i),
|
||||
onLoading: () => {},
|
||||
getCommandConfirmation: async () => ({ review: "yes" } as any),
|
||||
|
||||
@@ -112,6 +112,7 @@ describe("AgentLoop", () => {
|
||||
expect(config.instructions).toContain("Hello docs!");
|
||||
|
||||
const agent = new AgentLoop({
|
||||
additionalWritableRoots: [],
|
||||
model: "o3", // arbitrary
|
||||
instructions: config.instructions,
|
||||
config,
|
||||
|
||||
@@ -79,6 +79,7 @@ describe("AgentLoop – rate‑limit handling", () => {
|
||||
model: "any",
|
||||
instructions: "",
|
||||
approvalPolicy: { mode: "auto" } as any,
|
||||
additionalWritableRoots: [],
|
||||
onItem: (i) => received.push(i),
|
||||
onLoading: () => {},
|
||||
getCommandConfirmation: async () => ({ review: "yes" } as any),
|
||||
|
||||
@@ -97,6 +97,7 @@ describe("AgentLoop – automatic retry on 5xx errors", () => {
|
||||
model: "any",
|
||||
instructions: "",
|
||||
approvalPolicy: { mode: "auto" } as any,
|
||||
additionalWritableRoots: [],
|
||||
onItem: (i) => received.push(i),
|
||||
onLoading: () => {},
|
||||
getCommandConfirmation: async () => ({ review: "yes" } as any),
|
||||
@@ -134,6 +135,7 @@ describe("AgentLoop – automatic retry on 5xx errors", () => {
|
||||
model: "any",
|
||||
instructions: "",
|
||||
approvalPolicy: { mode: "auto" } as any,
|
||||
additionalWritableRoots: [],
|
||||
onItem: (i) => received.push(i),
|
||||
onLoading: () => {},
|
||||
getCommandConfirmation: async () => ({ review: "yes" } as any),
|
||||
|
||||
@@ -82,7 +82,14 @@ describe("Agent terminate (hard cancel)", () => {
|
||||
it("suppresses function_call_output and stops processing once terminate() is invoked", async () => {
|
||||
// Simulate a long‑running exec that would normally resolve with output.
|
||||
vi.spyOn(handleExec, "handleExecCommand").mockImplementation(
|
||||
async (_args, _config, _policy, _getConf, abortSignal) => {
|
||||
async (
|
||||
_args,
|
||||
_config,
|
||||
_policy,
|
||||
_additionalWritableRoots,
|
||||
_getConf,
|
||||
abortSignal,
|
||||
) => {
|
||||
// Wait until the abort signal is fired or 2s (whichever comes first).
|
||||
await new Promise<void>((resolve) => {
|
||||
if (abortSignal?.aborted) {
|
||||
@@ -106,6 +113,7 @@ describe("Agent terminate (hard cancel)", () => {
|
||||
instructions: "",
|
||||
config: { model: "any", instructions: "" },
|
||||
approvalPolicy: { mode: "auto" } as any,
|
||||
additionalWritableRoots: [],
|
||||
onItem: (item) => received.push(item),
|
||||
onLoading: () => {},
|
||||
getCommandConfirmation: async () => ({ review: "yes" } as any),
|
||||
@@ -141,6 +149,7 @@ describe("Agent terminate (hard cancel)", () => {
|
||||
instructions: "",
|
||||
config: { model: "any", instructions: "" },
|
||||
approvalPolicy: { mode: "auto" } as any,
|
||||
additionalWritableRoots: [],
|
||||
onItem: () => {},
|
||||
onLoading: () => {},
|
||||
getCommandConfirmation: async () => ({ review: "yes" } as any),
|
||||
|
||||
@@ -107,6 +107,7 @@ describe("thinking time counter", () => {
|
||||
model: "any",
|
||||
instructions: "",
|
||||
approvalPolicy: { mode: "auto" } as any,
|
||||
additionalWritableRoots: [],
|
||||
onItem: (i) => items.push(i),
|
||||
onLoading: () => {},
|
||||
getCommandConfirmation: async () => ({ review: "yes" } as any),
|
||||
|
||||
@@ -53,10 +53,12 @@ describe("handleExecCommand – invalid executable", () => {
|
||||
const policy = { mode: "auto" } as any;
|
||||
const getConfirmation = async () => ({ review: "yes" } as any);
|
||||
|
||||
const additionalWritableRoots: Array<string> = [];
|
||||
const { outputText, metadata } = await handleExecCommand(
|
||||
execInput,
|
||||
config,
|
||||
policy,
|
||||
additionalWritableRoots,
|
||||
getConfirmation,
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user