@posthog/agent 2.3.388 → 2.3.401
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/dist/adapters/claude/conversion/tool-use-to-acp.js.map +1 -1
- package/dist/adapters/claude/mcp/tool-metadata.d.ts +24 -0
- package/dist/adapters/claude/mcp/tool-metadata.js +165 -0
- package/dist/adapters/claude/mcp/tool-metadata.js.map +1 -0
- package/dist/adapters/claude/tools.js.map +1 -1
- package/dist/agent.js +113 -3
- package/dist/agent.js.map +1 -1
- package/dist/handoff-checkpoint.d.ts +1 -0
- package/dist/handoff-checkpoint.js +17 -1
- package/dist/handoff-checkpoint.js.map +1 -1
- package/dist/index.d.ts +7 -9
- package/dist/index.js.map +1 -1
- package/dist/posthog-api.js +5 -1
- package/dist/posthog-api.js.map +1 -1
- package/dist/server/agent-server.js +258 -101
- package/dist/server/agent-server.js.map +1 -1
- package/dist/server/bin.cjs +248 -98
- package/dist/server/bin.cjs.map +1 -1
- package/dist/tree-tracker.js +128 -97
- package/dist/tree-tracker.js.map +1 -1
- package/package.json +7 -3
- package/src/adapters/claude/claude-agent.ts +5 -0
- package/src/adapters/claude/mcp/tool-metadata.test.ts +93 -0
- package/src/adapters/claude/mcp/tool-metadata.ts +33 -0
- package/src/adapters/claude/permissions/permission-handlers.test.ts +165 -0
- package/src/adapters/claude/permissions/permission-handlers.ts +105 -0
- package/src/adapters/claude/session/instructions.ts +9 -1
- package/src/adapters/claude/types.ts +2 -0
- package/src/handoff-checkpoint.test.ts +1 -0
- package/src/handoff-checkpoint.ts +17 -1
- package/src/sagas/apply-snapshot-saga.test.ts +1 -0
- package/src/sagas/apply-snapshot-saga.ts +68 -54
- package/src/sagas/capture-tree-saga.test.ts +18 -0
- package/src/sagas/capture-tree-saga.ts +64 -49
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@posthog/agent",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.401",
|
|
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": {
|
|
@@ -52,6 +52,10 @@
|
|
|
52
52
|
"types": "./dist/adapters/reasoning-effort.d.ts",
|
|
53
53
|
"import": "./dist/adapters/reasoning-effort.js"
|
|
54
54
|
},
|
|
55
|
+
"./adapters/claude/mcp/tool-metadata": {
|
|
56
|
+
"types": "./dist/adapters/claude/mcp/tool-metadata.d.ts",
|
|
57
|
+
"import": "./dist/adapters/claude/mcp/tool-metadata.js"
|
|
58
|
+
},
|
|
55
59
|
"./execution-mode": {
|
|
56
60
|
"types": "./dist/execution-mode.d.ts",
|
|
57
61
|
"import": "./dist/execution-mode.js"
|
|
@@ -103,8 +107,8 @@
|
|
|
103
107
|
"typescript": "^5.5.0",
|
|
104
108
|
"vitest": "^2.1.8",
|
|
105
109
|
"@posthog/git": "1.0.0",
|
|
106
|
-
"@posthog/
|
|
107
|
-
"@posthog/
|
|
110
|
+
"@posthog/enricher": "1.0.0",
|
|
111
|
+
"@posthog/shared": "1.0.0"
|
|
108
112
|
},
|
|
109
113
|
"dependencies": {
|
|
110
114
|
"@agentclientprotocol/sdk": "0.19.0",
|
|
@@ -72,6 +72,7 @@ import type { EnrichedReadCache } from "./hooks";
|
|
|
72
72
|
import {
|
|
73
73
|
fetchMcpToolMetadata,
|
|
74
74
|
getConnectedMcpServerNames,
|
|
75
|
+
setMcpToolApprovalStates,
|
|
75
76
|
} from "./mcp/tool-metadata";
|
|
76
77
|
import { canUseTool } from "./permissions/permission-handlers";
|
|
77
78
|
import { getAvailableSlashCommands } from "./session/commands";
|
|
@@ -1091,6 +1092,10 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
1091
1092
|
: {};
|
|
1092
1093
|
const systemPrompt = buildSystemPrompt(meta?.systemPrompt);
|
|
1093
1094
|
|
|
1095
|
+
if (meta?.mcpToolApprovals) {
|
|
1096
|
+
setMcpToolApprovalStates(meta.mcpToolApprovals);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1094
1099
|
// Configure structured output via SDK's native outputFormat
|
|
1095
1100
|
const outputFormat =
|
|
1096
1101
|
meta?.jsonSchema && this.options?.onStructuredOutput
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
clearMcpToolMetadataCache,
|
|
4
|
+
getMcpToolApprovalState,
|
|
5
|
+
getMcpToolMetadata,
|
|
6
|
+
isMcpToolReadOnly,
|
|
7
|
+
sanitizeMcpServerName,
|
|
8
|
+
setMcpToolApprovalStates,
|
|
9
|
+
} from "./tool-metadata";
|
|
10
|
+
|
|
11
|
+
describe("tool-metadata approval states", () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
clearMcpToolMetadataCache();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe("setMcpToolApprovalStates", () => {
|
|
17
|
+
it("creates entries for unknown tools", () => {
|
|
18
|
+
setMcpToolApprovalStates({
|
|
19
|
+
mcp__server__tool1: "approved",
|
|
20
|
+
mcp__server__tool2: "do_not_use",
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
expect(getMcpToolApprovalState("mcp__server__tool1")).toBe("approved");
|
|
24
|
+
expect(getMcpToolApprovalState("mcp__server__tool2")).toBe("do_not_use");
|
|
25
|
+
|
|
26
|
+
const meta = getMcpToolMetadata("mcp__server__tool1");
|
|
27
|
+
expect(meta).toBeDefined();
|
|
28
|
+
expect(meta?.readOnly).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("merges with existing entries preserving readOnly", () => {
|
|
32
|
+
setMcpToolApprovalStates({
|
|
33
|
+
mcp__server__ro_tool: "needs_approval",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const before = getMcpToolMetadata("mcp__server__ro_tool");
|
|
37
|
+
expect(before?.readOnly).toBe(false);
|
|
38
|
+
expect(before?.approvalState).toBe("needs_approval");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("updates approval state on existing entries without overwriting other fields", () => {
|
|
42
|
+
setMcpToolApprovalStates({
|
|
43
|
+
mcp__server__tool: "approved",
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
setMcpToolApprovalStates({
|
|
47
|
+
mcp__server__tool: "do_not_use",
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
expect(getMcpToolApprovalState("mcp__server__tool")).toBe("do_not_use");
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
describe("getMcpToolApprovalState", () => {
|
|
55
|
+
it("returns undefined for unknown tools", () => {
|
|
56
|
+
expect(getMcpToolApprovalState("mcp__server__unknown")).toBeUndefined();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("returns the correct state", () => {
|
|
60
|
+
setMcpToolApprovalStates({
|
|
61
|
+
mcp__s__t: "needs_approval",
|
|
62
|
+
});
|
|
63
|
+
expect(getMcpToolApprovalState("mcp__s__t")).toBe("needs_approval");
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("isMcpToolReadOnly with approval states", () => {
|
|
68
|
+
it("returns false for tools that only have approval state", () => {
|
|
69
|
+
setMcpToolApprovalStates({
|
|
70
|
+
mcp__server__tool: "approved",
|
|
71
|
+
});
|
|
72
|
+
expect(isMcpToolReadOnly("mcp__server__tool")).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe("sanitizeMcpServerName", () => {
|
|
77
|
+
it("passes through simple alphanumeric names", () => {
|
|
78
|
+
expect(sanitizeMcpServerName("HubSpot")).toBe("HubSpot");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("replaces spaces with underscores", () => {
|
|
82
|
+
expect(sanitizeMcpServerName("My Server")).toBe("My_Server");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("replaces special characters with underscores", () => {
|
|
86
|
+
expect(sanitizeMcpServerName("server@v2.0!")).toBe("server_v2_0_");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("preserves hyphens and underscores", () => {
|
|
90
|
+
expect(sanitizeMcpServerName("my-server_v2")).toBe("my-server_v2");
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import type { McpServerStatus, Query } from "@anthropic-ai/claude-agent-sdk";
|
|
2
2
|
import { Logger } from "../../../utils/logger";
|
|
3
3
|
|
|
4
|
+
export type McpToolApprovalState = "approved" | "needs_approval" | "do_not_use";
|
|
5
|
+
|
|
6
|
+
/** Maps MCP tool keys (e.g. `mcp__server__tool`) to their backend approval state. */
|
|
7
|
+
export type McpToolApprovals = Record<string, McpToolApprovalState>;
|
|
8
|
+
|
|
4
9
|
export interface McpToolMetadata {
|
|
5
10
|
readOnly: boolean;
|
|
6
11
|
name: string;
|
|
7
12
|
description?: string;
|
|
13
|
+
approvalState?: McpToolApprovalState;
|
|
8
14
|
}
|
|
9
15
|
|
|
10
16
|
const mcpToolMetadataCache: Map<string, McpToolMetadata> = new Map();
|
|
@@ -12,6 +18,10 @@ const mcpToolMetadataCache: Map<string, McpToolMetadata> = new Map();
|
|
|
12
18
|
const PENDING_RETRY_INTERVAL_MS = 1_000;
|
|
13
19
|
const PENDING_MAX_RETRIES = 10;
|
|
14
20
|
|
|
21
|
+
export function sanitizeMcpServerName(name: string): string {
|
|
22
|
+
return name.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
23
|
+
}
|
|
24
|
+
|
|
15
25
|
function buildToolKey(serverName: string, toolName: string): string {
|
|
16
26
|
return `mcp__${serverName}__${toolName}`;
|
|
17
27
|
}
|
|
@@ -49,10 +59,12 @@ export async function fetchMcpToolMetadata(
|
|
|
49
59
|
const toolKey = buildToolKey(server.name, tool.name);
|
|
50
60
|
const readOnly = tool.annotations?.readOnly === true;
|
|
51
61
|
|
|
62
|
+
const existing = mcpToolMetadataCache.get(toolKey);
|
|
52
63
|
mcpToolMetadataCache.set(toolKey, {
|
|
53
64
|
readOnly,
|
|
54
65
|
name: tool.name,
|
|
55
66
|
description: tool.description,
|
|
67
|
+
approvalState: existing?.approvalState,
|
|
56
68
|
});
|
|
57
69
|
if (readOnly) readOnlyCount++;
|
|
58
70
|
}
|
|
@@ -104,6 +116,27 @@ export function getConnectedMcpServerNames(): string[] {
|
|
|
104
116
|
return [...names];
|
|
105
117
|
}
|
|
106
118
|
|
|
119
|
+
export function getMcpToolApprovalState(
|
|
120
|
+
toolName: string,
|
|
121
|
+
): McpToolApprovalState | undefined {
|
|
122
|
+
return mcpToolMetadataCache.get(toolName)?.approvalState;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function setMcpToolApprovalStates(approvals: McpToolApprovals): void {
|
|
126
|
+
for (const [toolKey, approvalState] of Object.entries(approvals)) {
|
|
127
|
+
const existing = mcpToolMetadataCache.get(toolKey);
|
|
128
|
+
if (existing) {
|
|
129
|
+
existing.approvalState = approvalState;
|
|
130
|
+
} else {
|
|
131
|
+
mcpToolMetadataCache.set(toolKey, {
|
|
132
|
+
readOnly: false,
|
|
133
|
+
name: toolKey,
|
|
134
|
+
approvalState,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
107
140
|
export function clearMcpToolMetadataCache(): void {
|
|
108
141
|
mcpToolMetadataCache.clear();
|
|
109
142
|
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
clearMcpToolMetadataCache,
|
|
4
|
+
setMcpToolApprovalStates,
|
|
5
|
+
} from "../mcp/tool-metadata";
|
|
6
|
+
import { canUseTool } from "./permission-handlers";
|
|
7
|
+
|
|
8
|
+
function createContext(
|
|
9
|
+
toolName: string,
|
|
10
|
+
overrides: Record<string, unknown> = {},
|
|
11
|
+
) {
|
|
12
|
+
return {
|
|
13
|
+
session: {
|
|
14
|
+
permissionMode: "default" as const,
|
|
15
|
+
settingsManager: {
|
|
16
|
+
getRepoRoot: vi.fn().mockReturnValue("/repo"),
|
|
17
|
+
},
|
|
18
|
+
...((overrides.session as Record<string, unknown>) ?? {}),
|
|
19
|
+
},
|
|
20
|
+
toolName,
|
|
21
|
+
toolInput: {},
|
|
22
|
+
toolUseID: "test-tool-use-id",
|
|
23
|
+
suggestions: undefined,
|
|
24
|
+
signal: undefined,
|
|
25
|
+
client: {
|
|
26
|
+
sessionUpdate: vi.fn().mockResolvedValue(undefined),
|
|
27
|
+
requestPermission: vi.fn().mockResolvedValue({
|
|
28
|
+
outcome: { outcome: "selected", optionId: "allow" },
|
|
29
|
+
}),
|
|
30
|
+
},
|
|
31
|
+
sessionId: "test-session",
|
|
32
|
+
fileContentCache: {},
|
|
33
|
+
logger: {
|
|
34
|
+
info: vi.fn(),
|
|
35
|
+
warn: vi.fn(),
|
|
36
|
+
error: vi.fn(),
|
|
37
|
+
debug: vi.fn(),
|
|
38
|
+
},
|
|
39
|
+
updateConfigOption: vi.fn().mockResolvedValue(undefined),
|
|
40
|
+
...overrides,
|
|
41
|
+
} as unknown as Parameters<typeof canUseTool>[0];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe("canUseTool MCP approval enforcement", () => {
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
clearMcpToolMetadataCache();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("denies do_not_use MCP tools with correct message", async () => {
|
|
50
|
+
setMcpToolApprovalStates({
|
|
51
|
+
mcp__server__blocked_tool: "do_not_use",
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const result = await canUseTool(createContext("mcp__server__blocked_tool"));
|
|
55
|
+
|
|
56
|
+
expect(result.behavior).toBe("deny");
|
|
57
|
+
if (result.behavior === "deny") {
|
|
58
|
+
expect(result.message).toContain("Settings > MCP Servers");
|
|
59
|
+
expect(result.message).toContain("PostHog Code");
|
|
60
|
+
expect(result.interrupt).toBe(false);
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("routes needs_approval MCP tools to permission dialog with descriptive title", async () => {
|
|
65
|
+
setMcpToolApprovalStates({
|
|
66
|
+
mcp__HubSpot__search_crm_objects: "needs_approval",
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const context = createContext("mcp__HubSpot__search_crm_objects");
|
|
70
|
+
const result = await canUseTool(context);
|
|
71
|
+
|
|
72
|
+
expect(result.behavior).toBe("allow");
|
|
73
|
+
expect(context.client.requestPermission).toHaveBeenCalledWith(
|
|
74
|
+
expect.objectContaining({
|
|
75
|
+
toolCall: expect.objectContaining({
|
|
76
|
+
title: "The agent wants to call search_crm_objects (HubSpot)",
|
|
77
|
+
}),
|
|
78
|
+
}),
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("allows approved MCP tools through normal flow", async () => {
|
|
83
|
+
setMcpToolApprovalStates({
|
|
84
|
+
mcp__server__approved_tool: "approved",
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const result = await canUseTool(
|
|
88
|
+
createContext("mcp__server__approved_tool"),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// Approved falls through to isToolAllowedForMode; MCP tools without
|
|
92
|
+
// readOnly annotation are not auto-allowed, so they go to the default
|
|
93
|
+
// permission flow which calls requestPermission
|
|
94
|
+
expect(result.behavior).toBe("allow");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("falls through for MCP tools with no approval state", async () => {
|
|
98
|
+
const context = createContext("mcp__server__unknown_tool");
|
|
99
|
+
const result = await canUseTool(context);
|
|
100
|
+
|
|
101
|
+
// No approval state → falls through to isToolAllowedForMode → not allowed
|
|
102
|
+
// in default mode → goes to default permission flow
|
|
103
|
+
expect(result.behavior).toBe("allow");
|
|
104
|
+
expect(context.client.requestPermission).toHaveBeenCalled();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("blocks do_not_use even on read-only MCP tools", async () => {
|
|
108
|
+
setMcpToolApprovalStates({
|
|
109
|
+
mcp__server__readonly_blocked: "do_not_use",
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const result = await canUseTool(
|
|
113
|
+
createContext("mcp__server__readonly_blocked"),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
expect(result.behavior).toBe("deny");
|
|
117
|
+
if (result.behavior === "deny") {
|
|
118
|
+
expect(result.message).toContain("blocked");
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("blocks do_not_use even in bypassPermissions mode", async () => {
|
|
123
|
+
setMcpToolApprovalStates({
|
|
124
|
+
mcp__server__blocked_bypass: "do_not_use",
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const result = await canUseTool(
|
|
128
|
+
createContext("mcp__server__blocked_bypass", {
|
|
129
|
+
session: { permissionMode: "bypassPermissions" },
|
|
130
|
+
}),
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
expect(result.behavior).toBe("deny");
|
|
134
|
+
if (result.behavior === "deny") {
|
|
135
|
+
expect(result.message).toContain("blocked");
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("does not affect non-MCP tools", async () => {
|
|
140
|
+
const result = await canUseTool(createContext("Read"));
|
|
141
|
+
|
|
142
|
+
// Read is in the auto-allowed set for default mode
|
|
143
|
+
expect(result.behavior).toBe("allow");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("emits tool denial notification for do_not_use", async () => {
|
|
147
|
+
setMcpToolApprovalStates({
|
|
148
|
+
mcp__server__denied_tool: "do_not_use",
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const context = createContext("mcp__server__denied_tool");
|
|
152
|
+
await canUseTool(context);
|
|
153
|
+
|
|
154
|
+
expect(context.client.sessionUpdate).toHaveBeenCalledWith(
|
|
155
|
+
expect.objectContaining({
|
|
156
|
+
sessionId: "test-session",
|
|
157
|
+
update: expect.objectContaining({
|
|
158
|
+
sessionUpdate: "tool_call_update",
|
|
159
|
+
toolCallId: "test-tool-use-id",
|
|
160
|
+
status: "failed",
|
|
161
|
+
}),
|
|
162
|
+
}),
|
|
163
|
+
);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -9,6 +9,10 @@ import type {
|
|
|
9
9
|
import { text } from "../../../utils/acp-content";
|
|
10
10
|
import type { Logger } from "../../../utils/logger";
|
|
11
11
|
import { toolInfoFromToolUse } from "../conversion/tool-use-to-acp";
|
|
12
|
+
import {
|
|
13
|
+
getMcpToolApprovalState,
|
|
14
|
+
getMcpToolMetadata,
|
|
15
|
+
} from "../mcp/tool-metadata";
|
|
12
16
|
import {
|
|
13
17
|
getClaudePlansDir,
|
|
14
18
|
getLatestAssistantText,
|
|
@@ -408,6 +412,92 @@ async function handleDefaultPermissionFlow(
|
|
|
408
412
|
}
|
|
409
413
|
}
|
|
410
414
|
|
|
415
|
+
function parseMcpToolName(toolName: string): {
|
|
416
|
+
serverName: string;
|
|
417
|
+
tool: string;
|
|
418
|
+
} {
|
|
419
|
+
const parts = toolName.split("__");
|
|
420
|
+
return {
|
|
421
|
+
serverName: parts[1] ?? toolName,
|
|
422
|
+
tool: parts.slice(2).join("__") || toolName,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async function handleMcpApprovalFlow(
|
|
427
|
+
context: ToolHandlerContext,
|
|
428
|
+
): Promise<ToolPermissionResult> {
|
|
429
|
+
const { toolName, toolInput, toolUseID, client, sessionId } = context;
|
|
430
|
+
|
|
431
|
+
const { serverName, tool: displayTool } = parseMcpToolName(toolName);
|
|
432
|
+
const metadata = getMcpToolMetadata(toolName);
|
|
433
|
+
const description = metadata?.description
|
|
434
|
+
? `\n\n${metadata.description}`
|
|
435
|
+
: "";
|
|
436
|
+
|
|
437
|
+
const response = await client.requestPermission({
|
|
438
|
+
options: [
|
|
439
|
+
{ kind: "allow_once", name: "Yes", optionId: "allow" },
|
|
440
|
+
{
|
|
441
|
+
kind: "allow_always",
|
|
442
|
+
name: "Yes, always allow",
|
|
443
|
+
optionId: "allow_always",
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
kind: "reject_once",
|
|
447
|
+
name: "Type here to tell the agent what to do differently",
|
|
448
|
+
optionId: "reject",
|
|
449
|
+
_meta: { customInput: true },
|
|
450
|
+
},
|
|
451
|
+
],
|
|
452
|
+
sessionId,
|
|
453
|
+
toolCall: {
|
|
454
|
+
toolCallId: toolUseID,
|
|
455
|
+
title: `The agent wants to call ${displayTool} (${serverName})`,
|
|
456
|
+
kind: "other",
|
|
457
|
+
content: description
|
|
458
|
+
? [{ type: "content" as const, content: text(description) }]
|
|
459
|
+
: [],
|
|
460
|
+
rawInput: { ...(toolInput as Record<string, unknown>), toolName },
|
|
461
|
+
},
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
if (context.signal?.aborted || response.outcome?.outcome === "cancelled") {
|
|
465
|
+
throw new Error("Tool use aborted");
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
if (
|
|
469
|
+
response.outcome?.outcome === "selected" &&
|
|
470
|
+
(response.outcome.optionId === "allow" ||
|
|
471
|
+
response.outcome.optionId === "allow_always")
|
|
472
|
+
) {
|
|
473
|
+
if (response.outcome.optionId === "allow_always") {
|
|
474
|
+
return {
|
|
475
|
+
behavior: "allow",
|
|
476
|
+
updatedInput: toolInput as Record<string, unknown>,
|
|
477
|
+
updatedPermissions: [
|
|
478
|
+
{
|
|
479
|
+
type: "addRules",
|
|
480
|
+
rules: [{ toolName }],
|
|
481
|
+
behavior: "allow",
|
|
482
|
+
destination: "localSettings",
|
|
483
|
+
},
|
|
484
|
+
],
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
return {
|
|
488
|
+
behavior: "allow",
|
|
489
|
+
updatedInput: toolInput as Record<string, unknown>,
|
|
490
|
+
};
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
const feedback = (response._meta?.customInput as string | undefined)?.trim();
|
|
494
|
+
const message = feedback
|
|
495
|
+
? `User refused permission to run tool with feedback: ${feedback}`
|
|
496
|
+
: "User refused permission to run tool";
|
|
497
|
+
await emitToolDenial(context, message);
|
|
498
|
+
return { behavior: "deny", message, interrupt: !feedback };
|
|
499
|
+
}
|
|
500
|
+
|
|
411
501
|
function handlePlanFileException(
|
|
412
502
|
context: ToolHandlerContext,
|
|
413
503
|
): ToolPermissionResult | null {
|
|
@@ -510,6 +600,21 @@ export async function canUseTool(
|
|
|
510
600
|
}
|
|
511
601
|
}
|
|
512
602
|
|
|
603
|
+
if (toolName.startsWith("mcp__")) {
|
|
604
|
+
const approvalState = getMcpToolApprovalState(toolName);
|
|
605
|
+
|
|
606
|
+
if (approvalState === "do_not_use") {
|
|
607
|
+
const message =
|
|
608
|
+
"This tool has been blocked. To re-enable it, go to Settings > MCP Servers in PostHog Code.";
|
|
609
|
+
await emitToolDenial(context, message);
|
|
610
|
+
return { behavior: "deny", message, interrupt: false };
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
if (approvalState === "needs_approval") {
|
|
614
|
+
return handleMcpApprovalFlow(context);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
513
618
|
if (isToolAllowedForMode(toolName, session.permissionMode)) {
|
|
514
619
|
return {
|
|
515
620
|
behavior: "allow",
|
|
@@ -16,4 +16,12 @@ Only enter plan mode (EnterPlanMode) when the user is requesting a significant c
|
|
|
16
16
|
When in doubt, continue executing and incorporate the feedback inline.
|
|
17
17
|
`;
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
const MCP_TOOLS = `
|
|
20
|
+
# MCP Tool Access
|
|
21
|
+
|
|
22
|
+
If an MCP tool call is explicitly denied with a message, relay that denial message to the user exactly as given. Do NOT suggest checking "Claude Code settings."
|
|
23
|
+
|
|
24
|
+
If an MCP tool call returns an error, treat it as a normal tool error — troubleshoot, retry, or inform the user about the specific error. Do NOT assume it is a permissions issue and do NOT direct the user to any settings page.
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
export const APPENDED_INSTRUCTIONS = BRANCH_NAMING + PLAN_MODE + MCP_TOOLS;
|
|
@@ -10,6 +10,7 @@ import type {
|
|
|
10
10
|
} from "@anthropic-ai/claude-agent-sdk";
|
|
11
11
|
import type { Pushable } from "../../utils/streams";
|
|
12
12
|
import type { BaseSession } from "../base-acp-agent";
|
|
13
|
+
import type { McpToolApprovals } from "./mcp/tool-metadata";
|
|
13
14
|
import type { SettingsManager } from "./session/settings";
|
|
14
15
|
import type { CodeExecutionMode } from "./tools";
|
|
15
16
|
|
|
@@ -117,6 +118,7 @@ export type NewSessionMeta = {
|
|
|
117
118
|
/** Model ID to use for this session (e.g. "claude-sonnet-4-6") */
|
|
118
119
|
model?: string;
|
|
119
120
|
jsonSchema?: Record<string, unknown> | null;
|
|
121
|
+
mcpToolApprovals?: McpToolApprovals;
|
|
120
122
|
claudeCode?: {
|
|
121
123
|
options?: Options;
|
|
122
124
|
emitRawSDKMessages?: boolean | SDKMessageFilter[];
|
|
@@ -179,5 +179,6 @@ describe("HandoffCheckpointTracker", () => {
|
|
|
179
179
|
expect(status).toContain("M tracked.txt");
|
|
180
180
|
expect(status).toContain(" M unstaged.txt");
|
|
181
181
|
expect(status).toContain("?? untracked.txt");
|
|
182
|
+
expect(localRepo.exists(".posthog/tmp")).toBe(false);
|
|
182
183
|
});
|
|
183
184
|
});
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
mkdir,
|
|
3
|
+
readdir,
|
|
4
|
+
readFile,
|
|
5
|
+
rm,
|
|
6
|
+
rmdir,
|
|
7
|
+
writeFile,
|
|
8
|
+
} from "node:fs/promises";
|
|
2
9
|
import { join } from "node:path";
|
|
3
10
|
import {
|
|
4
11
|
type GitHandoffBranchDivergence,
|
|
@@ -161,6 +168,7 @@ export class HandoffCheckpointTracker {
|
|
|
161
168
|
} finally {
|
|
162
169
|
await this.removeIfPresent(packPath);
|
|
163
170
|
await this.removeIfPresent(indexPath);
|
|
171
|
+
await this.removeTmpDirIfEmpty(tmpDir);
|
|
164
172
|
}
|
|
165
173
|
}
|
|
166
174
|
|
|
@@ -364,4 +372,12 @@ export class HandoffCheckpointTracker {
|
|
|
364
372
|
}
|
|
365
373
|
await rm(filePath, { force: true }).catch(() => {});
|
|
366
374
|
}
|
|
375
|
+
|
|
376
|
+
private async removeTmpDirIfEmpty(tmpDir: string): Promise<void> {
|
|
377
|
+
const entries = await readdir(tmpDir).catch(() => null);
|
|
378
|
+
if (!entries || entries.length > 0) {
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
await rmdir(tmpDir).catch(() => {});
|
|
382
|
+
}
|
|
367
383
|
}
|
|
@@ -328,6 +328,7 @@ describe("ApplySnapshotSaga", () => {
|
|
|
328
328
|
});
|
|
329
329
|
|
|
330
330
|
expect(repo.exists(".posthog/tmp/test-tree-hash.tar.gz")).toBe(false);
|
|
331
|
+
expect(repo.exists(".posthog/tmp")).toBe(false);
|
|
331
332
|
});
|
|
332
333
|
|
|
333
334
|
it("cleans up downloaded archive on checkout failure (rollback verification)", async () => {
|