Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions readme-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
24 changes: 23 additions & 1 deletion src/CodexAcpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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));
}
Expand Down
70 changes: 68 additions & 2 deletions src/__tests__/CodexACPAgent/CodexAcpClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<SessionState> = {},
) {
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[] = [
Expand Down Expand Up @@ -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<SessionState>) {
const mockFixture = createCodexMockTestFixture();
function setupPromptFixture(sessionOverrides?: Partial<SessionState>, codexConfig?: Parameters<typeof createCodexMockTestFixture>[0]) {
const mockFixture = createCodexMockTestFixture(codexConfig);
const sessionState = createTestSessionState(sessionOverrides);
const turnStartSpy = vi.spyOn(mockFixture.getCodexAppServerClient(), "turnStart").mockResolvedValue({
turn: {
Expand Down
8 changes: 5 additions & 3 deletions src/__tests__/acp-test-utils.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -85,6 +85,7 @@ export interface ConnectionConfig {
connection: MessageConnection;
getExitCode: () => number | null;
acpConnection?: AcpConnectionConfig;
codexConfig?: JsonObject;
}

export function createBaseTestFixture(config: ConnectionConfig): TestFixture {
Expand All @@ -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[] = [];
Expand Down Expand Up @@ -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<string, (params: unknown) => Promise<unknown>>();

Expand Down Expand Up @@ -303,6 +304,7 @@ export function createCodexMockTestFixture(): CodexMockTestFixture {
const baseFixture = createBaseTestFixture({
connection: mockCodexConnection,
getExitCode: () => null,
...(codexConfig === undefined ? {} : {codexConfig}),
acpConnection: {
connection: acpConnection,
events: acpConnectionEvents,
Expand Down