@posthog/agent 2.3.403 → 2.3.418

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.403",
3
+ "version": "2.3.418",
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": {
@@ -102,8 +102,8 @@
102
102
  "tsx": "^4.20.6",
103
103
  "typescript": "^5.5.0",
104
104
  "vitest": "^2.1.8",
105
- "@posthog/enricher": "1.0.0",
106
105
  "@posthog/shared": "1.0.0",
106
+ "@posthog/enricher": "1.0.0",
107
107
  "@posthog/git": "1.0.0"
108
108
  },
109
109
  "dependencies": {
@@ -1026,6 +1026,19 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
1026
1026
  configOptions: this.session.configOptions,
1027
1027
  },
1028
1028
  });
1029
+
1030
+ // Notify the agent-server so its cached permissionMode stays in sync.
1031
+ // Without this, cloud sessions that change mode via plan approval or
1032
+ // setSessionMode use a stale mode for relay decisions.
1033
+ if (configId === "mode") {
1034
+ await this.client.sessionUpdate({
1035
+ sessionId: this.sessionId,
1036
+ update: {
1037
+ sessionUpdate: "current_mode_update",
1038
+ currentModeId: value,
1039
+ },
1040
+ });
1041
+ }
1029
1042
  }
1030
1043
 
1031
1044
  private async applySessionMode(modeId: string): Promise<void> {
@@ -1322,6 +1335,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
1322
1335
  logger: this.logger,
1323
1336
  updateConfigOption: (configId: string, value: string) =>
1324
1337
  this.updateConfigOption(configId, value),
1338
+ applySessionMode: (modeId: string) => this.applySessionMode(modeId),
1325
1339
  allowedDomains,
1326
1340
  });
1327
1341
  }
@@ -56,6 +56,7 @@ interface ToolHandlerContext {
56
56
  fileContentCache: { [key: string]: string };
57
57
  logger: Logger;
58
58
  updateConfigOption: (configId: string, value: string) => Promise<void>;
59
+ applySessionMode: (modeId: string) => Promise<void>;
59
60
  allowedDomains?: string[];
60
61
  }
61
62
 
