Skip to content

Commit 2009734

Browse files
committed
feat: give codex permissions support
1 parent d04efd4 commit 2009734

8 files changed

Lines changed: 163 additions & 17 deletions

File tree

apps/code/src/main/services/agent/service.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ import { isMcpToolReadOnly } from "@posthog/agent";
1616
import { hydrateSessionJsonl } from "@posthog/agent/adapters/claude/session/jsonl-hydration";
1717
import { getEffortOptions } from "@posthog/agent/adapters/claude/session/models";
1818
import { Agent } from "@posthog/agent/agent";
19-
import { getAvailableModes } from "@posthog/agent/execution-mode";
19+
import {
20+
getAvailableCodexModes,
21+
getAvailableModes,
22+
} from "@posthog/agent/execution-mode";
2023
import {
2124
DEFAULT_CODEX_MODEL,
2225
DEFAULT_GATEWAY_MODEL,
@@ -1635,18 +1638,21 @@ For git operations while detached:
16351638
});
16361639
}
16371640

1638-
const modeOptions = getAvailableModes().map((mode) => ({
1641+
const modes =
1642+
adapter === "codex" ? getAvailableCodexModes() : getAvailableModes();
1643+
const modeOptions = modes.map((mode) => ({
16391644
value: mode.id,
16401645
name: mode.name,
16411646
description: mode.description ?? undefined,
16421647
}));
1648+
const defaultMode = adapter === "codex" ? "auto" : "plan";
16431649

16441650
const configOptions: SessionConfigOption[] = [
16451651
{
16461652
id: "mode",
16471653
name: "Approval Preset",
16481654
type: "select",
1649-
currentValue: "plan",
1655+
currentValue: defaultMode,
16501656
options: modeOptions,
16511657
category: "mode",
16521658
description:

apps/code/src/renderer/features/task-detail/components/TaskInput.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,8 +196,11 @@ export function TaskInput({
196196
// Defaults ensure values are always passed even before the preview config loads.
197197
const currentModel =
198198
modelOption?.type === "select" ? modelOption.currentValue : undefined;
199+
const adapterDefault = adapter === "codex" ? "auto" : "plan";
199200
const modeFallback =
200-
defaultInitialTaskMode === "last_used" ? lastUsedInitialTaskMode : "plan";
201+
defaultInitialTaskMode === "last_used"
202+
? (lastUsedInitialTaskMode ?? adapterDefault)
203+
: adapterDefault;
201204
const currentExecutionMode =
202205
getCurrentModeFromConfigOptions(modeOption ? [modeOption] : undefined) ??
203206
modeFallback;

apps/code/src/renderer/features/task-detail/hooks/usePreviewConfig.ts

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,40 @@ export function usePreviewConfig(
6161

6262
const { defaultInitialTaskMode, lastUsedInitialTaskMode } =
6363
useSettingsStore.getState();
64-
const initialMode =
65-
defaultInitialTaskMode === "last_used"
66-
? lastUsedInitialTaskMode
67-
: "plan";
64+
65+
// Use the mode option's existing currentValue (set by the server
66+
// based on the adapter) when the user hasn't chosen a preference,
67+
// or when their last-used mode doesn't match the current adapter's
68+
// available modes.
69+
const modeOpt = options.find((o) => o.id === "mode");
70+
const serverDefault = modeOpt?.currentValue;
71+
const availableValues: string[] =
72+
modeOpt?.type === "select"
73+
? (
74+
modeOpt.options as Array<{
75+
value?: string;
76+
options?: Array<{ value: string }>;
77+
}>
78+
).flatMap((o) =>
79+
o.options
80+
? o.options.map((go) => go.value)
81+
: o.value
82+
? [o.value]
83+
: [],
84+
)
85+
: [];
86+
87+
let initialMode: string;
88+
if (
89+
defaultInitialTaskMode === "last_used" &&
90+
lastUsedInitialTaskMode &&
91+
availableValues.includes(lastUsedInitialTaskMode)
92+
) {
93+
initialMode = lastUsedInitialTaskMode;
94+
} else {
95+
initialMode =
96+
typeof serverDefault === "string" ? serverDefault : "plan";
97+
}
6898

6999
const withMode = options.map((opt) =>
70100
opt.id === "mode"

apps/code/src/shared/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ export const executionModeSchema = z.enum([
77
"acceptEdits",
88
"plan",
99
"bypassPermissions",
10+
"auto",
11+
"read-only",
12+
"full-access",
1013
]);
1114
export type ExecutionMode = z.infer<typeof executionModeSchema>;
1215

packages/agent/src/adapters/codex/codex-agent.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ import {
3636
} from "@agentclientprotocol/sdk";
3737
import packageJson from "../../../package.json" with { type: "json" };
3838
import { POSTHOG_NOTIFICATIONS } from "../../acp-extensions";
39+
import {
40+
CODEX_NATIVE_MODES,
41+
type CodexNativeMode,
42+
type PermissionMode,
43+
} from "../../execution-mode";
3944
import type { ProcessSpawnedCallback } from "../../types";
4045
import { Logger } from "../../utils/logger";
4146
import {
@@ -80,6 +85,13 @@ type CodexSession = BaseSession & {
8085
settingsManager: CodexSettingsManager;
8186
};
8287

88+
function toPermissionMode(mode?: string): PermissionMode {
89+
if (mode && (CODEX_NATIVE_MODES as readonly string[]).includes(mode)) {
90+
return mode as CodexNativeMode;
91+
}
92+
return "auto";
93+
}
94+
8395
export class CodexAcpAgent extends BaseAcpAgent {
8496
readonly adapterName = "codex";
8597
declare session: CodexSession;
@@ -125,14 +137,15 @@ export class CodexAcpAgent extends BaseAcpAgent {
125137
this.sessionState ?? {
126138
sessionId: "",
127139
cwd: "",
128-
modeId: "default",
140+
modeId: "auto",
129141
configOptions: [],
130142
accumulatedUsage: {
131143
inputTokens: 0,
132144
outputTokens: 0,
133145
cachedReadTokens: 0,
134146
cachedWriteTokens: 0,
135147
},
148+
permissionMode: "auto",
136149
cancelled: false,
137150
},
138151
),
@@ -182,6 +195,7 @@ export class CodexAcpAgent extends BaseAcpAgent {
182195
taskId: meta?.taskId ?? meta?.persistence?.taskId,
183196
modeId: response.modes?.currentModeId ?? "default",
184197
modelId: response.models?.currentModelId,
198+
permissionMode: toPermissionMode(meta?.permissionMode),
185199
});
186200
this.sessionId = response.sessionId;
187201
this.sessionState.configOptions = response.configOptions ?? [];
@@ -353,9 +367,16 @@ export class CodexAcpAgent extends BaseAcpAgent {
353367
async setSessionMode(
354368
params: SetSessionModeRequest,
355369
): Promise<SetSessionModeResponse> {
356-
const response = await this.codexConnection.setSessionMode(params);
370+
const permissionMode = toPermissionMode(params.modeId);
371+
372+
const response = await this.codexConnection.setSessionMode({
373+
...params,
374+
modeId: permissionMode,
375+
});
376+
357377
if (this.sessionState) {
358-
this.sessionState.modeId = params.modeId;
378+
this.sessionState.modeId = permissionMode;
379+
this.sessionState.permissionMode = permissionMode;
359380
}
360381
return response ?? {};
361382
}

packages/agent/src/adapters/codex/codex-client.ts

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,13 @@ import type {
2323
TerminalHandle,
2424
TerminalOutputRequest,
2525
TerminalOutputResponse,
26+
ToolKind,
2627
WaitForTerminalExitRequest,
2728
WaitForTerminalExitResponse,
2829
WriteTextFileRequest,
2930
WriteTextFileResponse,
3031
} from "@agentclientprotocol/sdk";
32+
import type { PermissionMode } from "../../execution-mode";
3133
import type { Logger } from "../../utils/logger";
3234
import type { CodexSessionState } from "./session-state";
3335

@@ -36,6 +38,32 @@ export interface CodexClientCallbacks {
3638
onUsageUpdate?: (update: Record<string, unknown>) => void;
3739
}
3840

41+
const AUTO_APPROVED_KINDS: Partial<Record<PermissionMode, Set<ToolKind>>> = {
42+
auto: new Set(["read", "search", "fetch", "think"]),
43+
"read-only": new Set(["read", "search", "fetch", "think"]),
44+
"full-access": new Set([
45+
"read",
46+
"edit",
47+
"delete",
48+
"move",
49+
"search",
50+
"execute",
51+
"think",
52+
"fetch",
53+
"switch_mode",
54+
"other",
55+
]),
56+
};
57+
58+
function shouldAutoApprove(
59+
mode: PermissionMode,
60+
kind: ToolKind | null | undefined,
61+
): boolean {
62+
if (mode === "full-access") return true;
63+
if (!kind) return false;
64+
return AUTO_APPROVED_KINDS[mode]?.has(kind) ?? false;
65+
}
66+
3967
/**
4068
* Creates an ACP Client that delegates all requests from codex-acp
4169
* to the upstream PostHog Code client (via AgentSideConnection).
@@ -46,16 +74,31 @@ export function createCodexClient(
4674
sessionState: CodexSessionState,
4775
callbacks?: CodexClientCallbacks,
4876
): Client {
49-
// Track terminal handles for delegation
5077
const terminalHandles = new Map<string, TerminalHandle>();
5178

5279
return {
5380
async requestPermission(
5481
params: RequestPermissionRequest,
5582
): Promise<RequestPermissionResponse> {
56-
logger.debug("Relaying permission request to upstream", {
57-
sessionId: params.sessionId,
58-
});
83+
const kind = params.toolCall?.kind as ToolKind | null | undefined;
84+
85+
if (shouldAutoApprove(sessionState.permissionMode, kind)) {
86+
logger.debug("Auto-approving permission", {
87+
mode: sessionState.permissionMode,
88+
kind,
89+
toolCallId: params.toolCall?.toolCallId,
90+
});
91+
const allowOption = params.options?.find(
92+
(o) => o.kind === "allow_once" || o.kind === "allow_always",
93+
);
94+
return {
95+
outcome: {
96+
outcome: "selected",
97+
optionId: allowOption?.optionId ?? "allow",
98+
},
99+
};
100+
}
101+
59102
return upstreamClient.requestPermission(params);
60103
},
61104

packages/agent/src/adapters/codex/session-state.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55

66
import type { SessionConfigOption } from "@agentclientprotocol/sdk";
7+
import type { PermissionMode } from "../../execution-mode";
78

89
export interface CodexUsage {
910
inputTokens: number;
@@ -21,6 +22,7 @@ export interface CodexSessionState {
2122
accumulatedUsage: CodexUsage;
2223
contextSize?: number;
2324
contextUsed?: number;
25+
permissionMode: PermissionMode;
2426
cancelled: boolean;
2527
interruptReason?: string;
2628
taskRunId?: string;
@@ -35,12 +37,13 @@ export function createSessionState(
3537
taskId?: string;
3638
modeId?: string;
3739
modelId?: string;
40+
permissionMode?: PermissionMode;
3841
},
3942
): CodexSessionState {
4043
return {
4144
sessionId,
4245
cwd,
43-
modeId: opts?.modeId ?? "default",
46+
modeId: opts?.modeId ?? "auto",
4447
modelId: opts?.modelId,
4548
configOptions: [],
4649
accumulatedUsage: {
@@ -49,6 +52,7 @@ export function createSessionState(
4952
cachedReadTokens: 0,
5053
cachedWriteTokens: 0,
5154
},
55+
permissionMode: opts?.permissionMode ?? "auto",
5256
cancelled: false,
5357
taskRunId: opts?.taskRunId,
5458
taskId: opts?.taskId,

packages/agent/src/execution-mode.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { IS_ROOT } from "./utils/common";
22

33
export interface ModeInfo {
4-
id: CodeExecutionMode;
4+
id: string;
55
name: string;
66
description: string;
77
}
@@ -57,3 +57,39 @@ export function getAvailableModes(): ModeInfo[] {
5757
? availableModes.filter((m) => m.id !== "bypassPermissions")
5858
: availableModes;
5959
}
60+
61+
// --- Codex-native modes ---
62+
63+
export const CODEX_NATIVE_MODES = ["auto", "read-only", "full-access"] as const;
64+
65+
export type CodexNativeMode = (typeof CODEX_NATIVE_MODES)[number];
66+
67+
/** Union of all permission mode IDs across adapters */
68+
export type PermissionMode = CodeExecutionMode | CodexNativeMode;
69+
70+
const codexModes: ModeInfo[] = [
71+
{
72+
id: "read-only",
73+
name: "Read Only",
74+
description: "Read-only access, no file modifications",
75+
},
76+
{
77+
id: "auto",
78+
name: "Auto",
79+
description: "Standard behavior, prompts for dangerous operations",
80+
},
81+
];
82+
83+
if (ALLOW_BYPASS) {
84+
codexModes.push({
85+
id: "full-access",
86+
name: "Full Access",
87+
description: "Auto-accept all permission requests",
88+
});
89+
}
90+
91+
export function getAvailableCodexModes(): ModeInfo[] {
92+
return IS_ROOT
93+
? codexModes.filter((m) => m.id !== "full-access")
94+
: codexModes;
95+
}

0 commit comments

Comments
 (0)