diff --git a/docs/permission-profiles.md b/docs/permission-profiles.md index c1514bc2a..16dba7c81 100644 --- a/docs/permission-profiles.md +++ b/docs/permission-profiles.md @@ -14,9 +14,9 @@ When custom profiles exist, the config option groups entries under **Sandbox Mod ## Initial and restored modes -New, loaded, resumed, and forked sessions select the active custom profile reported by Codex when that profile is available. Built-in or unavailable active profiles keep the adapter's default `agent` mode (Auto review). +New, loaded, resumed, and forked sessions select the active custom profile reported by Codex when that profile is available. Otherwise the starting mode is the built-in mode that reproduces the sandbox and approval policy Codex resolved for the thread: `dangerFullAccess` with `never` selects `agent-full-access`, `readOnly` with `on-request` selects `read-only`, and `workspaceWrite` with `on-request` selects `agent`. A resolved pair that no built-in mode represents keeps the adapter's default `agent` mode (Auto review). -`INITIAL_AGENT_MODE` overrides the active profile. Set it to an available mode ID, including `permission-profile:`. An unavailable override falls back to the default mode. +`INITIAL_AGENT_MODE` overrides both the active profile and the resolved policy. Set it to an available mode ID, including `permission-profile:`. An unavailable override falls back to the default mode. ## Applying permissions diff --git a/src/AgentMode.ts b/src/AgentMode.ts index ddf69fa50..1b9765266 100644 --- a/src/AgentMode.ts +++ b/src/AgentMode.ts @@ -13,6 +13,15 @@ const PERMISSION_PROFILE_MODE_PREFIX = "permission-profile:"; type AgentModeKind = "plan" | "auto_review" | "standard" | "full_access"; +/** + * The permissions Codex resolved for a thread, as reported by `thread/start`, + * `thread/resume`, and `thread/fork`. A field is null when the response omitted it. + */ +export type ResolvedPermissions = { + approvalPolicy: AskForApproval | null; + sandboxPolicy: SandboxPolicy | null; +}; + export class AgentMode { readonly id: string; readonly name: string; @@ -193,6 +202,7 @@ export class AgentMode { static getInitialAgentMode( availableModes: AgentMode[] = AgentMode.all(), activePermissionProfileId: string | null = null, + resolvedPermissions: ResolvedPermissions | null = null, ): AgentMode { const predefinedAgentMode = process.env["INITIAL_AGENT_MODE"]; if (predefinedAgentMode) { @@ -204,6 +214,34 @@ export class AgentMode { return profileMode; } } + if (resolvedPermissions !== null) { + const resolvedMode = AgentMode.fromResolvedPermissions(resolvedPermissions, availableModes); + if (resolvedMode !== null) { + return resolvedMode; + } + } return AgentMode.DEFAULT_AGENT_MODE; } + + /** The built-in mode that matches the sandbox and approval policy Codex resolved for a thread, or null when none does. */ + private static fromResolvedPermissions( + {approvalPolicy, sandboxPolicy}: ResolvedPermissions, + availableModes: AgentMode[], + ): AgentMode | null { + const find = (mode: AgentMode): AgentMode | null => + availableModes.find(candidate => candidate.id === mode.id) ?? null; + if (sandboxPolicy === null) { + return null; + } + switch (sandboxPolicy.type) { + case "dangerFullAccess": + return approvalPolicy === "never" ? find(AgentMode.AgentFullAccess) : null; + case "readOnly": + return approvalPolicy === "on-request" ? find(AgentMode.ReadOnly) : null; + case "workspaceWrite": + return approvalPolicy === "on-request" ? find(AgentMode.Agent) : null; + case "externalSandbox": + return null; + } + } } diff --git a/src/CodexAcpClient.ts b/src/CodexAcpClient.ts index b7271800b..ada64af19 100644 --- a/src/CodexAcpClient.ts +++ b/src/CodexAcpClient.ts @@ -26,7 +26,7 @@ import type { import type {ServiceTier} from "./app-server/ServiceTier"; import type {JsonValue} from "./app-server/serde_json/JsonValue"; import {ModelId} from "./ModelId"; -import {AgentMode} from "./AgentMode"; +import {AgentMode, type ResolvedPermissions} from "./AgentMode"; import path from "node:path"; import {isDeepStrictEqual} from "node:util"; import {logger} from "./Logger"; @@ -113,6 +113,8 @@ type ResumedThread = { materialized: boolean; /** The mode that the resume response reports. Null when the thread has no rollout yet. */ collaborationMode: ModeKind | null; + /** The permissions the resume response reports. Null when `thread/read` answered instead. */ + resolvedPermissions: ResolvedPermissions | null; }; /** @@ -653,6 +655,10 @@ export class CodexAcpClient { itemsBackwardsCursor: response.itemsBackwardsCursor ?? null, materialized: true, collaborationMode: response.collaborationMode?.mode ?? null, + resolvedPermissions: { + approvalPolicy: response.approvalPolicy ?? null, + sandboxPolicy: response.sandbox ?? null, + }, }; } catch (err) { if (isThreadActiveWriterError(err)) throw threadActiveWriterRequestError(params.threadId, err); @@ -680,6 +686,7 @@ export class CodexAcpClient { itemsBackwardsCursor: null, materialized: false, collaborationMode: null, + resolvedPermissions: null, }; } } @@ -720,6 +727,7 @@ export class CodexAcpClient { skippedMcpServers, ...(instructions ? {instructions} : {}), activePermissionProfileId: response.materialized ? response.activePermissionProfileId : defaultPermissionProfileId ?? null, + ...(response.resolvedPermissions ? {resolvedPermissions: response.resolvedPermissions} : {}), ...permissionProfileMetadata, } } @@ -805,6 +813,7 @@ export class CodexAcpClient { skippedMcpServers, ...(instructions ? {instructions} : {}), activePermissionProfileId: response.materialized ? response.activePermissionProfileId : defaultPermissionProfileId ?? null, + ...(response.resolvedPermissions ? {resolvedPermissions: response.resolvedPermissions} : {}), ...permissionProfileMetadata, }; } @@ -888,6 +897,10 @@ export class CodexAcpClient { skippedMcpServers, ...(instructions ? {instructions} : {}), activePermissionProfileId: response.activePermissionProfile?.id ?? null, + resolvedPermissions: { + approvalPolicy: response.approvalPolicy ?? null, + sandboxPolicy: response.sandbox ?? null, + }, ...permissionProfileMetadata, }; } diff --git a/src/CodexAcpServer.ts b/src/CodexAcpServer.ts index dcc15a8a9..9892bb9bf 100644 --- a/src/CodexAcpServer.ts +++ b/src/CodexAcpServer.ts @@ -1395,6 +1395,7 @@ export class CodexAcpServer { agentMode: AgentMode.getInitialAgentMode( availableAgentModes, sessionMetadata.activePermissionProfileId, + sessionMetadata.resolvedPermissions ?? null, ), availableAgentModes, collaborationMode: sessionMetadata.collaborationMode, @@ -3630,6 +3631,7 @@ export class CodexAcpServer { agentMode: AgentMode.getInitialAgentMode( availableAgentModes, sessionMetadata.activePermissionProfileId, + sessionMetadata.resolvedPermissions ?? null, ), availableAgentModes, collaborationMode: sessionMetadata.collaborationMode, diff --git a/src/SessionFork.ts b/src/SessionFork.ts index f4823e93a..2b05d017c 100644 --- a/src/SessionFork.ts +++ b/src/SessionFork.ts @@ -71,6 +71,10 @@ export async function forkSession( additionalDirectories, skippedMcpServers, activePermissionProfileId: response.activePermissionProfile?.id ?? null, + resolvedPermissions: { + approvalPolicy: response.approvalPolicy ?? null, + sandboxPolicy: response.sandbox ?? null, + }, ...permissionProfileMetadata, ...(instructions ? {instructions} : {}), }; diff --git a/src/SessionMetadata.ts b/src/SessionMetadata.ts index b56dfe85f..84f166695 100644 --- a/src/SessionMetadata.ts +++ b/src/SessionMetadata.ts @@ -8,6 +8,7 @@ import type { ThreadItemEntry, ThreadStatus, } from "./app-server/v2"; +import type {ResolvedPermissions} from "./AgentMode"; import type {SessionInstructions} from "./ClientSystemPrompt"; export type SessionMetadata = { @@ -23,6 +24,8 @@ export type SessionMetadata = { instructions?: SessionInstructions, sessionTitle?: string | null, activePermissionProfileId?: string | null, + /** The permissions Codex resolved for the thread; absent when its response reported none. */ + resolvedPermissions?: ResolvedPermissions, approvalPolicy?: AskForApproval, approvalsReviewer?: ApprovalsReviewer, permissionProfiles?: PermissionProfileSummary[], diff --git a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts index 251c99998..dd970bbf9 100644 --- a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts +++ b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts @@ -693,6 +693,53 @@ describe('ACP server test', { timeout: 40_000 }, () => { expect(codexAcpAgent.getSessionState("thread-id").agentMode).toMatchObject(expected); }); + function createResolvedPermissionsFixture() { + vi.stubEnv("INITIAL_AGENT_MODE", undefined); + const mockFixture = createCodexMockTestFixture(); + const codexAcpClient = mockFixture.getCodexAcpClient(); + const codexAcpAgent = mockFixture.getCodexAcpAgent(); + const codexAppServerClient = mockFixture.getCodexAppServerClient(); + + vi.spyOn(codexAcpClient, "readAuthRequirement").mockResolvedValue({required: false, account: null}); + vi.spyOn(codexAcpClient, "getAccount").mockResolvedValue({account: null, requiresOpenaiAuth: false}); + vi.spyOn(codexAppServerClient, "listSkills").mockResolvedValue({data: []}); + vi.spyOn(codexAppServerClient, "configRead").mockResolvedValue({config: {}} as any); + vi.spyOn(codexAppServerClient, "listPermissionProfiles").mockResolvedValue({data: [], nextCursor: null}); + const threadResponse = { + thread: {id: "thread-id", turns: [], status: {type: "idle"}}, + model: "gpt-5", + modelProvider: "openai", + reasoningEffort: "medium", + serviceTier: null, + activePermissionProfile: null, + approvalPolicy: "never", + approvalsReviewer: "user", + sandbox: {type: "dangerFullAccess"}, + collaborationMode: null, + itemsBackwardsCursor: null, + }; + vi.spyOn(codexAppServerClient, "threadStart").mockResolvedValue(threadResponse as any); + vi.spyOn(codexAppServerClient, "threadResume").mockResolvedValue(threadResponse as any); + vi.spyOn(codexAppServerClient, "threadFork").mockResolvedValue(threadResponse as any); + vi.spyOn(codexAppServerClient, "threadReadWithHistory").mockResolvedValue({thread: threadResponse.thread} as any); + vi.spyOn(codexAppServerClient, "listModels").mockResolvedValue({ + data: [createTestModel({id: "gpt-5"})], + nextCursor: null, + }); + return codexAcpAgent; + } + + it.each([ + "newSession", "loadSession", "resumeSession", "forkSession", + ] as const)('starts %s in the mode of the permissions Codex resolved', async method => { + const codexAcpAgent = createResolvedPermissionsFixture(); + + const session = await codexAcpAgent[method]({cwd: "/workspace", mcpServers: [], sessionId: "thread-id"}); + + expect(session.modes?.currentModeId).toBe(AgentMode.AgentFullAccess.id); + expect(codexAcpAgent.getSessionState("thread-id").agentMode).toBe(AgentMode.AgentFullAccess); + }); + it('applies ACP additional directories to resumed and loaded sessions explicitly', async () => { const mockFixture = createCodexMockTestFixture(); const codexAcpClient = mockFixture.getCodexAcpClient(); diff --git a/src/__tests__/CodexACPAgent/session-config-options.test.ts b/src/__tests__/CodexACPAgent/session-config-options.test.ts index f3d954c24..c236c804d 100644 --- a/src/__tests__/CodexACPAgent/session-config-options.test.ts +++ b/src/__tests__/CodexACPAgent/session-config-options.test.ts @@ -1,7 +1,7 @@ -import {afterEach, describe, expect, it, vi} from "vitest"; +import {afterEach, beforeEach, describe, expect, it, vi} from "vitest"; import * as acp from "@agentclientprotocol/sdk/unstable"; import {createCodexMockTestFixture, createTestModel} from "../acp-test-utils"; -import {AgentMode, MODE_CONFIG_ID} from "../../AgentMode"; +import {AgentMode, MODE_CONFIG_ID, type ResolvedPermissions} from "../../AgentMode"; import { MODEL_CONFIG_ID, REASONING_EFFORT_CONFIG_ID, @@ -47,6 +47,7 @@ async function createSession( activePermissionProfileId?: string | null; approvalPolicy?: "on-request" | "never"; approvalsReviewer?: "user" | "auto_review"; + resolvedPermissions?: ResolvedPermissions; } = {}, ) { const { @@ -56,6 +57,7 @@ async function createSession( activePermissionProfileId = null, approvalPolicy = "on-request", approvalsReviewer = "user", + resolvedPermissions, } = options; const fixture = createCodexMockTestFixture(); const codexAcpAgent = fixture.getCodexAcpAgent(); @@ -73,6 +75,7 @@ async function createSession( approvalPolicy, approvalsReviewer, permissionProfiles, + ...(resolvedPermissions ? {resolvedPermissions} : {}), }); if (clientCapabilities) { @@ -298,6 +301,97 @@ describe("Session config options", () => { }, ); + describe("initial mode from the permissions Codex resolved", () => { + beforeEach(() => { + vi.stubEnv("INITIAL_AGENT_MODE", undefined); + }); + + it.each<{ + case: string; + resolvedPermissions: ResolvedPermissions; + expectedModeId: string; + }>([ + { + case: "danger-full-access with no approvals", + resolvedPermissions: { + approvalPolicy: "never", + sandboxPolicy: {type: "dangerFullAccess"}, + }, + expectedModeId: AgentMode.AgentFullAccess.id, + }, + { + case: "read-only with on-request approvals", + resolvedPermissions: { + approvalPolicy: "on-request", + sandboxPolicy: {type: "readOnly", networkAccess: false}, + }, + expectedModeId: AgentMode.ReadOnly.id, + }, + { + case: "workspace-write with on-request approvals", + resolvedPermissions: { + approvalPolicy: "on-request", + sandboxPolicy: { + type: "workspaceWrite", + writableRoots: [], + networkAccess: false, + excludeTmpdirEnvVar: false, + excludeSlashTmp: false, + }, + }, + expectedModeId: AgentMode.Agent.id, + }, + { + case: "a pair no built-in mode represents", + resolvedPermissions: { + approvalPolicy: "never", + sandboxPolicy: { + type: "workspaceWrite", + writableRoots: [], + networkAccess: false, + excludeTmpdirEnvVar: false, + excludeSlashTmp: false, + }, + }, + expectedModeId: AgentMode.DEFAULT_AGENT_MODE.id, + }, + ])("starts a session from $case", async ({resolvedPermissions, expectedModeId}) => { + const {fast} = buildModels(); + const {response} = await createSession("fast-model[medium]", [fast], {resolvedPermissions}); + + expect(response.modes?.currentModeId).toBe(expectedModeId); + expect(response.configOptions?.find(option => option.id === MODE_CONFIG_ID)?.currentValue) + .toBe(expectedModeId); + }); + + it("keeps INITIAL_AGENT_MODE ahead of the resolved permissions", async () => { + vi.stubEnv("INITIAL_AGENT_MODE", AgentMode.ReadOnly.id); + const {fast} = buildModels(); + const {response} = await createSession("fast-model[medium]", [fast], { + resolvedPermissions: { + approvalPolicy: "never", + sandboxPolicy: {type: "dangerFullAccess"}, + }, + }); + + expect(response.modes?.currentModeId).toBe(AgentMode.ReadOnly.id); + }); + + it("keeps the active permission profile ahead of the resolved permissions", async () => { + const {fast} = buildModels(); + const {response} = await createSession("fast-model[medium]", [fast], { + activePermissionProfileId: "team-default", + permissionProfiles: [{id: "team-default", description: null, allowed: true}], + resolvedPermissions: { + approvalPolicy: "never", + sandboxPolicy: {type: "dangerFullAccess"}, + }, + }); + + expect(response.modes?.currentModeId).toBe("permission-profile:team-default"); + }); + }); + it.each([ {initialMode: "permission-profile:team-default", expectedMode: "permission-profile:team-default"}, {initialMode: "permission-profile:blocked", expectedMode: AgentMode.DEFAULT_AGENT_MODE.id}, diff --git a/src/__tests__/CodexACPAgent/session-lifecycle-v2.test.ts b/src/__tests__/CodexACPAgent/session-lifecycle-v2.test.ts index 35b333530..35595e199 100644 --- a/src/__tests__/CodexACPAgent/session-lifecycle-v2.test.ts +++ b/src/__tests__/CodexACPAgent/session-lifecycle-v2.test.ts @@ -7,7 +7,7 @@ import {CodexAcpServer, type SessionFailure} from '../../CodexAcpServer'; import {CodexAcpClient} from '../../CodexAcpClient'; import {CodexAppServerClient} from '../../CodexAppServerClient'; import type {PermissionProfileSummary, SkillsListParams, SkillsListResponse, Thread} from '../../app-server/v2'; -import {MODE_CONFIG_ID} from '../../AgentMode'; +import {AgentMode, MODE_CONFIG_ID} from '../../AgentMode'; import {FAST_MODE_CONFIG_ID} from '../../FastModeConfig'; import {createTestModel} from '../acp-test-utils'; import {createMockConnections} from './test-utils'; @@ -117,6 +117,12 @@ async function connectV2Client(options?: { activePermissionProfileId?: string; defaultPermissionProfileId?: string; serviceTier?: string | null; + /** The permissions the mocked thread responses report as resolved. */ + threadPermissions?: { + approvalPolicy: string; + approvalsReviewer: string; + sandbox: Record; + }; }) { const mocks = createMockConnections(); const codexRequests: Array<{method: string, params: unknown}> = []; @@ -149,6 +155,9 @@ async function connectV2Client(options?: { if (options?.activePermissionProfileId && ["thread/start", "thread/resume", "thread/fork"].includes(method)) { return {...response, activePermissionProfile: {id: options.activePermissionProfileId, extends: null}}; } + if (options?.threadPermissions && ["thread/start", "thread/resume", "thread/fork"].includes(method)) { + return {...response, ...options.threadPermissions}; + } return response; }); const codexAcpClient = new CodexAcpClient(new CodexAppServerClient(mocks.mockCodexConnection as any)); @@ -253,6 +262,29 @@ describe('Session lifecycle over ACP v2', () => { .toMatchObject({type: "select", currentValue: "permission-profile:team-default"}); }); + it.each([ + {operation: "new", replayFrom: undefined}, + {operation: "resume", replayFrom: null}, + {operation: "resume", replayFrom: {type: "start"}}, + {operation: "fork", replayFrom: undefined}, + ] as const)('starts over v2 $operation in the mode of the permissions Codex resolved', async ({operation, replayFrom}) => { + const {connection} = await connectV2Client({ + threadPermissions: { + approvalPolicy: "never", + approvalsReviewer: "user", + sandbox: {type: "dangerFullAccess"}, + }, + }); + closeClient = () => connection.close(); + + const response = await connection.agent.request(acpV2.methods.agent.session[operation], { + sessionId, cwd, ...(replayFrom === undefined ? {} : {replayFrom}), + }); + + expect(response.configOptions?.find(option => option.configId === MODE_CONFIG_ID)) + .toMatchObject({type: "select", currentValue: AgentMode.AgentFullAccess.id}); + }); + describe.each([ {operation: "new", replayFrom: undefined}, {operation: "resume", replayFrom: null}, diff --git a/src/__tests__/scenarios/data/air/history-replay.jsonl b/src/__tests__/scenarios/data/air/history-replay.jsonl index ee93b9d67..5fa53f4a5 100644 --- a/src/__tests__/scenarios/data/air/history-replay.jsonl +++ b/src/__tests__/scenarios/data/air/history-replay.jsonl @@ -1,6 +1,6 @@ {"direction":"response","method":"initialize","params":{"_meta":{"jetbrains":{"air":{"capabilities":["sessionFailure","diffPatch","agentFileChangeReport","customInstructions","nativeSubagentSessions","asyncTasks","recommendedValue","rawInputRendering","planContentDelta","codexHooks"],"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{},"systemPrompt":{},"title":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"},{"description":"Sign in to ChatGPT by opening a verification page and entering a one-time code","id":"chat-gpt-device-code","name":"ChatGPT (device code)"}],"protocolVersion":1}} {"direction":"notify","method":"_auth/status_update","params":{"authStatus":{"kind":"none","label":"Not logged in"}}} -{"direction":"response","method":"session/load","params":{"configOptions":[{"category":"mode","currentValue":"agent","description":"Approval and sandboxing preset for the session","id":"mode","name":"Mode","options":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","name":"Read-only","value":"read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","name":"Workspace access","value":"workspace-write"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","name":"Auto review","value":"agent"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","name":"Full access","value":"agent-full-access"}],"type":"select"},{"category":"collaboration_mode","currentValue":"default","description":"How Codex collaborates for subsequent turns","id":"collaboration_mode","name":"Collaboration mode","options":[{"name":"Default","value":"default"},{"description":"Plan before making changes","name":"Plan","value":"plan"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"model-id","version":1}}},"category":"model","currentValue":"model-id","description":"Model Codex uses for the session","id":"model","name":"Model","options":[{"description":"model-id model","name":"model-id","value":"model-id"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"medium","version":1}}},"category":"thought_level","currentValue":"medium","description":"How much reasoning effort the model should use","id":"reasoning_effort","name":"Reasoning effort","options":[{"description":"Balanced","name":"Medium","value":"medium"}],"type":"select"}],"models":{"availableModels":[{"description":"model-id model Balanced","modelId":"model-id[medium]","name":"model-id (medium)"}],"currentModelId":"model-id[medium]"},"modes":{"availableModes":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","id":"read-only","name":"Read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","id":"workspace-write","name":"Workspace access"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","id":"agent","name":"Auto review"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","id":"agent-full-access","name":"Full access"}],"currentModeId":"agent"}}} +{"direction":"response","method":"session/load","params":{"configOptions":[{"category":"mode","currentValue":"agent-full-access","description":"Approval and sandboxing preset for the session","id":"mode","name":"Mode","options":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","name":"Read-only","value":"read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","name":"Workspace access","value":"workspace-write"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","name":"Auto review","value":"agent"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","name":"Full access","value":"agent-full-access"}],"type":"select"},{"category":"collaboration_mode","currentValue":"default","description":"How Codex collaborates for subsequent turns","id":"collaboration_mode","name":"Collaboration mode","options":[{"name":"Default","value":"default"},{"description":"Plan before making changes","name":"Plan","value":"plan"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"model-id","version":1}}},"category":"model","currentValue":"model-id","description":"Model Codex uses for the session","id":"model","name":"Model","options":[{"description":"model-id model","name":"model-id","value":"model-id"}],"type":"select"},{"_meta":{"jetbrains":{"air":{"recommendedValue":"medium","version":1}}},"category":"thought_level","currentValue":"medium","description":"How much reasoning effort the model should use","id":"reasoning_effort","name":"Reasoning effort","options":[{"description":"Balanced","name":"Medium","value":"medium"}],"type":"select"}],"models":{"availableModels":[{"description":"model-id model Balanced","modelId":"model-id[medium]","name":"model-id (medium)"}],"currentModelId":"model-id[medium]"},"modes":{"availableModes":[{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Requires approval to edit files and access the internet.","id":"read-only","name":"Read-only"},{"_meta":{"jetbrains":{"air":{"kind":"standard","version":1}}},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","id":"workspace-write","name":"Workspace access"},{"_meta":{"jetbrains":{"air":{"kind":"auto_review","version":1}}},"description":"Only ask for actions detected as potentially unsafe","id":"agent","name":"Auto review"},{"_meta":{"jetbrains":{"air":{"kind":"full_access","version":1}}},"description":"Unrestricted access to the internet and any file on your computer","id":"agent-full-access","name":"Full access"}],"currentModeId":"agent-full-access"}}} {"direction":"notify","method":"session/update","params":{"sessionId":"session-1","update":{"availableCommands":[{"_meta":{"jetbrains":{"air":{"commandAction":{"configId":"collaboration_mode","kind":"setConfigOption","presentation":"state","resetValue":"default","value":"plan"},"version":1}}},"description":"Turn plan mode on.","input":null,"name":"plan"},{"description":"Show the status of the MCP servers, or reconnect them.","input":{"hint":"[reconnect]"},"name":"mcp"},{"description":"List available skills.","input":null,"name":"skills"},{"description":"Display session configuration and token usage.","input":null,"name":"status"},{"description":"Review uncommitted changes, or review with custom instructions.","input":{"hint":"optional review instructions"},"name":"review"},{"description":"Review changes relative to a base branch.","input":{"hint":"branch name"},"name":"review-branch"},{"description":"Review a specific commit.","input":{"hint":"commit sha"},"name":"review-commit"},{"description":"Summarize conversation to avoid hitting the context limit.","input":null,"name":"compact"},{"_meta":{"jetbrains":{"air":{"commandAction":{"kind":"prefixPrompt","presentation":"state"},"version":1}}},"description":"Set a goal to keep pursuing.","input":{"hint":"[|clear|pause|resume]"},"name":"goal"},{"description":"Rename the current session.","input":{"hint":"new name"},"name":"rename"},{"description":"Sign out of Codex. This option is available when you are logged in via ChatGPT.","input":null,"name":"logout"}],"sessionUpdate":"available_commands_update"}}} {"direction":"notify","method":"session/update","params":{"sessionId":"session-1","update":{"_meta":{"jetbrains":{"air":{"goal":null,"version":1}}},"sessionUpdate":"session_info_update"}}} {"direction":"notify","method":"session/update","params":{"sessionId":"session-1","update":{"sessionUpdate":"session_info_update","title":"Run the tests"}}} diff --git a/src/__tests__/scenarios/data/baseline/plain/history-replay.jsonl b/src/__tests__/scenarios/data/baseline/plain/history-replay.jsonl index 28789cf09..b7c378766 100644 --- a/src/__tests__/scenarios/data/baseline/plain/history-replay.jsonl +++ b/src/__tests__/scenarios/data/baseline/plain/history-replay.jsonl @@ -1,6 +1,6 @@ {"direction":"response","method":"initialize","params":{"_meta":{"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"jetbrains":{"air":{"capabilities":["sessionFailure","agentFileChangeReport","nativeSubagentSessions","asyncTasks","recommendedValue"],"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"}],"protocolVersion":1}} {"direction":"notify","method":"_auth/status_update","params":{"authStatus":{"kind":"none","label":"Not logged in"}}} -{"direction":"response","method":"session/load","params":{"configOptions":[{"category":"mode","currentValue":"agent","description":"Approval and sandboxing preset for the session","id":"mode","name":"Mode","options":[{"_meta":{"kind":"standard"},"description":"Requires approval to edit files and access the internet.","name":"Read-only","value":"read-only"},{"_meta":{"kind":"standard"},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","name":"Workspace access","value":"workspace-write"},{"_meta":{"kind":"auto_review"},"description":"Only ask for actions detected as potentially unsafe","name":"Auto review","value":"agent"},{"_meta":{"kind":"full_access"},"description":"Unrestricted access to the internet and any file on your computer","name":"Full access","value":"agent-full-access"}],"type":"select"},{"category":"collaboration_mode","currentValue":"default","description":"How Codex collaborates for subsequent turns","id":"collaboration_mode","name":"Collaboration mode","options":[{"name":"Default","value":"default"},{"description":"Plan before making changes","name":"Plan","value":"plan"}],"type":"select"},{"category":"model","currentValue":"model-id","description":"Model Codex uses for the session","id":"model","name":"Model","options":[{"description":"model-id model","name":"model-id","value":"model-id"}],"type":"select"},{"category":"thought_level","currentValue":"medium","description":"How much reasoning effort the model should use","id":"reasoning_effort","name":"Reasoning effort","options":[{"description":"Balanced","name":"Medium","value":"medium"}],"type":"select"}],"models":{"availableModels":[{"description":"model-id model Balanced","modelId":"model-id[medium]","name":"model-id (medium)"}],"currentModelId":"model-id[medium]"},"modes":{"availableModes":[{"_meta":{"kind":"standard"},"description":"Requires approval to edit files and access the internet.","id":"read-only","name":"Read-only"},{"_meta":{"kind":"standard"},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","id":"workspace-write","name":"Workspace access"},{"_meta":{"kind":"auto_review"},"description":"Only ask for actions detected as potentially unsafe","id":"agent","name":"Auto review"},{"_meta":{"kind":"full_access"},"description":"Unrestricted access to the internet and any file on your computer","id":"agent-full-access","name":"Full access"}],"currentModeId":"agent"}}} +{"direction":"response","method":"session/load","params":{"configOptions":[{"category":"mode","currentValue":"agent-full-access","description":"Approval and sandboxing preset for the session","id":"mode","name":"Mode","options":[{"_meta":{"kind":"standard"},"description":"Requires approval to edit files and access the internet.","name":"Read-only","value":"read-only"},{"_meta":{"kind":"standard"},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","name":"Workspace access","value":"workspace-write"},{"_meta":{"kind":"auto_review"},"description":"Only ask for actions detected as potentially unsafe","name":"Auto review","value":"agent"},{"_meta":{"kind":"full_access"},"description":"Unrestricted access to the internet and any file on your computer","name":"Full access","value":"agent-full-access"}],"type":"select"},{"category":"collaboration_mode","currentValue":"default","description":"How Codex collaborates for subsequent turns","id":"collaboration_mode","name":"Collaboration mode","options":[{"name":"Default","value":"default"},{"description":"Plan before making changes","name":"Plan","value":"plan"}],"type":"select"},{"category":"model","currentValue":"model-id","description":"Model Codex uses for the session","id":"model","name":"Model","options":[{"description":"model-id model","name":"model-id","value":"model-id"}],"type":"select"},{"category":"thought_level","currentValue":"medium","description":"How much reasoning effort the model should use","id":"reasoning_effort","name":"Reasoning effort","options":[{"description":"Balanced","name":"Medium","value":"medium"}],"type":"select"}],"models":{"availableModels":[{"description":"model-id model Balanced","modelId":"model-id[medium]","name":"model-id (medium)"}],"currentModelId":"model-id[medium]"},"modes":{"availableModes":[{"_meta":{"kind":"standard"},"description":"Requires approval to edit files and access the internet.","id":"read-only","name":"Read-only"},{"_meta":{"kind":"standard"},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","id":"workspace-write","name":"Workspace access"},{"_meta":{"kind":"auto_review"},"description":"Only ask for actions detected as potentially unsafe","id":"agent","name":"Auto review"},{"_meta":{"kind":"full_access"},"description":"Unrestricted access to the internet and any file on your computer","id":"agent-full-access","name":"Full access"}],"currentModeId":"agent-full-access"}}} {"direction":"notify","method":"session/update","params":{"sessionId":"session-1","update":{"availableCommands":[{"_meta":{"commandAction":{"configId":"collaboration_mode","kind":"setConfigOption","presentation":"state","resetValue":"default","value":"plan"}},"description":"Turn plan mode on.","input":null,"name":"plan"},{"description":"List configured Model Context Protocol (MCP) tools.","input":null,"name":"mcp"},{"description":"List available skills.","input":null,"name":"skills"},{"description":"Display session configuration and token usage.","input":null,"name":"status"},{"description":"Review uncommitted changes, or review with custom instructions.","input":{"hint":"optional review instructions"},"name":"review"},{"description":"Review changes relative to a base branch.","input":{"hint":"branch name"},"name":"review-branch"},{"description":"Review a specific commit.","input":{"hint":"commit sha"},"name":"review-commit"},{"description":"Summarize conversation to avoid hitting the context limit.","input":null,"name":"compact"},{"_meta":{"commandAction":{"kind":"prefixPrompt","presentation":"state"}},"description":"Set a goal to keep pursuing.","input":{"hint":"[|clear|pause|resume]"},"name":"goal"},{"description":"Rename the current session.","input":{"hint":"new name"},"name":"rename"},{"description":"Sign out of Codex. This option is available when you are logged in via ChatGPT.","input":null,"name":"logout"}],"sessionUpdate":"available_commands_update"}}} {"direction":"notify","method":"session/update","params":{"sessionId":"session-1","update":{"_meta":{"goal":null},"sessionUpdate":"session_info_update"}}} {"direction":"notify","method":"session/update","params":{"sessionId":"session-1","update":{"sessionUpdate":"session_info_update","title":"Run the tests"}}} diff --git a/src/__tests__/scenarios/data/baseline/zed/history-replay.jsonl b/src/__tests__/scenarios/data/baseline/zed/history-replay.jsonl index fb4f37280..79c896dc2 100644 --- a/src/__tests__/scenarios/data/baseline/zed/history-replay.jsonl +++ b/src/__tests__/scenarios/data/baseline/zed/history-replay.jsonl @@ -1,6 +1,6 @@ {"direction":"response","method":"initialize","params":{"_meta":{"goal":{"actions":["set","pause","resume","clear"],"controlMethod":"_session/goal","version":1},"jetbrains":{"air":{"capabilities":["sessionFailure","agentFileChangeReport","nativeSubagentSessions","asyncTasks","recommendedValue"],"version":1}},"steering":{"supported":true}},"agentCapabilities":{"_meta":{"authStatus":{}},"auth":{"logout":{}},"loadSession":true,"mcpCapabilities":{"acp":false,"http":true,"sse":false},"promptCapabilities":{"embeddedContext":true,"image":true},"providers":{},"sessionCapabilities":{"additionalDirectories":{},"close":{},"delete":{},"fork":{},"list":{},"resume":{},"subagents":{}}},"agentInfo":{"name":"@agentclientprotocol/codex-acp","title":"Codex","version":""},"authMethods":[{"_meta":{"api-key":{"provider":"openai"}},"description":"Use an API key to authenticate","id":"api-key","name":"API Key"},{"description":"Use ChatGPT to authenticate","id":"chat-gpt","name":"ChatGPT"}],"protocolVersion":1}} {"direction":"notify","method":"_auth/status_update","params":{"authStatus":{"kind":"none","label":"Not logged in"}}} -{"direction":"response","method":"session/load","params":{"configOptions":[{"category":"mode","currentValue":"agent","description":"Approval and sandboxing preset for the session","id":"mode","name":"Mode","options":[{"_meta":{"kind":"standard"},"description":"Requires approval to edit files and access the internet.","name":"Read-only","value":"read-only"},{"_meta":{"kind":"standard"},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","name":"Workspace access","value":"workspace-write"},{"_meta":{"kind":"auto_review"},"description":"Only ask for actions detected as potentially unsafe","name":"Auto review","value":"agent"},{"_meta":{"kind":"full_access"},"description":"Unrestricted access to the internet and any file on your computer","name":"Full access","value":"agent-full-access"}],"type":"select"},{"category":"collaboration_mode","currentValue":"default","description":"How Codex collaborates for subsequent turns","id":"collaboration_mode","name":"Collaboration mode","options":[{"name":"Default","value":"default"},{"description":"Plan before making changes","name":"Plan","value":"plan"}],"type":"select"},{"category":"model","currentValue":"model-id","description":"Model Codex uses for the session","id":"model","name":"Model","options":[{"description":"model-id model","name":"model-id","value":"model-id"}],"type":"select"},{"category":"thought_level","currentValue":"medium","description":"How much reasoning effort the model should use","id":"reasoning_effort","name":"Reasoning effort","options":[{"description":"Balanced","name":"Medium","value":"medium"}],"type":"select"}],"models":{"availableModels":[{"description":"model-id model Balanced","modelId":"model-id[medium]","name":"model-id (medium)"}],"currentModelId":"model-id[medium]"},"modes":{"availableModes":[{"_meta":{"kind":"standard"},"description":"Requires approval to edit files and access the internet.","id":"read-only","name":"Read-only"},{"_meta":{"kind":"standard"},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","id":"workspace-write","name":"Workspace access"},{"_meta":{"kind":"auto_review"},"description":"Only ask for actions detected as potentially unsafe","id":"agent","name":"Auto review"},{"_meta":{"kind":"full_access"},"description":"Unrestricted access to the internet and any file on your computer","id":"agent-full-access","name":"Full access"}],"currentModeId":"agent"}}} +{"direction":"response","method":"session/load","params":{"configOptions":[{"category":"mode","currentValue":"agent-full-access","description":"Approval and sandboxing preset for the session","id":"mode","name":"Mode","options":[{"_meta":{"kind":"standard"},"description":"Requires approval to edit files and access the internet.","name":"Read-only","value":"read-only"},{"_meta":{"kind":"standard"},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","name":"Workspace access","value":"workspace-write"},{"_meta":{"kind":"auto_review"},"description":"Only ask for actions detected as potentially unsafe","name":"Auto review","value":"agent"},{"_meta":{"kind":"full_access"},"description":"Unrestricted access to the internet and any file on your computer","name":"Full access","value":"agent-full-access"}],"type":"select"},{"category":"collaboration_mode","currentValue":"default","description":"How Codex collaborates for subsequent turns","id":"collaboration_mode","name":"Collaboration mode","options":[{"name":"Default","value":"default"},{"description":"Plan before making changes","name":"Plan","value":"plan"}],"type":"select"},{"category":"model","currentValue":"model-id","description":"Model Codex uses for the session","id":"model","name":"Model","options":[{"description":"model-id model","name":"model-id","value":"model-id"}],"type":"select"},{"category":"thought_level","currentValue":"medium","description":"How much reasoning effort the model should use","id":"reasoning_effort","name":"Reasoning effort","options":[{"description":"Balanced","name":"Medium","value":"medium"}],"type":"select"}],"models":{"availableModels":[{"description":"model-id model Balanced","modelId":"model-id[medium]","name":"model-id (medium)"}],"currentModelId":"model-id[medium]"},"modes":{"availableModes":[{"_meta":{"kind":"standard"},"description":"Requires approval to edit files and access the internet.","id":"read-only","name":"Read-only"},{"_meta":{"kind":"standard"},"description":"Edit workspace files; ask before writing outside the workspace or accessing the network.","id":"workspace-write","name":"Workspace access"},{"_meta":{"kind":"auto_review"},"description":"Only ask for actions detected as potentially unsafe","id":"agent","name":"Auto review"},{"_meta":{"kind":"full_access"},"description":"Unrestricted access to the internet and any file on your computer","id":"agent-full-access","name":"Full access"}],"currentModeId":"agent-full-access"}}} {"direction":"notify","method":"session/update","params":{"sessionId":"session-1","update":{"availableCommands":[{"_meta":{"commandAction":{"configId":"collaboration_mode","kind":"setConfigOption","presentation":"state","resetValue":"default","value":"plan"}},"description":"Turn plan mode on.","input":null,"name":"plan"},{"description":"List configured Model Context Protocol (MCP) tools.","input":null,"name":"mcp"},{"description":"List available skills.","input":null,"name":"skills"},{"description":"Display session configuration and token usage.","input":null,"name":"status"},{"description":"Review uncommitted changes, or review with custom instructions.","input":{"hint":"optional review instructions"},"name":"review"},{"description":"Review changes relative to a base branch.","input":{"hint":"branch name"},"name":"review-branch"},{"description":"Review a specific commit.","input":{"hint":"commit sha"},"name":"review-commit"},{"description":"Summarize conversation to avoid hitting the context limit.","input":null,"name":"compact"},{"_meta":{"commandAction":{"kind":"prefixPrompt","presentation":"state"}},"description":"Set a goal to keep pursuing.","input":{"hint":"[|clear|pause|resume]"},"name":"goal"},{"description":"Rename the current session.","input":{"hint":"new name"},"name":"rename"},{"description":"Sign out of Codex. This option is available when you are logged in via ChatGPT.","input":null,"name":"logout"}],"sessionUpdate":"available_commands_update"}}} {"direction":"notify","method":"session/update","params":{"sessionId":"session-1","update":{"_meta":{"goal":null},"sessionUpdate":"session_info_update"}}} {"direction":"notify","method":"session/update","params":{"sessionId":"session-1","update":{"sessionUpdate":"session_info_update","title":"Run the tests"}}}