@@ -167,8 +168,6 @@ async function applyPlanApproval(
167
168
  context: ToolHandlerContext,
168
169
  updatedInput: Record<string, unknown>,
169
170
  ): Promise<ToolPermissionResult> {
170
- const { session } = context;
171
-
172
171
  if (
173
172
  response.outcome?.outcome === "selected" &&
174
173
  (response.outcome.optionId === "auto" ||
@@ -176,16 +175,7 @@ async function applyPlanApproval(
176
175
  response.outcome.optionId === "acceptEdits" ||
177
176
  response.outcome.optionId === "bypassPermissions")
178
177
  ) {
179
- session.permissionMode = response.outcome
180
- .optionId as typeof session.permissionMode;
181
- await session.query.setPermissionMode(response.outcome.optionId);
182
- await context.client.sessionUpdate({
183
- sessionId: context.sessionId,
184
- update: {
185
- sessionUpdate: "current_mode_update",
186
- currentModeId: response.outcome.optionId,
187
- },
188
- });
178
+ await context.applySessionMode(response.outcome.optionId);
189
179
  await context.updateConfigOption("mode", response.outcome.optionId);
190
180
 
191
181
  return {
@@ -215,10 +205,9 @@ async function applyPlanApproval(
215
205
  async function handleEnterPlanModeTool(
216
206
  context: ToolHandlerContext,
217
207
  ): Promise<ToolPermissionResult> {
218
- const { session, toolInput } = context;
208
+ const { toolInput } = context;
219
209
 
220
- session.permissionMode = "plan";
221
- await session.query.setPermissionMode("plan");
210
+ await context.applySessionMode("plan");
222
211
  await context.updateConfigOption("mode", "plan");
223
212
 
224
213
  return {
@@ -1,5 +1,5 @@
1
1
  import type { PermissionUpdate } from "@anthropic-ai/claude-agent-sdk";
2
- import { IS_ROOT } from "../../../utils/common";
2
+ import { ALLOW_BYPASS } from "../../../utils/common";
3
3
  import { BASH_TOOLS, READ_TOOLS, SEARCH_TOOLS, WRITE_TOOLS } from "../tools";
4
4
 
5
5
  export interface PermissionOption {
@@ -92,8 +92,6 @@ export function buildPermissionOptions(
92
92
  return permissionOptions("Yes, always allow");
93
93
  }
94
94
 
95
- const ALLOW_BYPASS = !IS_ROOT || !!process.env.IS_SANDBOX;
96
-
97
95
  export function buildExitPlanModePermissionOptions(): PermissionOption[] {
98
96
  const options: PermissionOption[] = [];
99
97
 
@@ -317,7 +317,7 @@ export function buildSessionOptions(params: BuildOptionsParams): Options {
317
317
  stderr: (err) => params.logger.error(err),
318
318
  cwd: params.cwd,
319
319
  includePartialMessages: true,
320
- allowDangerouslySkipPermissions: !IS_ROOT,
320
+ allowDangerouslySkipPermissions: !IS_ROOT || !!process.env.IS_SANDBOX,
321
321
  permissionMode: params.permissionMode,
322
322
  canUseTool: params.canUseTool,
323
323
  executable: "node",
@@ -667,6 +667,20 @@ export class CodexAcpAgent extends BaseAcpAgent {
667
667
  if (response.configOptions) {
668
668
  this.sessionState.configOptions = response.configOptions;
669
669
  }
670
+ if (params.configId === "mode" && typeof params.value === "string") {
671
+ // Signal the mode change to agent-server so its session.permissionMode
672
+ // cache (used by shouldRelayPermissionToClient) stays in sync with the
673
+ // real Codex mode. Claude emits the same signal from its equivalent
674
+ // handler; without it, the agent-server's relay decisions for cloud
675
+ // runs would use a stale mode and silently auto-approve tool calls.
676
+ await this.client.sessionUpdate({
677
+ sessionId: this.sessionId,
678
+ update: {
679
+ sessionUpdate: "current_mode_update",
680
+ currentModeId: params.value,
681
+ },
682
+ });
683
+ }
670
684
  return response;
671
685
  }
672
686
 
@@ -1,4 +1,4 @@
1
- import { IS_ROOT } from "./utils/common";
1
+ import { ALLOW_BYPASS } from "./utils/common";
2
2
 
3
3
  export interface ModeInfo {
4
4
  id: string;
@@ -6,9 +6,6 @@ export interface ModeInfo {
6
6
  description: string;
7
7
  }
8
8
 
9
- // Helper constant that can easily be toggled for env/feature flag/etc
10
- const ALLOW_BYPASS = !IS_ROOT;
11
-
12
9
  const availableModes: ModeInfo[] = [
13
10
  {
14
11
  id: "default",
@@ -58,9 +55,9 @@ export function isCodeExecutionMode(mode: string): mode is CodeExecutionMode {
58
55
  }
59
56
 
60
57
  export function getAvailableModes(): ModeInfo[] {
61
- return IS_ROOT
62
- ? availableModes.filter((m) => m.id !== "bypassPermissions")
63
- : availableModes;
58
+ return ALLOW_BYPASS
59
+ ? availableModes
60
+ : availableModes.filter((m) => m.id !== "bypassPermissions");
64
61
  }
65
62
 
66
63
  // --- Codex-native modes ---
@@ -98,7 +95,7 @@ if (ALLOW_BYPASS) {
98
95
  }
99
96
 
100
97
  export function getAvailableCodexModes(): ModeInfo[] {
101
- return IS_ROOT
102
- ? codexModes.filter((m) => m.id !== "full-access")
103
- : codexModes;
98
+ return ALLOW_BYPASS
99
+ ? codexModes
100
+ : codexModes.filter((m) => m.id !== "full-access");
104
101
  }
@@ -48,6 +48,7 @@ import { resourceLink } from "../utils/acp-content";
48
48
  import { AsyncMutex } from "../utils/async-mutex";
49
49
  import { getLlmGatewayUrl } from "../utils/gateway";
50
50
  import { Logger } from "../utils/logger";
51
+ import { logAgentshRuntimeInfo } from "./agentsh-runtime";
51
52
  import {
52
53
  normalizeCloudPromptContent,
53
54
  promptBlocksToText,
@@ -297,7 +298,7 @@ export class AgentServer {
297
298
  }
298
299
 
299
300
  private shouldRelayPermissionToClient(mode: PermissionMode): boolean {
300
- return mode === "default" || mode === "auto";
301
+ return mode === "default" || mode === "auto" || mode === "read-only";
301
302
  }
302
303
 
303
304
  private createApp(): Hono {
@@ -954,6 +955,7 @@ export class AgentServer {
954
955
  this.logger.debug(
955
956
  `Agent version: ${this.config.version ?? packageJson.version}`,
956
957
  );
958
+ await logAgentshRuntimeInfo(this.logger);
957
959
  this.logger.debug(`Initial permission mode: ${initialPermissionMode}`);
958
960
 
959
961
  // Signal in_progress so the UI can start polling for updates
@@ -0,0 +1,107 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import {
3
+ logAgentshRuntimeInfo,
4
+ resolveAgentshRuntimeInfo,
5
+ } from "./agentsh-runtime";
6
+
7
+ describe("agentsh runtime detection", () => {
8
+ it("returns null when no agentsh session marker exists", async () => {
9
+ const getVersion = vi.fn();
10
+ const result = await resolveAgentshRuntimeInfo({
11
+ readSessionId: async () => {
12
+ const error = new Error("missing") as NodeJS.ErrnoException;
13
+ error.code = "ENOENT";
14
+ throw error;
15
+ },
16
+ getVersion,
17
+ });
18
+
19
+ expect(result).toBeNull();
20
+ expect(getVersion).not.toHaveBeenCalled();
21
+ });
22
+
23
+ it("rethrows unexpected session marker read errors", async () => {
24
+ const error = new Error("permission denied") as NodeJS.ErrnoException;
25
+ error.code = "EACCES";
26
+
27
+ await expect(
28
+ resolveAgentshRuntimeInfo({
29
+ readSessionId: async () => {
30
+ throw error;
31
+ },
32
+ }),
33
+ ).rejects.toBe(error);
34
+ });
35
+
36
+ it.each([
37
+ {
38
+ name: "returns the agentsh session id and version",
39
+ getVersion: async () => ({
40
+ stdout: "agentsh version 0.16.7\n",
41
+ stderr: "",
42
+ }),
43
+ expected: {
44
+ sessionId: "session-123",
45
+ version: "agentsh version 0.16.7",
46
+ },
47
+ },
48
+ {
49
+ name: "keeps the agentsh signal when version lookup fails",
50
+ getVersion: async () => {
51
+ throw new Error("agentsh not found");
52
+ },
53
+ expected: {
54
+ sessionId: "session-123",
55
+ version: null,
56
+ versionLookupError: "agentsh not found",
57
+ },
58
+ },
59
+ ])("$name", async ({ getVersion, expected }) => {
60
+ const result = await resolveAgentshRuntimeInfo({
61
+ readSessionId: async () => "session-123\n",
62
+ getVersion,
63
+ });
64
+
65
+ expect(result).toEqual(expected);
66
+ });
67
+
68
+ it("logs session id and version details", async () => {
69
+ const logger = { debug: vi.fn() };
70
+
71
+ await logAgentshRuntimeInfo(logger, {
72
+ readSessionId: async () => "session-123\n",
73
+ getVersion: async () => ({
74
+ stdout: "agentsh version 0.16.7\n",
75
+ stderr: "",
76
+ }),
77
+ });
78
+
79
+ expect(logger.debug).toHaveBeenCalledWith(
80
+ "Agentsh session ID: session-123",
81
+ );
82
+ expect(logger.debug).toHaveBeenCalledWith(
83
+ "Agentsh hardening version: agentsh version 0.16.7",
84
+ );
85
+ });
86
+
87
+ it("logs version lookup failures", async () => {
88
+ const logger = { debug: vi.fn() };
89
+
90
+ await logAgentshRuntimeInfo(logger, {
91
+ readSessionId: async () => "session-123\n",
92
+ getVersion: async () => {
93
+ throw new Error("agentsh not found");
94
+ },
95
+ });
96
+
97
+ expect(logger.debug).toHaveBeenCalledWith(
98
+ "Agentsh session ID: session-123",
99
+ );
100
+ expect(logger.debug).toHaveBeenCalledWith(
101
+ "Agentsh hardening version: unknown",
102
+ );
103
+ expect(logger.debug).toHaveBeenCalledWith(
104
+ "Agentsh version lookup failed: agentsh not found",
105
+ );
106
+ });
107
+ });
@@ -0,0 +1,97 @@
1
+ import { execFile } from "node:child_process";
2
+ import { readFile } from "node:fs/promises";
3
+ import { promisify } from "node:util";
4
+ import type { Logger } from "../utils/logger";
5
+
6
+ export const AGENTSH_SESSION_ID_FILE = "/tmp/agentsh-session-id";
7
+
8
+ const execFileAsync = promisify(execFile);
9
+
10
+ interface AgentshVersionOutput {
11
+ stdout: string;
12
+ stderr: string;
13
+ }
14
+
15
+ interface ResolveAgentshRuntimeInfoOptions {
16
+ sessionIdPath?: string;
17
+ readSessionId?: (path: string) => Promise<string>;
18
+ getVersion?: () => Promise<AgentshVersionOutput>;
19
+ }
20
+
21
+ export interface AgentshRuntimeInfo {
22
+ sessionId: string;
23
+ version: string | null;
24
+ versionLookupError?: string;
25
+ }
26
+
27
+ function errorMessage(error: unknown): string {
28
+ return error instanceof Error ? error.message : String(error);
29
+ }
30
+
31
+ function parseAgentshVersion(output: AgentshVersionOutput): string | null {
32
+ const version = `${output.stdout}\n${output.stderr}`
33
+ .split("\n")
34
+ .map((line) => line.trim())
35
+ .find(Boolean);
36
+ return version ?? null;
37
+ }
38
+
39
+ async function getAgentshVersion(): Promise<AgentshVersionOutput> {
40
+ const { stdout, stderr } = await execFileAsync("agentsh", ["--version"], {
41
+ timeout: 5_000,
42
+ });
43
+ return { stdout, stderr };
44
+ }
45
+
46
+ export async function resolveAgentshRuntimeInfo({
47
+ sessionIdPath = AGENTSH_SESSION_ID_FILE,
48
+ readSessionId = async (path: string) => readFile(path, "utf8"),
49
+ getVersion = getAgentshVersion,
50
+ }: ResolveAgentshRuntimeInfoOptions = {}): Promise<AgentshRuntimeInfo | null> {
51
+ let sessionId: string;
52
+ try {
53
+ sessionId = (await readSessionId(sessionIdPath)).trim();
54
+ } catch (error) {
55
+ const code = (error as NodeJS.ErrnoException).code;
56
+ if (code === "ENOENT") {
57
+ return null;
58
+ }
59
+ throw error;
60
+ }
61
+
62
+ if (!sessionId) {
63
+ return null;
64
+ }
65
+
66
+ try {
67
+ const output = await getVersion();
68
+ return {
69
+ sessionId,
70
+ version: parseAgentshVersion(output),
71
+ };
72
+ } catch (error) {
73
+ return {
74
+ sessionId,
75
+ version: null,
76
+ versionLookupError: errorMessage(error),
77
+ };
78
+ }
79
+ }
80
+
81
+ export async function logAgentshRuntimeInfo(
82
+ logger: Pick<Logger, "debug">,
83
+ options?: ResolveAgentshRuntimeInfoOptions,
84
+ ): Promise<void> {
85
+ const agentsh = await resolveAgentshRuntimeInfo(options);
86
+ if (!agentsh) {
87
+ return;
88
+ }
89
+
90
+ logger.debug(`Agentsh session ID: ${agentsh.sessionId}`);
91
+ logger.debug(`Agentsh hardening version: ${agentsh.version ?? "unknown"}`);
92
+ if (agentsh.versionLookupError) {
93
+ logger.debug(
94
+ `Agentsh version lookup failed: ${agentsh.versionLookupError}`,
95
+ );
96
+ }
97
+ }
@@ -23,6 +23,8 @@ export const IS_ROOT =
23
23
  typeof process !== "undefined" &&
24
24
  (process.geteuid?.() ?? process.getuid?.()) === 0;
25
25
 
26
+ export const ALLOW_BYPASS = !IS_ROOT || !!process.env.IS_SANDBOX;
27
+
26
28
  export function unreachable(value: never, logger: Logger): void {
27
29
  let valueAsString: string;
28
30
  try {