From 941a5b5e9f0bd081335792360aef3efea351516f Mon Sep 17 00:00:00 2001 From: zfy0701 <1646270+zfy0701@users.noreply.github.com> Date: Sun, 2 Aug 2026 16:58:09 +0800 Subject: [PATCH] feat(sandbox): support launch-configured permission profiles --- readme-dev.md | 14 +++++ src/CodexAcpClient.ts | 59 +++++++++++++++++- src/CodexAcpServer.ts | 10 ++- src/CodexAppServerClient.ts | 31 ++++++++-- src/CodexJsonRpcConnection.ts | 13 ++-- src/PermissionProfileConfig.ts | 41 ++++++++++++ .../CodexACPAgent/CodexAcpClient.test.ts | 62 ++++++++++++++++++- .../session-config-options.test.ts | 16 ++++- src/__tests__/acp-test-utils.ts | 30 +++++++-- src/index.ts | 15 ++++- 10 files changed, 265 insertions(+), 26 deletions(-) create mode 100644 src/PermissionProfileConfig.ts diff --git a/readme-dev.md b/readme-dev.md index bc147807..b5198d26 100644 --- a/readme-dev.md +++ b/readme-dev.md @@ -13,6 +13,20 @@ 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. +### External permission profiles + +A trusted runtime launcher may set `CODEX_ACP_PERMISSION_PROFILE_CONFIG` to a JSON +object containing `configOverrides` for the long-lived Codex app-server and a +`modeProfiles` mapping for the `read-only`, `agent`, and `agent-full-access` ACP +modes. The adapter treats profile definitions as opaque operator policy. + +When configured, codex-acp selects the matching profile on thread start, resume, +and mode changes, preserves ACP additional workspace roots, and omits the legacy +per-turn `sandboxPolicy` that would otherwise override the selected profile. The +launch variable is removed from the Codex child environment after it is parsed. +Malformed or incomplete mappings fail startup instead of falling back to legacy +sandbox behavior. + ### Quick start #### Develop on Windows? diff --git a/src/CodexAcpClient.ts b/src/CodexAcpClient.ts index 0c7af207..ae7cfa48 100644 --- a/src/CodexAcpClient.ts +++ b/src/CodexAcpClient.ts @@ -44,6 +44,10 @@ import packageJson from "../package.json"; import type {AuthenticationStatusResponse} from "./AcpExtensions"; import {createCodexCollaborationMode} from "./CollaborationModeConfig"; import type {ModeKind} from "./app-server/ModeKind"; +import { + permissionProfileForMode, + type PermissionProfileConfig, +} from "./PermissionProfileConfig"; /** * Well-known provider id for the client-configurable custom LLM gateway. @@ -68,6 +72,7 @@ export class CodexAcpClient { private readonly codexClient: CodexAppServerClient; private readonly config: JsonObject; private readonly modelProvider: string | null; + private readonly permissionProfileConfig: PermissionProfileConfig | undefined; private gatewayConfig: GatewayConfig | null; private pendingLoginCompleted: Promise | null = null; private pendingAccountUpdated: Promise | null = null; @@ -76,10 +81,16 @@ export class CodexAcpClient { private configPath: string | null = null; - constructor(codexClient: CodexAppServerClient, codexConfig?: JsonObject, modelProvider?: string) { + constructor( + codexClient: CodexAppServerClient, + codexConfig?: JsonObject, + modelProvider?: string, + permissionProfileConfig?: PermissionProfileConfig, + ) { this.codexClient = codexClient; this.config = codexConfig ?? {}; this.modelProvider = modelProvider ?? null; + this.permissionProfileConfig = permissionProfileConfig; this.gatewayConfig = null; } @@ -328,6 +339,7 @@ export class CodexAcpClient { async resumeSession(request: acp.ResumeSessionRequest, onSubscribed?: () => void): Promise { const additionalDirectories = readAdditionalDirectories(request.cwd, request.additionalDirectories, request._meta); + const initialAgentMode = AgentMode.getInitialAgentMode(); await this.refreshSkills(request.cwd, additionalDirectories); const response = await this.codexClient.threadResume({ @@ -335,6 +347,7 @@ export class CodexAcpClient { cwd: request.cwd, modelProvider: await this.getResumeModelProvider(), threadId: request.sessionId, + ...this.permissionProfileSelection(initialAgentMode, request.cwd, additionalDirectories), }); onSubscribed?.(); const codexModels = await this.fetchAvailableModels(); @@ -352,6 +365,7 @@ export class CodexAcpClient { async loadSession(request: acp.LoadSessionRequest, onSubscribed?: () => void): Promise { const additionalDirectories = readAdditionalDirectories(request.cwd, request.additionalDirectories, request._meta); + const initialAgentMode = AgentMode.getInitialAgentMode(); await this.refreshSkills(request.cwd, additionalDirectories); const response = await this.codexClient.threadResume({ @@ -359,6 +373,7 @@ export class CodexAcpClient { cwd: request.cwd, modelProvider: await this.getResumeModelProvider(), threadId: request.sessionId, + ...this.permissionProfileSelection(initialAgentMode, request.cwd, additionalDirectories), }); onSubscribed?.(); const historyResponse = await this.codexClient.threadRead({ @@ -381,12 +396,14 @@ export class CodexAcpClient { async newSession(request: acp.NewSessionRequest): Promise { const additionalDirectories = readAdditionalDirectories(request.cwd, request.additionalDirectories, request._meta); + const initialAgentMode = AgentMode.getInitialAgentMode(); await this.refreshSkills(request.cwd, additionalDirectories); const response = await this.codexClient.threadStart({ config: await this.createSessionConfig(request.cwd, additionalDirectories, request.mcpServers), modelProvider: this.getModelProvider(), cwd: request.cwd, + ...this.permissionProfileSelection(initialAgentMode, request.cwd, additionalDirectories), }); const codexModels = await this.fetchAvailableModels(); @@ -413,6 +430,20 @@ export class CodexAcpClient { } } + private permissionProfileSelection( + agentMode: AgentMode, + cwd: string, + additionalDirectories: string[], + ): {approvalPolicy: AgentMode["approvalPolicy"]; permissions: string; runtimeWorkspaceRoots: string[]} | Record { + const config = this.permissionProfileConfig; + if (!config) return {}; + return { + approvalPolicy: agentMode.approvalPolicy, + permissions: permissionProfileForMode(config, agentMode.id), + runtimeWorkspaceRoots: sessionRoots(cwd, additionalDirectories), + }; + } + async deleteSession(sessionId: string): Promise { await this.codexClient.threadArchive({threadId: sessionId}); } @@ -489,7 +520,7 @@ export class CodexAcpClient { private async createSessionConfig( projectPath: string, additionalDirectories: string[], - mcpServers: Array + mcpServers: Array, ): Promise { const sessionRoots = [projectPath, ...additionalDirectories]; const mergedConfig = { @@ -700,11 +731,16 @@ export class CodexAcpClient { if (shouldCancel?.()) { return null; } + const sandboxSelection = this.permissionProfileConfig ? { + runtimeWorkspaceRoots: sessionRoots(cwd, additionalDirectories), + } : { + sandboxPolicy: addAdditionalDirectoriesToSandboxPolicy(agentMode.sandboxPolicy, additionalDirectories), + }; return await this.codexClient.runTurn({ threadId: request.sessionId, input: input, approvalPolicy: agentMode.approvalPolicy, - sandboxPolicy: addAdditionalDirectoriesToSandboxPolicy(agentMode.sandboxPolicy, additionalDirectories), + ...sandboxSelection, summary: disableSummary ? "none" : "auto", effort: effort, model: modelId.model, @@ -712,6 +748,19 @@ export class CodexAcpClient { }, onTurnStarted); } + async setAgentMode( + sessionId: string, + agentMode: AgentMode, + ): Promise { + const config = this.permissionProfileConfig; + if (!config) return; + await this.codexClient.threadSettingsUpdate({ + threadId: sessionId, + approvalPolicy: agentMode.approvalPolicy, + permissions: permissionProfileForMode(config, agentMode.id), + }); + } + async setCollaborationMode(sessionId: string, mode: ModeKind, currentModelId: string): Promise { await this.codexClient.threadSettingsUpdate({ threadId: sessionId, @@ -1066,6 +1115,10 @@ function uniqueStrings(values: string[]): string[] { return Array.from(new Set(values)); } +function sessionRoots(cwd: string, additionalDirectories: string[]): string[] { + return uniqueStrings([cwd, ...additionalDirectories]); +} + function arraysEqual(left: string[], right: string[]): boolean { if (left.length !== right.length) { return false; diff --git a/src/CodexAcpServer.ts b/src/CodexAcpServer.ts index 0ed48705..77429a9f 100644 --- a/src/CodexAcpServer.ts +++ b/src/CodexAcpServer.ts @@ -734,7 +734,7 @@ export class CodexAcpServer { const sessionState = this.sessions.get(_params.sessionId); if (!sessionState) throw new Error(`Session ${_params.sessionId} not found`); - this.applyModeChange(sessionState, _params.modeId); + await this.applyModeChange(sessionState, _params.modeId); return {}; } @@ -759,7 +759,7 @@ export class CodexAcpServer { this.applyFastModeChange(sessionState, params); break; case MODE_CONFIG_ID: - this.applyModeChange(sessionState, this.stringConfigValue(params)); + await this.applyModeChange(sessionState, this.stringConfigValue(params)); break; case COLLABORATION_MODE_CONFIG_ID: await this.applyCollaborationModeChange(sessionState, this.stringConfigValue(params)); @@ -794,11 +794,15 @@ export class CodexAcpServer { return params.value; } - private applyModeChange(sessionState: SessionState, value: string): void { + private async applyModeChange(sessionState: SessionState, value: string): Promise { const newMode = AgentMode.find(value); if (!newMode) { throw RequestError.invalidParams(); } + await this.codexAcpClient.setAgentMode( + sessionState.sessionId, + newMode, + ); sessionState.agentMode = newMode; } diff --git a/src/CodexAppServerClient.ts b/src/CodexAppServerClient.ts index eb26c83f..ea9c71a8 100644 --- a/src/CodexAppServerClient.ts +++ b/src/CodexAppServerClient.ts @@ -6,6 +6,7 @@ import type { ServerNotification } from "./app-server"; import type { + AskForApproval, ConfigReadParams, ConfigReadResponse, GetAccountParams, @@ -266,11 +267,11 @@ export class CodexAppServerClient { return await this.sendRequest({ method: "initialize", params: params }); } - async turnStart(params: TurnStartParams): Promise { + async turnStart(params: ExperimentalTurnStartParams): Promise { return await this.sendRequest({ method: "turn/start", params: params }); } - async runTurn(params: TurnStartParams, onTurnStarted?: (turnId: string) => void): Promise { + async runTurn(params: ExperimentalTurnStartParams, onTurnStarted?: (turnId: string) => void): Promise { const capturedCompletions: Array = []; const releaseCapture = this.captureTurnCompletions(params.threadId, (event) => { capturedCompletions.push(event); @@ -520,11 +521,11 @@ export class CodexAppServerClient { this.staleTurnIds.set(threadId, threadStaleTurns); } - async threadStart(params: ThreadStartParams): Promise { + async threadStart(params: ExperimentalThreadStartParams): Promise { return await this.sendRequest({ method: "thread/start", params: params }); } - async threadResume(params: ThreadResumeParams): Promise { + async threadResume(params: ExperimentalThreadResumeParams): Promise { return await this.sendRequest({ method: "thread/resume", params: params }); } @@ -976,7 +977,8 @@ type DistributiveOmit = T extends any export interface ExperimentalThreadSettingsUpdateParams { threadId: string; - collaborationMode: { + approvalPolicy?: AskForApproval; + collaborationMode?: { mode: "default" | "plan"; settings: { model: string; @@ -984,8 +986,27 @@ export interface ExperimentalThreadSettingsUpdateParams { developer_instructions: string | null; }; }; + permissions?: string; } +// Codex's generated public TypeScript schema omits fields gated by ExperimentalApi. +// This adapter opts into that API during initialize, so keep the small wire extension +// local until those fields are emitted by `codex app-server generate-ts`. +export type ExperimentalThreadStartParams = ThreadStartParams & { + permissions?: string; + runtimeWorkspaceRoots?: string[]; +}; + +export type ExperimentalThreadResumeParams = ThreadResumeParams & { + permissions?: string; + runtimeWorkspaceRoots?: string[]; +}; + +export type ExperimentalTurnStartParams = TurnStartParams & { + permissions?: string; + runtimeWorkspaceRoots?: string[]; +}; + type McpServerStartupSnapshot = { status: McpServerStartupState; error: string | null; diff --git a/src/CodexJsonRpcConnection.ts b/src/CodexJsonRpcConnection.ts index 0f79117a..4d84b068 100644 --- a/src/CodexJsonRpcConnection.ts +++ b/src/CodexJsonRpcConnection.ts @@ -12,17 +12,20 @@ export interface CodexConnection { readonly process: ChildProcessWithoutNullStreams; } -export function startCodexConnection(codexPath?: string, env?: NodeJS.ProcessEnv): CodexConnection { +export function startCodexConnection( + codexPath?: string, + env?: NodeJS.ProcessEnv, + configOverrides: string[] = [], +): CodexConnection { const spawnEnv = env ?? process.env; + const args = ["app-server", ...configOverrides.flatMap((override) => ["-c", override])]; let codex: ChildProcessWithoutNullStreams; if (codexPath) { - codex = process.platform === 'win32' - ? spawn(`"${codexPath}" app-server`, { shell: true, env: spawnEnv }) - : spawn(codexPath, ['app-server'], { env: spawnEnv }); + codex = spawn(codexPath, args, {env: spawnEnv, shell: process.platform === "win32"}); } else { const bundledCodexPath = createRequire(import.meta.url).resolve("@openai/codex/bin/codex.js"); - codex = spawn(process.execPath, [bundledCodexPath, 'app-server'], {env: spawnEnv}); + codex = spawn(process.execPath, [bundledCodexPath, ...args], {env: spawnEnv}); } attachLogs(codex); diff --git a/src/PermissionProfileConfig.ts b/src/PermissionProfileConfig.ts new file mode 100644 index 00000000..24559bb9 --- /dev/null +++ b/src/PermissionProfileConfig.ts @@ -0,0 +1,41 @@ +import {z} from "zod"; + +export const PERMISSION_PROFILE_CONFIG_ENV = "CODEX_ACP_PERMISSION_PROFILE_CONFIG"; + +const permissionProfileConfigSchema = z.object({ + configOverrides: z.array(z.string().min(1)).min(1), + modeProfiles: z.object({ + "read-only": z.string().min(1), + agent: z.string().min(1), + "agent-full-access": z.string().min(1), + }), +}); + +export type PermissionProfileConfig = z.infer; + +/** + * Read an operator-owned, launch-wide permission profile mapping. The adapter + * deliberately treats profile definitions as opaque Codex configuration: the + * trusted launcher owns policy construction, while codex-acp only selects the + * profile matching the active ACP mode. + */ +export function readPermissionProfileConfig( + env: NodeJS.ProcessEnv = process.env, +): PermissionProfileConfig | undefined { + const raw = env[PERMISSION_PROFILE_CONFIG_ENV]; + if (!raw) return undefined; + + let parsed: unknown; + try { + parsed = JSON.parse(raw); + } catch { + throw new Error(`${PERMISSION_PROFILE_CONFIG_ENV} must contain a JSON object`); + } + return permissionProfileConfigSchema.parse(parsed); +} + +export function permissionProfileForMode(config: PermissionProfileConfig, modeId: string): string { + const profile = config.modeProfiles[modeId as keyof PermissionProfileConfig["modeProfiles"]]; + if (!profile) throw new Error(`No permission profile configured for ACP mode ${modeId}`); + return profile; +} diff --git a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts index d58bd594..d5c74d8f 100644 --- a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts +++ b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts @@ -8,6 +8,8 @@ import { createTestFixture, createTestModel, createTestSessionState, + setupPromptTestSession, + TEST_PERMISSION_PROFILE_CONFIG, type TestFixture } from "../acp-test-utils"; import type {ServerNotification} from "../../app-server"; @@ -405,8 +407,40 @@ describe('ACP server test', { timeout: 40_000 }, () => { }); }); + it('selects external permission profiles without losing workspace roots', async () => { + const mockFixture = createCodexMockTestFixture(TEST_PERMISSION_PROFILE_CONFIG); + const codexAcpClient = mockFixture.getCodexAcpClient(); + const codexAppServerClient = mockFixture.getCodexAppServerClient(); + + vi.spyOn(codexAppServerClient, "skillsExtraRootsSet").mockResolvedValue(undefined); + vi.spyOn(codexAppServerClient, "listSkills").mockResolvedValue({data: []}); + const threadStartSpy = vi.spyOn(codexAppServerClient, "threadStart").mockResolvedValue({ + thread: {id: "thread-id"} as any, + model: "gpt-5", + reasoningEffort: "medium", + serviceTier: null, + } as any); + vi.spyOn(codexAppServerClient, "listModels").mockResolvedValue({ + data: [createTestModel({id: "gpt-5"})], + nextCursor: null, + }); + + const session = await codexAcpClient.newSession({ + cwd: "/workspace", + additionalDirectories: ["/workspace/extra"], + mcpServers: [], + }); + + const request = threadStartSpy.mock.calls[0]![0]; + expect(request).toMatchObject({ + approvalPolicy: "on-request", + permissions: "external-agent", + runtimeWorkspaceRoots: ["/workspace", "/workspace/extra"], + }); + }); + it('applies ACP additional directories to resumed and loaded sessions explicitly', async () => { - const mockFixture = createCodexMockTestFixture(); + const mockFixture = createCodexMockTestFixture(TEST_PERMISSION_PROFILE_CONFIG); const codexAcpClient = mockFixture.getCodexAcpClient(); const codexAppServerClient = mockFixture.getCodexAppServerClient(); @@ -440,6 +474,14 @@ describe('ACP server test', { timeout: 40_000 }, () => { expect(resumed.additionalDirectories).toEqual(["/workspace/resume-extra"]); expect(loaded.additionalDirectories).toEqual(["/workspace/load-extra"]); + expect(threadResumeSpy.mock.calls[0]![0]).toMatchObject({ + permissions: "external-agent", + runtimeWorkspaceRoots: ["/workspace", "/workspace/resume-extra"], + }); + expect(threadResumeSpy.mock.calls[1]![0]).toMatchObject({ + permissions: "external-agent", + runtimeWorkspaceRoots: ["/workspace", "/workspace/load-extra"], + }); expect(threadResumeSpy.mock.calls[0]![0].config?.["projects"]).toEqual({ "/workspace": {trust_level: "trusted"}, "/workspace/resume-extra": {trust_level: "trusted"}, @@ -848,6 +890,24 @@ describe('ACP server test', { timeout: 40_000 }, () => { }); }); + it('keeps the external profile sticky instead of sending a legacy turn sandbox policy', async () => { + const {mockFixture, turnStartSpy} = setupPromptTestSession({ + cwd: "/workspace", + additionalDirectories: ["/workspace/extra"], + }, TEST_PERMISSION_PROFILE_CONFIG); + + await mockFixture.getCodexAcpAgent().prompt({ + sessionId: "session-id", + prompt: [{type: "text", text: "Hello"}], + }); + + expect(turnStartSpy).toHaveBeenCalledWith(expect.objectContaining({ + runtimeWorkspaceRoots: ["/workspace", "/workspace/extra"], + })); + expect(turnStartSpy.mock.calls[0]![0]).not.toHaveProperty("permissions"); + expect(turnStartSpy.mock.calls[0]![0]).not.toHaveProperty("sandboxPolicy"); + }); + function loadNotifications(){ //TODO collect logs form dev run and then load them from file to speedup const serverNotifications: ServerNotification[] = [ diff --git a/src/__tests__/CodexACPAgent/session-config-options.test.ts b/src/__tests__/CodexACPAgent/session-config-options.test.ts index ab669916..e245cc65 100644 --- a/src/__tests__/CodexACPAgent/session-config-options.test.ts +++ b/src/__tests__/CodexACPAgent/session-config-options.test.ts @@ -1,5 +1,9 @@ import {describe, expect, it, vi} from "vitest"; -import {createCodexMockTestFixture, createTestModel} from "../acp-test-utils"; +import { + createCodexMockTestFixture, + createTestModel, + TEST_PERMISSION_PROFILE_CONFIG, +} from "../acp-test-utils"; import {AgentMode, MODE_CONFIG_ID} from "../../AgentMode"; import { MODEL_CONFIG_ID, @@ -36,7 +40,7 @@ function buildModels(): {fast: Model; slow: Model} { } async function createSession(currentModelId: string, availableModels: Array) { - const fixture = createCodexMockTestFixture(); + const fixture = createCodexMockTestFixture(TEST_PERMISSION_PROFILE_CONFIG); const codexAcpAgent = fixture.getCodexAcpAgent(); const codexAcpClient = fixture.getCodexAcpClient(); @@ -141,7 +145,8 @@ describe("Session config options", () => { it("changes the agent mode via setSessionConfigOption", async () => { const {fast} = buildModels(); - const {codexAcpAgent} = await createSession("fast-model[medium]", [fast]); + const {codexAcpAgent, codexAcpClient} = await createSession("fast-model[medium]", [fast]); + const update = vi.spyOn((codexAcpClient as any).codexClient, "threadSettingsUpdate").mockResolvedValue(undefined); const result = await codexAcpAgent.setSessionConfigOption({ sessionId: "session-id", @@ -150,6 +155,11 @@ describe("Session config options", () => { }); expect(codexAcpAgent.getSessionState("session-id").agentMode).toBe(AgentMode.ReadOnly); + expect(update).toHaveBeenCalledWith({ + threadId: "session-id", + approvalPolicy: "on-request", + permissions: "external-read-only", + }); const modeOption = result.configOptions?.find(o => o.id === MODE_CONFIG_ID); expect((modeOption as any).currentValue).toBe(AgentMode.ReadOnly.id); }); diff --git a/src/__tests__/acp-test-utils.ts b/src/__tests__/acp-test-utils.ts index de4ad962..f50e2e06 100644 --- a/src/__tests__/acp-test-utils.ts +++ b/src/__tests__/acp-test-utils.ts @@ -14,6 +14,16 @@ import {AgentMode} from "../AgentMode"; import {DEFAULT_COLLABORATION_MODE} from "../CollaborationModeConfig"; import {expect, vi} from "vitest"; import type {Model, ReasoningEffortOption} from "../app-server/v2"; +import type {PermissionProfileConfig} from "../PermissionProfileConfig"; + +export const TEST_PERMISSION_PROFILE_CONFIG: PermissionProfileConfig = { + configOverrides: ["permissions.external-agent.extends=\":workspace\""], + modeProfiles: { + "read-only": "external-read-only", + agent: "external-agent", + "agent-full-access": "external-full-access", + }, +}; export type MethodCallEvent = { method: string; args: any[] }; @@ -85,6 +95,7 @@ export interface ConnectionConfig { connection: MessageConnection; getExitCode: () => number | null; acpConnection?: AcpConnectionConfig; + permissionProfileConfig?: PermissionProfileConfig; } export function createBaseTestFixture(config: ConnectionConfig): TestFixture { @@ -97,7 +108,12 @@ export function createBaseTestFixture(config: ConnectionConfig): TestFixture { }); const codexAppServerClient = new CodexAppServerClient(config.connection); - const codexAcpClient = new CodexAcpClient(codexAppServerClient); + const codexAcpClient = new CodexAcpClient( + codexAppServerClient, + undefined, + undefined, + config.permissionProfileConfig, + ); const codexAcpAgent = new CodexAcpServer(acpConnection, codexAcpClient, undefined, config.getExitCode); const transportEvents: CodexConnectionEvent[] = []; @@ -255,7 +271,9 @@ 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( + permissionProfileConfig?: PermissionProfileConfig, +): CodexMockTestFixture { let unhandledNotificationHandler: ((notification: any) => void) | null = null; const requestHandlers = new Map Promise>(); @@ -303,6 +321,7 @@ export function createCodexMockTestFixture(): CodexMockTestFixture { const baseFixture = createBaseTestFixture({ connection: mockCodexConnection, getExitCode: () => null, + ...(permissionProfileConfig ? {permissionProfileConfig} : {}), acpConnection: { connection: acpConnection, events: acpConnectionEvents, @@ -419,8 +438,11 @@ export function createTestModel(overrides?: Partial): Model { }; } -export function setupPromptTestSession(sessionOverrides?: Partial) { - const mockFixture = createCodexMockTestFixture(); +export function setupPromptTestSession( + sessionOverrides?: Partial, + permissionProfileConfig?: PermissionProfileConfig, +) { + const mockFixture = createCodexMockTestFixture(permissionProfileConfig); const sessionState = createTestSessionState(sessionOverrides); vi.spyOn(mockFixture.getCodexAcpAgent(), "getSessionState").mockReturnValue(sessionState); diff --git a/src/index.ts b/src/index.ts index 014801ff..bb8db27b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,6 +16,10 @@ import { GOAL_CONTROL_METHOD, LEGACY_SET_SESSION_MODEL_METHOD, SESSION_STEERING_METHOD, } from "./AcpExtensions"; +import { + PERMISSION_PROFILE_CONFIG_ENV, + readPermissionProfileConfig, +} from "./PermissionProfileConfig"; const emptyExtensionParamsParser = z.preprocess( (params) => params ?? {}, @@ -70,6 +74,9 @@ function startAcpServer() { const config = configString ? JSON.parse(configString) : undefined; const parsedAuthRequest = authRequestString ? JSON.parse(authRequestString) : undefined; const defaultAuthRequest = parsedAuthRequest && isCodexAuthRequest(parsedAuthRequest) ? parsedAuthRequest : undefined; + const permissionProfileConfig = readPermissionProfileConfig(); + const codexEnv = {...process.env}; + delete codexEnv[PERMISSION_PROFILE_CONFIG_ENV]; logger.log("Startup", { name: packageJson.name, @@ -81,7 +88,11 @@ function startAcpServer() { defaultAuthRequest: defaultAuthRequest ?? null, }); - const codexConnection = startCodexConnection(codexPath); + const codexConnection = startCodexConnection( + codexPath, + codexEnv, + permissionProfileConfig?.configOverrides, + ); const maxStderrTailChars = 2 * 1024; let stderr = ""; @@ -104,7 +115,7 @@ function startAcpServer() { function createAgent(connection: acp.AgentContext): CodexAcpServer { const appServerClient = new CodexAppServerClient(codexConnection.connection); - const codexClient = new CodexAcpClient(appServerClient, config, modelProvider); + const codexClient = new CodexAcpClient(appServerClient, config, modelProvider, permissionProfileConfig); return new CodexAcpServer(connection, codexClient, defaultAuthRequest, () => codexConnection.process.exitCode, () => stderr); }