diff --git a/readme-dev.md b/readme-dev.md index bc147807..9cd0ec48 100644 --- a/readme-dev.md +++ b/readme-dev.md @@ -13,6 +13,19 @@ Set `CODEX_PATH` to run a different Codex binary; versions other than the one sp - `NO_BROWSER` - hide browser-based ChatGPT auth when set. - `APP_SERVER_LOGS` - directory for adapter logs. +`CODEX_CONFIG` can enable network access for the `agent` (workspace-write) mode: + +```json +{ + "sandbox_workspace_write": { + "network_access": true + } +} +``` + +Only a boolean value overrides the workspace-write default of `false`. This setting +does not change the `read-only` or `agent-full-access` policies. + ### Quick start #### Develop on Windows? diff --git a/src/CodexAcpClient.ts b/src/CodexAcpClient.ts index 0c7af207..57e73764 100644 --- a/src/CodexAcpClient.ts +++ b/src/CodexAcpClient.ts @@ -704,7 +704,10 @@ export class CodexAcpClient { threadId: request.sessionId, input: input, approvalPolicy: agentMode.approvalPolicy, - sandboxPolicy: addAdditionalDirectoriesToSandboxPolicy(agentMode.sandboxPolicy, additionalDirectories), + sandboxPolicy: applySandboxWorkspaceWriteNetworkAccess( + addAdditionalDirectoriesToSandboxPolicy(agentMode.sandboxPolicy, additionalDirectories), + this.config, + ), summary: disableSummary ? "none" : "auto", effort: effort, model: modelId.model, @@ -1062,6 +1065,25 @@ function addAdditionalDirectoriesToSandboxPolicy( }; } +function applySandboxWorkspaceWriteNetworkAccess( + sandboxPolicy: SandboxPolicy, + config: JsonObject, +): SandboxPolicy { + if (sandboxPolicy.type !== "workspaceWrite") { + return sandboxPolicy; + } + + const sandboxConfig = config["sandbox_workspace_write"]; + if (!isJsonObject(sandboxConfig) || typeof sandboxConfig["network_access"] !== "boolean") { + return sandboxPolicy; + } + + return { + ...sandboxPolicy, + networkAccess: sandboxConfig["network_access"], + }; +} + function uniqueStrings(values: string[]): string[] { return Array.from(new Set(values)); } diff --git a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts index d58bd594..e682bc13 100644 --- a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts +++ b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts @@ -848,6 +848,72 @@ describe('ACP server test', { timeout: 40_000 }, () => { }); }); + describe('workspace-write network access configuration', () => { + async function promptWithConfig( + networkAccess: boolean | string, + sessionOverrides: Partial = {}, + ) { + const {mockFixture, turnStartSpy} = setupPromptFixture( + {additionalDirectories: ["/workspace/extra"], ...sessionOverrides}, + {sandbox_workspace_write: {network_access: networkAccess}}, + ); + + await mockFixture.getCodexAcpAgent().prompt({ + sessionId: "session-id", + prompt: [{type: "text", text: "Hello"}], + }); + + return turnStartSpy.mock.calls[0]![0].sandboxPolicy; + } + + it('enables network access when configured', async () => { + await expect(promptWithConfig(true)).resolves.toEqual({ + type: "workspaceWrite", + writableRoots: ["/workspace/extra"], + networkAccess: true, + excludeTmpdirEnvVar: false, + excludeSlashTmp: false, + }); + }); + + it('keeps the false default when the setting is absent', async () => { + const {mockFixture, turnStartSpy} = setupPromptFixture({additionalDirectories: ["/workspace/extra"]}); + + await mockFixture.getCodexAcpAgent().prompt({ + sessionId: "session-id", + prompt: [{type: "text", text: "Hello"}], + }); + + expect(turnStartSpy.mock.calls[0]![0].sandboxPolicy).toEqual({ + type: "workspaceWrite", + writableRoots: ["/workspace/extra"], + networkAccess: false, + excludeTmpdirEnvVar: false, + excludeSlashTmp: false, + }); + }); + + it('ignores non-boolean network access values', async () => { + await expect(promptWithConfig("true")).resolves.toMatchObject({ + type: "workspaceWrite", + networkAccess: false, + }); + }); + + it('preserves an explicit false setting', async () => { + await expect(promptWithConfig(false)).resolves.toMatchObject({ + type: "workspaceWrite", + networkAccess: false, + }); + }); + + it('leaves Agent Full Access policy unchanged', async () => { + await expect(promptWithConfig(true, {agentMode: AgentMode.AgentFullAccess})).resolves.toEqual({ + type: "dangerFullAccess", + }); + }); + }); + function loadNotifications(){ //TODO collect logs form dev run and then load them from file to speedup const serverNotifications: ServerNotification[] = [ @@ -3114,8 +3180,8 @@ describe('ACP server test', { timeout: 40_000 }, () => { * Sets up a mock fixture with turnStart/awaitTurnCompleted spied on, * and a given session state. Returns the fixture and turnStart spy. */ - function setupPromptFixture(sessionOverrides?: Partial) { - const mockFixture = createCodexMockTestFixture(); + function setupPromptFixture(sessionOverrides?: Partial, codexConfig?: Parameters[0]) { + const mockFixture = createCodexMockTestFixture(codexConfig); const sessionState = createTestSessionState(sessionOverrides); const turnStartSpy = vi.spyOn(mockFixture.getCodexAppServerClient(), "turnStart").mockResolvedValue({ turn: { diff --git a/src/__tests__/acp-test-utils.ts b/src/__tests__/acp-test-utils.ts index de4ad962..6b2d842c 100644 --- a/src/__tests__/acp-test-utils.ts +++ b/src/__tests__/acp-test-utils.ts @@ -1,6 +1,6 @@ import * as acp from "@agentclientprotocol/sdk"; import type {CreateElicitationResponse, McpServerStdio, RequestPermissionResponse} from "@agentclientprotocol/sdk"; -import {CodexAcpClient} from '../CodexAcpClient'; +import {CodexAcpClient, type JsonObject} from '../CodexAcpClient'; import {CodexAppServerClient, type CodexConnectionEvent} from '../CodexAppServerClient'; import {startCodexConnection} from "../CodexJsonRpcConnection"; import {CodexAcpServer, type SessionState} from "../CodexAcpServer"; @@ -85,6 +85,7 @@ export interface ConnectionConfig { connection: MessageConnection; getExitCode: () => number | null; acpConnection?: AcpConnectionConfig; + codexConfig?: JsonObject; } export function createBaseTestFixture(config: ConnectionConfig): TestFixture { @@ -97,7 +98,7 @@ export function createBaseTestFixture(config: ConnectionConfig): TestFixture { }); const codexAppServerClient = new CodexAppServerClient(config.connection); - const codexAcpClient = new CodexAcpClient(codexAppServerClient); + const codexAcpClient = new CodexAcpClient(codexAppServerClient, config.codexConfig); const codexAcpAgent = new CodexAcpServer(acpConnection, codexAcpClient, undefined, config.getExitCode); const transportEvents: CodexConnectionEvent[] = []; @@ -255,7 +256,7 @@ export interface CodexMockTestFixture extends TestFixture { * Provides `sendServerRequest()` to simulate server-initiated requests (e.g., approval requests). * Provides `setPermissionResponse()` to control ACP permission dialog responses. */ -export function createCodexMockTestFixture(): CodexMockTestFixture { +export function createCodexMockTestFixture(codexConfig?: JsonObject): CodexMockTestFixture { let unhandledNotificationHandler: ((notification: any) => void) | null = null; const requestHandlers = new Map Promise>(); @@ -303,6 +304,7 @@ export function createCodexMockTestFixture(): CodexMockTestFixture { const baseFixture = createBaseTestFixture({ connection: mockCodexConnection, getExitCode: () => null, + ...(codexConfig === undefined ? {} : {codexConfig}), acpConnection: { connection: acpConnection, events: acpConnectionEvents,