@posthog/agent 2.3.280 → 2.3.282

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@posthog/agent",
3
- "version": "2.3.280",
3
+ "version": "2.3.282",
4
4
  "repository": "https://github.com/PostHog/code",
5
5
  "description": "TypeScript agent framework wrapping Claude Agent SDK with Git-based task execution for PostHog",
6
6
  "exports": {
@@ -0,0 +1,117 @@
1
+ import { Readable, Writable } from "node:stream";
2
+ import type {
3
+ AgentSideConnection,
4
+ LoadSessionResponse,
5
+ NewSessionResponse,
6
+ } from "@agentclientprotocol/sdk";
7
+ import { beforeEach, describe, expect, it, vi } from "vitest";
8
+
9
+ const mockCodexConnection = {
10
+ initialize: vi.fn(),
11
+ newSession: vi.fn(),
12
+ loadSession: vi.fn(),
13
+ setSessionMode: vi.fn(),
14
+ listSessions: vi.fn(),
15
+ prompt: vi.fn(),
16
+ setSessionConfigOption: vi.fn(),
17
+ };
18
+
19
+ const mockKill = vi.fn();
20
+
21
+ vi.mock("@agentclientprotocol/sdk", async () => {
22
+ const actual = await vi.importActual("@agentclientprotocol/sdk");
23
+
24
+ return {
25
+ ...actual,
26
+ ClientSideConnection: vi.fn(() => mockCodexConnection),
27
+ ndJsonStream: vi.fn(() => ({}) as object),
28
+ };
29
+ });
30
+
31
+ vi.mock("./spawn", () => ({
32
+ spawnCodexProcess: vi.fn(() => ({
33
+ process: { pid: 1234 },
34
+ stdin: new Writable({
35
+ write(_chunk, _encoding, callback) {
36
+ callback();
37
+ },
38
+ }),
39
+ stdout: new Readable({
40
+ read() {},
41
+ }),
42
+ kill: mockKill,
43
+ })),
44
+ }));
45
+
46
+ vi.mock("./settings", () => ({
47
+ CodexSettingsManager: vi.fn().mockImplementation((cwd: string) => ({
48
+ initialize: vi.fn(),
49
+ dispose: vi.fn(),
50
+ getCwd: () => cwd,
51
+ setCwd: vi.fn(),
52
+ getSettings: () => ({}),
53
+ })),
54
+ }));
55
+
56
+ import { CodexAcpAgent } from "./codex-agent";
57
+
58
+ describe("CodexAcpAgent", () => {
59
+ beforeEach(() => {
60
+ vi.clearAllMocks();
61
+ });
62
+
63
+ function createAgent(): CodexAcpAgent {
64
+ const client = {
65
+ extNotification: vi.fn(),
66
+ } as unknown as AgentSideConnection;
67
+
68
+ return new CodexAcpAgent(client, {
69
+ codexProcessOptions: {
70
+ cwd: process.cwd(),
71
+ },
72
+ });
73
+ }
74
+
75
+ it("applies the requested initial mode for a new session", async () => {
76
+ const agent = createAgent();
77
+ mockCodexConnection.newSession.mockResolvedValue({
78
+ sessionId: "session-1",
79
+ modes: { currentModeId: "auto", availableModes: [] },
80
+ configOptions: [],
81
+ } satisfies Partial<NewSessionResponse>);
82
+
83
+ await agent.newSession({
84
+ cwd: process.cwd(),
85
+ _meta: { permissionMode: "read-only" },
86
+ } as never);
87
+
88
+ expect(mockCodexConnection.setSessionMode).toHaveBeenCalledWith({
89
+ sessionId: "session-1",
90
+ modeId: "read-only",
91
+ });
92
+ expect(
93
+ (agent as unknown as { sessionState: { permissionMode: string } })
94
+ .sessionState.permissionMode,
95
+ ).toBe("read-only");
96
+ });
97
+
98
+ it("preserves the live session mode when loading an existing session", async () => {
99
+ const agent = createAgent();
100
+ mockCodexConnection.loadSession.mockResolvedValue({
101
+ modes: { currentModeId: "read-only", availableModes: [] },
102
+ configOptions: [],
103
+ } satisfies Partial<LoadSessionResponse>);
104
+
105
+ await agent.loadSession({
106
+ sessionId: "session-1",
107
+ cwd: process.cwd(),
108
+ _meta: { permissionMode: "auto" },
109
+ } as never);
110
+
111
+ expect(mockCodexConnection.setSessionMode).not.toHaveBeenCalled();
112
+ expect(
113
+ (agent as unknown as { sessionState: { permissionMode: string } })
114
+ .sessionState.permissionMode,
115
+ ).toBe("read-only");
116
+ });
117
+ });
@@ -36,8 +36,11 @@ import {
36
36
  import packageJson from "../../../package.json" with { type: "json" };
37
37
  import { POSTHOG_NOTIFICATIONS } from "../../acp-extensions";
38
38
  import {
39
- CODE_EXECUTION_MODES,
40
39
  type CodeExecutionMode,
40
+ type CodexNativeMode,
41
+ isCodeExecutionMode,
42
+ isCodexNativeMode,
43
+ type PermissionMode,
41
44
  } from "../../execution-mode";
42
45
  import type { ProcessSpawnedCallback } from "../../types";
43
46
  import { Logger } from "../../utils/logger";
@@ -83,20 +86,41 @@ type CodexSession = BaseSession & {
83
86
  settingsManager: CodexSettingsManager;
84
87
  };
85
88
 
86
- function toCodeExecutionMode(mode?: string): CodeExecutionMode {
87
- if (mode && (CODE_EXECUTION_MODES as readonly string[]).includes(mode)) {
88
- return mode as CodeExecutionMode;
89
+ function toCodexPermissionMode(mode?: string): PermissionMode {
90
+ if (mode && (isCodexNativeMode(mode) || isCodeExecutionMode(mode))) {
91
+ return mode;
89
92
  }
90
- return "default";
93
+ return "auto";
91
94
  }
92
95
 
93
- const CODEX_NATIVE_MODE: Record<CodeExecutionMode, string> = {
94
- default: "default",
95
- acceptEdits: "default",
96
- plan: "plan",
97
- bypassPermissions: "default",
96
+ const CODEX_NATIVE_MODE: Record<CodeExecutionMode, CodexNativeMode> = {
97
+ default: "auto",
98
+ acceptEdits: "auto",
99
+ plan: "read-only",
100
+ bypassPermissions: "full-access",
98
101
  };
99
102
 
103
+ function toCodexNativeMode(mode?: string): CodexNativeMode {
104
+ if (mode && isCodexNativeMode(mode)) {
105
+ return mode;
106
+ }
107
+ if (mode && isCodeExecutionMode(mode)) {
108
+ return CODEX_NATIVE_MODE[mode];
109
+ }
110
+ return "auto";
111
+ }
112
+
113
+ function getCurrentPermissionMode(
114
+ currentModeId?: string,
115
+ fallbackMode?: string,
116
+ ): PermissionMode {
117
+ if (currentModeId && isCodexNativeMode(currentModeId)) {
118
+ return currentModeId;
119
+ }
120
+
121
+ return toCodexPermissionMode(fallbackMode);
122
+ }
123
+
100
124
  export class CodexAcpAgent extends BaseAcpAgent {
101
125
  readonly adapterName = "codex";
102
126
  declare session: CodexSession;
@@ -179,6 +203,7 @@ export class CodexAcpAgent extends BaseAcpAgent {
179
203
 
180
204
  async newSession(params: NewSessionRequest): Promise<NewSessionResponse> {
181
205
  const meta = params._meta as NewSessionMeta | undefined;
206
+ const requestedPermissionMode = toCodexPermissionMode(meta?.permissionMode);
182
207
 
183
208
  const response = await this.codexConnection.newSession(params);
184
209
 
@@ -186,13 +211,19 @@ export class CodexAcpAgent extends BaseAcpAgent {
186
211
  this.sessionState = createSessionState(response.sessionId, params.cwd, {
187
212
  taskRunId: meta?.taskRunId,
188
213
  taskId: meta?.taskId ?? meta?.persistence?.taskId,
189
- modeId: response.modes?.currentModeId ?? "default",
214
+ modeId: response.modes?.currentModeId ?? "auto",
190
215
  modelId: response.models?.currentModelId,
191
- permissionMode: toCodeExecutionMode(meta?.permissionMode),
216
+ permissionMode: requestedPermissionMode,
192
217
  });
193
218
  this.sessionId = response.sessionId;
194
219
  this.sessionState.configOptions = response.configOptions ?? [];
195
220
 
221
+ await this.applyInitialPermissionMode(
222
+ response.sessionId,
223
+ meta?.permissionMode,
224
+ response.modes?.currentModeId,
225
+ );
226
+
196
227
  // Emit _posthog/sdk_session so the app can track the session
197
228
  if (meta?.taskRunId) {
198
229
  await this.client.extNotification(POSTHOG_NOTIFICATIONS.SDK_SESSION, {
@@ -213,9 +244,14 @@ export class CodexAcpAgent extends BaseAcpAgent {
213
244
  async loadSession(params: LoadSessionRequest): Promise<LoadSessionResponse> {
214
245
  const response = await this.codexConnection.loadSession(params);
215
246
  const meta = params._meta as NewSessionMeta | undefined;
247
+ const currentPermissionMode = getCurrentPermissionMode(
248
+ response.modes?.currentModeId,
249
+ meta?.permissionMode,
250
+ );
216
251
 
217
252
  this.sessionState = createSessionState(params.sessionId, params.cwd, {
218
- permissionMode: toCodeExecutionMode(meta?.permissionMode),
253
+ modeId: response.modes?.currentModeId ?? "auto",
254
+ permissionMode: currentPermissionMode,
219
255
  });
220
256
  this.sessionId = params.sessionId;
221
257
  this.sessionState.configOptions = response.configOptions ?? [];
@@ -234,10 +270,15 @@ export class CodexAcpAgent extends BaseAcpAgent {
234
270
  });
235
271
 
236
272
  const meta = params._meta as NewSessionMeta | undefined;
273
+ const currentPermissionMode = getCurrentPermissionMode(
274
+ loadResponse.modes?.currentModeId,
275
+ meta?.permissionMode,
276
+ );
237
277
  this.sessionState = createSessionState(params.sessionId, params.cwd, {
238
278
  taskRunId: meta?.taskRunId,
239
279
  taskId: meta?.taskId ?? meta?.persistence?.taskId,
240
- permissionMode: toCodeExecutionMode(meta?.permissionMode),
280
+ modeId: loadResponse.modes?.currentModeId ?? "auto",
281
+ permissionMode: currentPermissionMode,
241
282
  });
242
283
  this.sessionId = params.sessionId;
243
284
  this.sessionState.configOptions = loadResponse.configOptions ?? [];
@@ -268,17 +309,49 @@ export class CodexAcpAgent extends BaseAcpAgent {
268
309
  });
269
310
 
270
311
  const meta = params._meta as NewSessionMeta | undefined;
312
+ const requestedPermissionMode = toCodexPermissionMode(meta?.permissionMode);
271
313
  this.sessionState = createSessionState(newResponse.sessionId, params.cwd, {
272
314
  taskRunId: meta?.taskRunId,
273
315
  taskId: meta?.taskId ?? meta?.persistence?.taskId,
274
- permissionMode: toCodeExecutionMode(meta?.permissionMode),
316
+ modeId: newResponse.modes?.currentModeId ?? "auto",
317
+ permissionMode: requestedPermissionMode,
275
318
  });
276
319
  this.sessionId = newResponse.sessionId;
277
320
  this.sessionState.configOptions = newResponse.configOptions ?? [];
278
321
 
322
+ await this.applyInitialPermissionMode(
323
+ newResponse.sessionId,
324
+ meta?.permissionMode,
325
+ newResponse.modes?.currentModeId,
326
+ );
327
+
279
328
  return newResponse;
280
329
  }
281
330
 
331
+ private async applyInitialPermissionMode(
332
+ sessionId: string,
333
+ permissionMode?: string,
334
+ currentModeId?: string,
335
+ ): Promise<void> {
336
+ if (!permissionMode) {
337
+ return;
338
+ }
339
+
340
+ const nativeMode = toCodexNativeMode(permissionMode);
341
+ if (nativeMode === currentModeId) {
342
+ this.sessionState.modeId = nativeMode;
343
+ this.sessionState.permissionMode = toCodexPermissionMode(permissionMode);
344
+ return;
345
+ }
346
+
347
+ await this.codexConnection.setSessionMode({
348
+ sessionId,
349
+ modeId: nativeMode,
350
+ });
351
+ this.sessionState.modeId = nativeMode;
352
+ this.sessionState.permissionMode = toCodexPermissionMode(permissionMode);
353
+ }
354
+
282
355
  async listSessions(
283
356
  params: ListSessionsRequest,
284
357
  ): Promise<ListSessionsResponse> {
@@ -347,8 +420,8 @@ export class CodexAcpAgent extends BaseAcpAgent {
347
420
  async setSessionMode(
348
421
  params: SetSessionModeRequest,
349
422
  ): Promise<SetSessionModeResponse> {
350
- const requestedMode = toCodeExecutionMode(params.modeId);
351
- const nativeMode = CODEX_NATIVE_MODE[requestedMode];
423
+ const requestedMode = toCodexPermissionMode(params.modeId);
424
+ const nativeMode = toCodexNativeMode(params.modeId);
352
425
 
353
426
  const response = await this.codexConnection.setSessionMode({
354
427
  ...params,
@@ -29,7 +29,7 @@ import type {
29
29
  WriteTextFileRequest,
30
30
  WriteTextFileResponse,
31
31
  } from "@agentclientprotocol/sdk";
32
- import type { CodeExecutionMode } from "../../execution-mode";
32
+ import type { PermissionMode } from "../../execution-mode";
33
33
  import type { Logger } from "../../utils/logger";
34
34
  import type { CodexSessionState } from "./session-state";
35
35
 
@@ -38,7 +38,7 @@ export interface CodexClientCallbacks {
38
38
  onUsageUpdate?: (update: Record<string, unknown>) => void;
39
39
  }
40
40
 
41
- const AUTO_APPROVED_KINDS: Record<CodeExecutionMode, Set<ToolKind>> = {
41
+ const AUTO_APPROVED_KINDS: Record<PermissionMode, Set<ToolKind>> = {
42
42
  default: new Set(["read", "search", "fetch", "think"]),
43
43
  acceptEdits: new Set(["read", "edit", "search", "fetch", "think"]),
44
44
  plan: new Set(["read", "search", "fetch", "think"]),
@@ -54,13 +54,27 @@ const AUTO_APPROVED_KINDS: Record<CodeExecutionMode, Set<ToolKind>> = {
54
54
  "switch_mode",
55
55
  "other",
56
56
  ]),
57
+ auto: new Set(["read", "search", "fetch", "think"]),
58
+ "read-only": new Set(["read", "search", "fetch", "think"]),
59
+ "full-access": new Set([
60
+ "read",
61
+ "edit",
62
+ "delete",
63
+ "move",
64
+ "search",
65
+ "execute",
66
+ "think",
67
+ "fetch",
68
+ "switch_mode",
69
+ "other",
70
+ ]),
57
71
  };
58
72
 
59
73
  function shouldAutoApprove(
60
- mode: CodeExecutionMode,
74
+ mode: PermissionMode,
61
75
  kind: ToolKind | null | undefined,
62
76
  ): boolean {
63
- if (mode === "bypassPermissions") return true;
77
+ if (mode === "bypassPermissions" || mode === "full-access") return true;
64
78
  if (!kind) return false;
65
79
  return AUTO_APPROVED_KINDS[mode]?.has(kind) ?? false;
66
80
  }
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import type { SessionConfigOption } from "@agentclientprotocol/sdk";
7
- import type { CodeExecutionMode } from "../../execution-mode";
7
+ import type { PermissionMode } from "../../execution-mode";
8
8
 
9
9
  export interface CodexUsage {
10
10
  inputTokens: number;
@@ -22,7 +22,7 @@ export interface CodexSessionState {
22
22
  accumulatedUsage: CodexUsage;
23
23
  contextSize?: number;
24
24
  contextUsed?: number;
25
- permissionMode: CodeExecutionMode;
25
+ permissionMode: PermissionMode;
26
26
  taskRunId?: string;
27
27
  taskId?: string;
28
28
  }
@@ -35,13 +35,13 @@ export function createSessionState(
35
35
  taskId?: string;
36
36
  modeId?: string;
37
37
  modelId?: string;
38
- permissionMode?: CodeExecutionMode;
38
+ permissionMode?: PermissionMode;
39
39
  },
40
40
  ): CodexSessionState {
41
41
  return {
42
42
  sessionId,
43
43
  cwd,
44
- modeId: opts?.modeId ?? "default",
44
+ modeId: opts?.modeId ?? "auto",
45
45
  modelId: opts?.modelId,
46
46
  configOptions: [],
47
47
  accumulatedUsage: {
@@ -50,7 +50,7 @@ export function createSessionState(
50
50
  cachedReadTokens: 0,
51
51
  cachedWriteTokens: 0,
52
52
  },
53
- permissionMode: opts?.permissionMode ?? "default",
53
+ permissionMode: opts?.permissionMode ?? "auto",
54
54
  taskRunId: opts?.taskRunId,
55
55
  taskId: opts?.taskId,
56
56
  };
@@ -0,0 +1,21 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { getAvailableCodexModes, getAvailableModes } from "./execution-mode";
3
+
4
+ describe("execution modes", () => {
5
+ it("includes auto-accept permissions for claude sessions", () => {
6
+ expect(getAvailableModes().map((mode) => mode.id)).toEqual([
7
+ "default",
8
+ "acceptEdits",
9
+ "plan",
10
+ "bypassPermissions",
11
+ ]);
12
+ });
13
+
14
+ it("includes full access for codex sessions", () => {
15
+ expect(getAvailableCodexModes().map((mode) => mode.id)).toEqual([
16
+ "read-only",
17
+ "auto",
18
+ "full-access",
19
+ ]);
20
+ });
21
+ });
@@ -35,8 +35,8 @@ const availableModes: ModeInfo[] = [
35
35
  if (ALLOW_BYPASS) {
36
36
  availableModes.push({
37
37
  id: "bypassPermissions",
38
- name: "Bypass Permissions",
39
- description: "Bypass all permission prompts",
38
+ name: "Auto-accept Permissions",
39
+ description: "Auto-accept all permission requests",
40
40
  });
41
41
  }
42
42
 
@@ -51,8 +51,11 @@ export const CODE_EXECUTION_MODES = [
51
51
 
52
52
  export type CodeExecutionMode = (typeof CODE_EXECUTION_MODES)[number];
53
53
 
54
+ export function isCodeExecutionMode(mode: string): mode is CodeExecutionMode {
55
+ return (CODE_EXECUTION_MODES as readonly string[]).includes(mode);
56
+ }
57
+
54
58
  export function getAvailableModes(): ModeInfo[] {
55
- // When IS_ROOT, do not allow bypassPermissions
56
59
  return IS_ROOT
57
60
  ? availableModes.filter((m) => m.id !== "bypassPermissions")
58
61
  : availableModes;
@@ -67,6 +70,10 @@ export type CodexNativeMode = (typeof CODEX_NATIVE_MODES)[number];
67
70
  /** Union of all permission mode IDs across adapters */
68
71
  export type PermissionMode = CodeExecutionMode | CodexNativeMode;
69
72
 
73
+ export function isCodexNativeMode(mode: string): mode is CodexNativeMode {
74
+ return (CODEX_NATIVE_MODES as readonly string[]).includes(mode);
75
+ }
76
+
70
77
  const codexModes: ModeInfo[] = [
71
78
  {
72
79
  id: "read-only",
@@ -84,7 +91,7 @@ if (ALLOW_BYPASS) {
84
91
  codexModes.push({
85
92
  id: "full-access",
86
93
  name: "Full Access",
87
- description: "Bypass all permission prompts",
94
+ description: "Auto-accept all permission requests",
88
95
  });
89
96
  }
90
97
 
@@ -18,7 +18,7 @@ import {
18
18
  type InProcessAcpConnection,
19
19
  } from "../adapters/acp-connection";
20
20
  import { selectRecentTurns } from "../adapters/claude/session/jsonl-hydration";
21
- import type { CodeExecutionMode } from "../execution-mode";
21
+ import type { PermissionMode } from "../execution-mode";
22
22
  import { DEFAULT_CODEX_MODEL } from "../gateway-models";
23
23
  import { PostHogAPIClient } from "../posthog-api";
24
24
  import {
@@ -164,7 +164,7 @@ interface ActiveSession {
164
164
  deviceInfo: DeviceInfo;
165
165
  logWriter: SessionLogWriter;
166
166
  /** Current permission mode, tracked for relay decisions */
167
- permissionMode: CodeExecutionMode;
167
+ permissionMode: PermissionMode;
168
168
  /** Whether a desktop client has ever connected via SSE during this session */
169
169
  hasDesktopConnected: boolean;
170
170
  }
@@ -265,8 +265,16 @@ export class AgentServer {
265
265
  return payload.mode ?? this.config.mode;
266
266
  }
267
267
 
268
- private getSessionPermissionMode(): CodeExecutionMode {
269
- return this.session?.permissionMode ?? "default";
268
+ private getSessionPermissionMode(): PermissionMode {
269
+ if (this.session?.permissionMode) {
270
+ return this.session.permissionMode;
271
+ }
272
+
273
+ return this.getRuntimeAdapter() === "codex" ? "auto" : "default";
274
+ }
275
+
276
+ private shouldRelayPermissionToClient(mode: PermissionMode): boolean {
277
+ return mode === "default" || mode === "auto";
270
278
  }
271
279
 
272
280
  private createApp(): Hono {
@@ -839,12 +847,15 @@ export class AgentServer {
839
847
  });
840
848
 
841
849
  const runState = preTaskRun?.state as Record<string, unknown> | undefined;
842
- // Cloud runs default to bypassPermissions (auto-approve everything).
843
- // Only PostHog Code sets initial_permission_mode explicitly (e.g., "plan").
844
- const initialPermissionMode: CodeExecutionMode =
850
+ // Preserve native Codex modes for cloud runs so they behave the same as
851
+ // local sessions. Claude keeps the historical auto-approved default when
852
+ // PostHog Code has not explicitly selected a mode.
853
+ const initialPermissionMode: PermissionMode =
845
854
  typeof runState?.initial_permission_mode === "string"
846
- ? (runState.initial_permission_mode as CodeExecutionMode)
847
- : "bypassPermissions";
855
+ ? (runState.initial_permission_mode as PermissionMode)
856
+ : runtimeAdapter === "codex"
857
+ ? "auto"
858
+ : "bypassPermissions";
848
859
  const sessionResponse = await clientConnection.newSession({
849
860
  cwd: this.config.repositoryPath ?? "/tmp/workspace",
850
861
  mcpServers: this.config.mcpServers ?? [],
@@ -1588,7 +1599,9 @@ ${attributionInstructions}
1588
1599
  const isQuestion = codeToolKind === "question";
1589
1600
  const sessionPermissionMode = this.getSessionPermissionMode();
1590
1601
  const needsRelay =
1591
- isQuestion || isPlanApproval || sessionPermissionMode === "default";
1602
+ isQuestion ||
1603
+ isPlanApproval ||
1604
+ this.shouldRelayPermissionToClient(sessionPermissionMode);
1592
1605
 
1593
1606
  if (needsRelay && this.session?.hasDesktopConnected) {
1594
1607
  this.logger.info("Relaying permission to connected client", {
@@ -1634,7 +1647,7 @@ ${attributionInstructions}
1634
1647
  this.session
1635
1648
  ) {
1636
1649
  this.session.permissionMode = params.update
1637
- .currentModeId as CodeExecutionMode;
1650
+ .currentModeId as PermissionMode;
1638
1651
  this.logger.info("Permission mode updated", {
1639
1652
  mode: params.update.currentModeId,
1640
1653
  });