@posthog/agent 2.3.647 → 2.3.655
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/permissions/permission-options.js +700 -0
- package/dist/adapters/claude/permissions/permission-options.js.map +1 -1
- package/dist/adapters/claude/tools.js +700 -0
- package/dist/adapters/claude/tools.js.map +1 -1
- package/dist/adapters/codex/local-tools-mcp-server.d.ts +2 -0
- package/dist/adapters/codex/local-tools-mcp-server.js +1172 -0
- package/dist/adapters/codex/local-tools-mcp-server.js.map +1 -0
- package/dist/agent.js +1488 -219
- package/dist/agent.js.map +1 -1
- package/dist/execution-mode.js +700 -0
- package/dist/execution-mode.js.map +1 -1
- package/dist/handoff-checkpoint.js.map +1 -1
- package/dist/posthog-api.js +1 -1
- package/dist/posthog-api.js.map +1 -1
- package/dist/server/agent-server.js +1604 -339
- package/dist/server/agent-server.js.map +1 -1
- package/dist/server/bin.cjs +1520 -258
- package/dist/server/bin.cjs.map +1 -1
- package/package.json +3 -3
- package/src/adapters/claude/claude-agent.ts +32 -2
- package/src/adapters/claude/hooks.test.ts +54 -0
- package/src/adapters/claude/hooks.ts +86 -0
- package/src/adapters/claude/mcp/local-tools.test.ts +50 -0
- package/src/adapters/claude/mcp/local-tools.ts +40 -0
- package/src/adapters/claude/session/options.ts +14 -9
- package/src/adapters/claude/types.ts +1 -0
- package/src/adapters/codex/codex-agent.ts +117 -22
- package/src/adapters/codex/local-tools-mcp-server.ts +71 -0
- package/src/adapters/local-tools/index.ts +22 -0
- package/src/adapters/local-tools/registry.test.ts +57 -0
- package/src/adapters/local-tools/registry.ts +81 -0
- package/src/adapters/local-tools/tools/signed-commit.ts +26 -0
- package/src/adapters/session-meta.ts +16 -0
- package/src/adapters/signed-commit-shared.ts +82 -0
- package/src/server/agent-server.test.ts +2 -4
- package/src/server/agent-server.ts +27 -30
- package/src/utils/common.ts +14 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@posthog/agent",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.655",
|
|
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": {
|
|
@@ -107,8 +107,8 @@
|
|
|
107
107
|
"typescript": "^5.5.0",
|
|
108
108
|
"vitest": "^2.1.8",
|
|
109
109
|
"@posthog/shared": "1.0.0",
|
|
110
|
-
"@posthog/
|
|
111
|
-
"@posthog/
|
|
110
|
+
"@posthog/enricher": "1.0.0",
|
|
111
|
+
"@posthog/git": "1.0.0"
|
|
112
112
|
},
|
|
113
113
|
"dependencies": {
|
|
114
114
|
"@agentclientprotocol/sdk": "0.19.0",
|
|
@@ -57,10 +57,17 @@ import {
|
|
|
57
57
|
type FileEnrichmentDeps,
|
|
58
58
|
} from "../../enrichment/file-enricher";
|
|
59
59
|
import type { PostHogAPIConfig } from "../../types";
|
|
60
|
-
import {
|
|
60
|
+
import {
|
|
61
|
+
isCloudRun,
|
|
62
|
+
resolveGithubToken,
|
|
63
|
+
unreachable,
|
|
64
|
+
withTimeout,
|
|
65
|
+
} from "../../utils/common";
|
|
61
66
|
import { Logger } from "../../utils/logger";
|
|
62
67
|
import { Pushable } from "../../utils/streams";
|
|
63
68
|
import { BaseAcpAgent } from "../base-acp-agent";
|
|
69
|
+
import { LOCAL_TOOLS_MCP_NAME } from "../local-tools";
|
|
70
|
+
import { resolveTaskId } from "../session-meta";
|
|
64
71
|
import { promptToClaude } from "./conversion/acp-to-sdk";
|
|
65
72
|
import {
|
|
66
73
|
handleResultMessage,
|
|
@@ -69,6 +76,7 @@ import {
|
|
|
69
76
|
handleUserAssistantMessage,
|
|
70
77
|
} from "./conversion/sdk-to-acp";
|
|
71
78
|
import type { EnrichedReadCache } from "./hooks";
|
|
79
|
+
import { createLocalToolsMcpServer } from "./mcp/local-tools";
|
|
72
80
|
import {
|
|
73
81
|
fetchMcpToolMetadata,
|
|
74
82
|
getConnectedMcpServerNames,
|
|
@@ -1091,7 +1099,10 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
1091
1099
|
const isResume = !!resume;
|
|
1092
1100
|
|
|
1093
1101
|
const meta = params._meta as NewSessionMeta | undefined;
|
|
1094
|
-
const taskId = meta
|
|
1102
|
+
const taskId = resolveTaskId(meta);
|
|
1103
|
+
// Gate signed-commit wiring on cloud-run detection so the desktop (which
|
|
1104
|
+
// signs via CommitSaga) is untouched.
|
|
1105
|
+
const cloudRun = isCloudRun(meta);
|
|
1095
1106
|
const effort = meta?.claudeCode?.options?.effort as EffortLevel | undefined;
|
|
1096
1107
|
|
|
1097
1108
|
// We want to create a new session id unless it is resume,
|
|
@@ -1115,6 +1126,24 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
1115
1126
|
const mcpServers = supportsMcpInjection(earlyModelId)
|
|
1116
1127
|
? parseMcpServers(params)
|
|
1117
1128
|
: {};
|
|
1129
|
+
|
|
1130
|
+
// Register the in-process general local-tools MCP server. Tools self-gate
|
|
1131
|
+
// via the registry (e.g. signed-commit is cloud-only and needs a GH token),
|
|
1132
|
+
// so adding a tool needs no change here. In cloud runs `git commit`/`git
|
|
1133
|
+
// push` are blocked by the PreToolUse guard (and the sandbox git shim), so
|
|
1134
|
+
// the agent commits via the signed-commit tool instead.
|
|
1135
|
+
const localToolsServer = createLocalToolsMcpServer(
|
|
1136
|
+
{ cwd, token: resolveGithubToken(), taskId },
|
|
1137
|
+
meta,
|
|
1138
|
+
);
|
|
1139
|
+
if (localToolsServer) {
|
|
1140
|
+
mcpServers[LOCAL_TOOLS_MCP_NAME] = localToolsServer;
|
|
1141
|
+
} else if (cloudRun) {
|
|
1142
|
+
this.logger.warn(
|
|
1143
|
+
"Cloud run registered no local tools — missing GH_TOKEN/GITHUB_TOKEN? signed commits unavailable",
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1118
1147
|
const systemPrompt = buildSystemPrompt(meta?.systemPrompt);
|
|
1119
1148
|
|
|
1120
1149
|
if (meta?.mcpToolApprovals) {
|
|
@@ -1164,6 +1193,7 @@ export class ClaudeAcpAgent extends BaseAcpAgent {
|
|
|
1164
1193
|
effort,
|
|
1165
1194
|
enrichmentDeps: this.enrichment?.deps,
|
|
1166
1195
|
enrichedReadCache: this.enrichedReadCache,
|
|
1196
|
+
cloudMode: cloudRun,
|
|
1167
1197
|
});
|
|
1168
1198
|
|
|
1169
1199
|
// Use the same abort controller that buildSessionOptions gave to the query
|
|
@@ -11,6 +11,7 @@ import { Logger } from "../../utils/logger";
|
|
|
11
11
|
import {
|
|
12
12
|
createPreToolUseHook,
|
|
13
13
|
createReadEnrichmentHook,
|
|
14
|
+
createSignedCommitGuardHook,
|
|
14
15
|
type EnrichedReadCache,
|
|
15
16
|
} from "./hooks";
|
|
16
17
|
import type {
|
|
@@ -311,3 +312,56 @@ describe("createPreToolUseHook", () => {
|
|
|
311
312
|
});
|
|
312
313
|
});
|
|
313
314
|
});
|
|
315
|
+
|
|
316
|
+
describe("createSignedCommitGuardHook", () => {
|
|
317
|
+
const logger = new Logger();
|
|
318
|
+
|
|
319
|
+
function bashInput(command: string): HookInput {
|
|
320
|
+
return {
|
|
321
|
+
session_id: "s",
|
|
322
|
+
transcript_path: "/tmp/t",
|
|
323
|
+
cwd: "/tmp",
|
|
324
|
+
hook_event_name: "PreToolUse",
|
|
325
|
+
tool_name: "Bash",
|
|
326
|
+
tool_use_id: "toolu_1",
|
|
327
|
+
tool_input: { command },
|
|
328
|
+
} as HookInput;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const guard = createSignedCommitGuardHook(logger);
|
|
332
|
+
const opts = { signal: new AbortController().signal };
|
|
333
|
+
|
|
334
|
+
test.each([
|
|
335
|
+
"git commit -m x",
|
|
336
|
+
"git push origin main",
|
|
337
|
+
"git add . && git commit -m 'y'",
|
|
338
|
+
"git -C /repo commit",
|
|
339
|
+
"git --no-pager push",
|
|
340
|
+
])("denies %s", async (command) => {
|
|
341
|
+
const result = await guard(bashInput(command), undefined, opts);
|
|
342
|
+
expect(result).toMatchObject({
|
|
343
|
+
hookSpecificOutput: { permissionDecision: "deny" },
|
|
344
|
+
});
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
test.each([
|
|
348
|
+
"git status",
|
|
349
|
+
"git add .",
|
|
350
|
+
"git fetch origin",
|
|
351
|
+
"git log --grep=commit",
|
|
352
|
+
"git stash push",
|
|
353
|
+
"git ls-remote --heads origin x",
|
|
354
|
+
])("allows %s", async (command) => {
|
|
355
|
+
const result = await guard(bashInput(command), undefined, opts);
|
|
356
|
+
expect(result).toEqual({ continue: true });
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
test("ignores non-Bash tools", async () => {
|
|
360
|
+
const result = await guard(
|
|
361
|
+
{ ...bashInput("git commit"), tool_name: "Read" } as HookInput,
|
|
362
|
+
undefined,
|
|
363
|
+
opts,
|
|
364
|
+
);
|
|
365
|
+
expect(result).toEqual({ continue: true });
|
|
366
|
+
});
|
|
367
|
+
});
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
type FileEnrichmentDeps,
|
|
5
5
|
} from "../../enrichment/file-enricher";
|
|
6
6
|
import type { Logger } from "../../utils/logger";
|
|
7
|
+
import { SIGNED_COMMIT_QUALIFIED_TOOL_NAME } from "../signed-commit-shared";
|
|
7
8
|
import { stripCatLineNumbers } from "./conversion/sdk-to-acp";
|
|
8
9
|
import {
|
|
9
10
|
extractPostHogSubTool,
|
|
@@ -222,6 +223,91 @@ export const createSubagentRewriteHook =
|
|
|
222
223
|
};
|
|
223
224
|
};
|
|
224
225
|
|
|
226
|
+
// git global options that consume the following token as their value, so the
|
|
227
|
+
// subcommand detector must skip both (mirrors the sandbox `git` PATH shim).
|
|
228
|
+
const GIT_VALUE_FLAGS = new Set([
|
|
229
|
+
"-C",
|
|
230
|
+
"-c",
|
|
231
|
+
"--git-dir",
|
|
232
|
+
"--work-tree",
|
|
233
|
+
"--namespace",
|
|
234
|
+
"--exec-path",
|
|
235
|
+
]);
|
|
236
|
+
|
|
237
|
+
function gitSubcommand(segment: string): string | null {
|
|
238
|
+
const tokens = segment.trim().split(/\s+/).filter(Boolean);
|
|
239
|
+
if (tokens.length === 0) return null;
|
|
240
|
+
// Strip a leading path so `/usr/bin/git` is still recognised as git.
|
|
241
|
+
const head = tokens[0].split("/").pop();
|
|
242
|
+
if (head !== "git") return null;
|
|
243
|
+
|
|
244
|
+
let skipNext = false;
|
|
245
|
+
for (const tok of tokens.slice(1)) {
|
|
246
|
+
if (skipNext) {
|
|
247
|
+
skipNext = false;
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (GIT_VALUE_FLAGS.has(tok)) {
|
|
251
|
+
skipNext = true;
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
if (tok.startsWith("-")) continue;
|
|
255
|
+
return tok;
|
|
256
|
+
}
|
|
257
|
+
return null;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* True when any top-level shell segment of `command` is a direct `git commit` /
|
|
262
|
+
* `git push` invocation (allowing `git`-level global flags like `-C path` or
|
|
263
|
+
* `--no-pager`). Does not match subcommands such as `git stash push` or
|
|
264
|
+
* `git log --grep=commit`. Git reached via command substitution (`$(git push)`)
|
|
265
|
+
* is not caught here — the sandbox `git` PATH shim is the authoritative backstop;
|
|
266
|
+
* this hook is a fast in-band deny with a helpful message.
|
|
267
|
+
*/
|
|
268
|
+
function blocksUnsignedGit(command: string): boolean {
|
|
269
|
+
// Cheap reject for the overwhelmingly common non-git Bash call before splitting.
|
|
270
|
+
if (!command.includes("git")) return false;
|
|
271
|
+
return command.split(/&&|\|\||[;\n|]/).some((segment) => {
|
|
272
|
+
const sub = gitSubcommand(segment);
|
|
273
|
+
return sub === "commit" || sub === "push";
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Cloud-only guard: blocks raw `git commit` / `git push` so unsigned commits
|
|
279
|
+
* cannot leave the sandbox. The agent must use the `git_signed_commit` tool,
|
|
280
|
+
* which creates GitHub-signed (Verified) commits via the API.
|
|
281
|
+
*/
|
|
282
|
+
export const createSignedCommitGuardHook =
|
|
283
|
+
(logger: Logger): HookCallback =>
|
|
284
|
+
async (input: HookInput, _toolUseID: string | undefined) => {
|
|
285
|
+
if (input.hook_event_name !== "PreToolUse") return { continue: true };
|
|
286
|
+
if (input.tool_name !== "Bash") return { continue: true };
|
|
287
|
+
|
|
288
|
+
const command = (input.tool_input as { command?: string } | undefined)
|
|
289
|
+
?.command;
|
|
290
|
+
if (!command || !blocksUnsignedGit(command)) {
|
|
291
|
+
return { continue: true };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
logger.info(
|
|
295
|
+
`[SignedCommitGuard] Blocking unsigned git command: ${command}`,
|
|
296
|
+
);
|
|
297
|
+
return {
|
|
298
|
+
continue: true,
|
|
299
|
+
hookSpecificOutput: {
|
|
300
|
+
hookEventName: "PreToolUse" as const,
|
|
301
|
+
permissionDecision: "deny" as const,
|
|
302
|
+
permissionDecisionReason:
|
|
303
|
+
"Commits must be signed: `git commit` and `git push` are disabled here. " +
|
|
304
|
+
"Stage changes with `git add`, then call the `git_signed_commit` tool " +
|
|
305
|
+
`(${SIGNED_COMMIT_QUALIFIED_TOOL_NAME}) with a \`message\` to create a signed ` +
|
|
306
|
+
"commit on the branch.",
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
};
|
|
310
|
+
|
|
225
311
|
export const createPreToolUseHook =
|
|
226
312
|
(settingsManager: SettingsManager, logger: Logger): HookCallback =>
|
|
227
313
|
async (input: HookInput, _toolUseID: string | undefined) => {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
2
|
+
import { InMemoryTransport } from "@modelcontextprotocol/sdk/inMemory.js";
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
4
|
+
import { createLocalToolsMcpServer } from "./local-tools";
|
|
5
|
+
|
|
6
|
+
describe("createLocalToolsMcpServer", () => {
|
|
7
|
+
const savedSandbox = process.env.IS_SANDBOX;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
// isCloudRun also keys off IS_SANDBOX; clear it so the meta arg is the only
|
|
11
|
+
// cloud signal under test.
|
|
12
|
+
delete process.env.IS_SANDBOX;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
if (savedSandbox === undefined) {
|
|
17
|
+
delete process.env.IS_SANDBOX;
|
|
18
|
+
} else {
|
|
19
|
+
process.env.IS_SANDBOX = savedSandbox;
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("returns undefined when no tool's gate passes (desktop run)", () => {
|
|
24
|
+
expect(
|
|
25
|
+
createLocalToolsMcpServer({ cwd: "/repo", token: "ghs_x" }, undefined),
|
|
26
|
+
).toBeUndefined();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("exposes git_signed_commit over MCP in a cloud run with a token", async () => {
|
|
30
|
+
const server = createLocalToolsMcpServer(
|
|
31
|
+
{ cwd: "/repo", token: "ghs_x" },
|
|
32
|
+
{ taskRunId: "run-1" },
|
|
33
|
+
);
|
|
34
|
+
if (!server) {
|
|
35
|
+
throw new Error("expected the local-tools server to be registered");
|
|
36
|
+
}
|
|
37
|
+
expect(server.name).toBe("posthog-local");
|
|
38
|
+
|
|
39
|
+
const [clientTransport, serverTransport] =
|
|
40
|
+
InMemoryTransport.createLinkedPair();
|
|
41
|
+
await server.instance.connect(serverTransport);
|
|
42
|
+
const client = new Client({ name: "test", version: "1.0.0" });
|
|
43
|
+
await client.connect(clientTransport);
|
|
44
|
+
|
|
45
|
+
const { tools } = await client.listTools();
|
|
46
|
+
expect(tools.map((t) => t.name)).toContain("git_signed_commit");
|
|
47
|
+
|
|
48
|
+
await client.close();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createSdkMcpServer,
|
|
3
|
+
type McpSdkServerConfigWithInstance,
|
|
4
|
+
tool,
|
|
5
|
+
} from "@anthropic-ai/claude-agent-sdk";
|
|
6
|
+
import {
|
|
7
|
+
enabledLocalTools,
|
|
8
|
+
LOCAL_TOOLS_MCP_NAME,
|
|
9
|
+
type LocalToolCtx,
|
|
10
|
+
type LocalToolGateMeta,
|
|
11
|
+
} from "../../local-tools";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* In-process SDK MCP server exposing the enabled local tools to the Claude
|
|
15
|
+
* adapter (see `../../local-tools` for the registry). Returns `undefined` when
|
|
16
|
+
* no tool's gate passes, so the caller can skip registering an empty server.
|
|
17
|
+
* Registered per session in `claude-agent.ts`.
|
|
18
|
+
*/
|
|
19
|
+
export function createLocalToolsMcpServer(
|
|
20
|
+
ctx: LocalToolCtx,
|
|
21
|
+
meta: LocalToolGateMeta | undefined,
|
|
22
|
+
): McpSdkServerConfigWithInstance | undefined {
|
|
23
|
+
const tools = enabledLocalTools(ctx, meta);
|
|
24
|
+
if (tools.length === 0) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
return createSdkMcpServer({
|
|
28
|
+
name: LOCAL_TOOLS_MCP_NAME,
|
|
29
|
+
version: "1.0.0",
|
|
30
|
+
tools: tools.map((t) =>
|
|
31
|
+
tool(
|
|
32
|
+
t.name,
|
|
33
|
+
t.description,
|
|
34
|
+
t.schema,
|
|
35
|
+
async (args) => t.handler(ctx, args),
|
|
36
|
+
{ alwaysLoad: t.alwaysLoad ?? false },
|
|
37
|
+
),
|
|
38
|
+
),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
createPostToolUseHook,
|
|
18
18
|
createPreToolUseHook,
|
|
19
19
|
createReadEnrichmentHook,
|
|
20
|
+
createSignedCommitGuardHook,
|
|
20
21
|
createSubagentRewriteHook,
|
|
21
22
|
type EnrichedReadCache,
|
|
22
23
|
type OnModeChange,
|
|
@@ -55,6 +56,8 @@ export interface BuildOptionsParams {
|
|
|
55
56
|
effort?: EffortLevel;
|
|
56
57
|
enrichmentDeps?: FileEnrichmentDeps;
|
|
57
58
|
enrichedReadCache?: EnrichedReadCache;
|
|
59
|
+
/** Cloud task session — enables the signed-commit guard. */
|
|
60
|
+
cloudMode?: boolean;
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
export function buildSystemPrompt(
|
|
@@ -129,6 +132,7 @@ function buildHooks(
|
|
|
129
132
|
enrichmentDeps: FileEnrichmentDeps | undefined,
|
|
130
133
|
enrichedReadCache: EnrichedReadCache | undefined,
|
|
131
134
|
registeredAgents: ReadonlySet<string>,
|
|
135
|
+
cloudMode: boolean,
|
|
132
136
|
): Options["hooks"] {
|
|
133
137
|
const postToolUseHooks = [createPostToolUseHook({ onModeChange })];
|
|
134
138
|
if (enrichmentDeps && enrichedReadCache) {
|
|
@@ -137,21 +141,21 @@ function buildHooks(
|
|
|
137
141
|
);
|
|
138
142
|
}
|
|
139
143
|
|
|
144
|
+
const preToolUseHooks = [
|
|
145
|
+
createPreToolUseHook(settingsManager, logger),
|
|
146
|
+
createSubagentRewriteHook(logger, registeredAgents),
|
|
147
|
+
];
|
|
148
|
+
if (cloudMode) {
|
|
149
|
+
preToolUseHooks.push(createSignedCommitGuardHook(logger));
|
|
150
|
+
}
|
|
151
|
+
|
|
140
152
|
return {
|
|
141
153
|
...userHooks,
|
|
142
154
|
PostToolUse: [
|
|
143
155
|
...(userHooks?.PostToolUse || []),
|
|
144
156
|
{ hooks: postToolUseHooks },
|
|
145
157
|
],
|
|
146
|
-
PreToolUse: [
|
|
147
|
-
...(userHooks?.PreToolUse || []),
|
|
148
|
-
{
|
|
149
|
-
hooks: [
|
|
150
|
-
createPreToolUseHook(settingsManager, logger),
|
|
151
|
-
createSubagentRewriteHook(logger, registeredAgents),
|
|
152
|
-
],
|
|
153
|
-
},
|
|
154
|
-
],
|
|
158
|
+
PreToolUse: [...(userHooks?.PreToolUse || []), { hooks: preToolUseHooks }],
|
|
155
159
|
};
|
|
156
160
|
}
|
|
157
161
|
|
|
@@ -352,6 +356,7 @@ export function buildSessionOptions(params: BuildOptionsParams): Options {
|
|
|
352
356
|
params.enrichmentDeps,
|
|
353
357
|
params.enrichedReadCache,
|
|
354
358
|
registeredAgentNames,
|
|
359
|
+
params.cloudMode ?? false,
|
|
355
360
|
),
|
|
356
361
|
outputFormat: params.outputFormat,
|
|
357
362
|
abortController: getAbortController(
|
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
type SetSessionModeRequest,
|
|
39
39
|
type SetSessionModeResponse,
|
|
40
40
|
} from "@agentclientprotocol/sdk";
|
|
41
|
+
import { ghTokenEnv } from "@posthog/git/signed-commit";
|
|
41
42
|
import packageJson from "../../../package.json" with { type: "json" };
|
|
42
43
|
import {
|
|
43
44
|
isMethod,
|
|
@@ -56,6 +57,7 @@ import {
|
|
|
56
57
|
type PermissionMode,
|
|
57
58
|
} from "../../execution-mode";
|
|
58
59
|
import type { PostHogAPIConfig, ProcessSpawnedCallback } from "../../types";
|
|
60
|
+
import { isCloudRun, resolveGithubToken } from "../../utils/common";
|
|
59
61
|
import { Logger } from "../../utils/logger";
|
|
60
62
|
import {
|
|
61
63
|
nodeReadableToWebReadable,
|
|
@@ -63,6 +65,12 @@ import {
|
|
|
63
65
|
} from "../../utils/streams";
|
|
64
66
|
import { BaseAcpAgent, type BaseSession } from "../base-acp-agent";
|
|
65
67
|
import { classifyAgentError } from "../error-classification";
|
|
68
|
+
import {
|
|
69
|
+
enabledLocalTools,
|
|
70
|
+
LOCAL_TOOLS_MCP_NAME,
|
|
71
|
+
type LocalToolCtx,
|
|
72
|
+
} from "../local-tools";
|
|
73
|
+
import { resolveTaskId } from "../session-meta";
|
|
66
74
|
import { createCodexClient } from "./codex-client";
|
|
67
75
|
import { normalizeCodexConfigOptions } from "./models";
|
|
68
76
|
import {
|
|
@@ -193,8 +201,7 @@ const STRUCTURED_OUTPUT_INSTRUCTIONS = `\n\nWhen you have completed the task, ca
|
|
|
193
201
|
* harness/bin.js, etc), `import.meta.dirname` sits at different depths. Walk
|
|
194
202
|
* up until we find the script so each bundle locates the shared dist asset.
|
|
195
203
|
*/
|
|
196
|
-
function
|
|
197
|
-
const rel = "adapters/codex/structured-output-mcp-server.js";
|
|
204
|
+
function resolveBundledMcpScript(rel: string): string {
|
|
198
205
|
let dir = import.meta.dirname ?? __dirname;
|
|
199
206
|
for (let i = 0; i < 5; i++) {
|
|
200
207
|
const candidate = resolvePath(dir, rel);
|
|
@@ -209,7 +216,9 @@ function resolveStructuredOutputMcpScript(): string {
|
|
|
209
216
|
function buildStructuredOutputMcpServer(
|
|
210
217
|
jsonSchema: Record<string, unknown>,
|
|
211
218
|
): McpServerStdio {
|
|
212
|
-
const scriptPath =
|
|
219
|
+
const scriptPath = resolveBundledMcpScript(
|
|
220
|
+
"adapters/codex/structured-output-mcp-server.js",
|
|
221
|
+
);
|
|
213
222
|
const schemaBase64 = Buffer.from(JSON.stringify(jsonSchema)).toString(
|
|
214
223
|
"base64",
|
|
215
224
|
);
|
|
@@ -221,6 +230,41 @@ function buildStructuredOutputMcpServer(
|
|
|
221
230
|
};
|
|
222
231
|
}
|
|
223
232
|
|
|
233
|
+
/**
|
|
234
|
+
* Builds the stdio MCP server config exposing the enabled local tools. Context
|
|
235
|
+
* (cwd, taskId, token) and the enabled tool names are passed base64/CSV-encoded
|
|
236
|
+
* so the child registers the same tools the Claude adapter exposes in-process.
|
|
237
|
+
*/
|
|
238
|
+
function buildLocalToolsMcpServer(
|
|
239
|
+
ctx: LocalToolCtx,
|
|
240
|
+
enabledNames: string[],
|
|
241
|
+
): McpServerStdio {
|
|
242
|
+
const scriptPath = resolveBundledMcpScript(
|
|
243
|
+
"adapters/codex/local-tools-mcp-server.js",
|
|
244
|
+
);
|
|
245
|
+
const ctxBase64 = Buffer.from(JSON.stringify(ctx)).toString("base64");
|
|
246
|
+
const env = [
|
|
247
|
+
{ name: "POSTHOG_LOCAL_TOOLS_CTX", value: ctxBase64 },
|
|
248
|
+
{ name: "POSTHOG_LOCAL_TOOLS_ENABLED", value: enabledNames.join(",") },
|
|
249
|
+
];
|
|
250
|
+
if (ctx.token) {
|
|
251
|
+
// Token also on the child env so its own git remote ops (fetch/ls-remote)
|
|
252
|
+
// authenticate; the var names come from the single shared source.
|
|
253
|
+
env.push(
|
|
254
|
+
...Object.entries(ghTokenEnv(ctx.token)).map(([name, value]) => ({
|
|
255
|
+
name,
|
|
256
|
+
value,
|
|
257
|
+
})),
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
return {
|
|
261
|
+
name: LOCAL_TOOLS_MCP_NAME,
|
|
262
|
+
command: process.execPath,
|
|
263
|
+
args: [scriptPath],
|
|
264
|
+
env,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
224
268
|
export class CodexAcpAgent extends BaseAcpAgent {
|
|
225
269
|
readonly adapterName = "codex";
|
|
226
270
|
declare session: CodexSession;
|
|
@@ -338,7 +382,10 @@ export class CodexAcpAgent extends BaseAcpAgent {
|
|
|
338
382
|
const meta = params._meta as NewSessionMeta | undefined;
|
|
339
383
|
const requestedPermissionMode = toCodexPermissionMode(meta?.permissionMode);
|
|
340
384
|
|
|
341
|
-
const injectedParams = this.
|
|
385
|
+
const injectedParams = this.applyLocalTools(
|
|
386
|
+
this.applyStructuredOutput(params, meta),
|
|
387
|
+
meta,
|
|
388
|
+
);
|
|
342
389
|
const response = await this.codexConnection.newSession(injectedParams);
|
|
343
390
|
response.configOptions = normalizeCodexConfigOptions(
|
|
344
391
|
response.configOptions,
|
|
@@ -347,7 +394,7 @@ export class CodexAcpAgent extends BaseAcpAgent {
|
|
|
347
394
|
// Initialize session state
|
|
348
395
|
this.sessionState = createSessionState(response.sessionId, params.cwd, {
|
|
349
396
|
taskRunId: meta?.taskRunId,
|
|
350
|
-
taskId: meta
|
|
397
|
+
taskId: resolveTaskId(meta),
|
|
351
398
|
modeId: response.modes?.currentModeId ?? "auto",
|
|
352
399
|
modelId: response.models?.currentModelId,
|
|
353
400
|
permissionMode: requestedPermissionMode,
|
|
@@ -380,7 +427,10 @@ export class CodexAcpAgent extends BaseAcpAgent {
|
|
|
380
427
|
|
|
381
428
|
async loadSession(params: LoadSessionRequest): Promise<LoadSessionResponse> {
|
|
382
429
|
const meta = params._meta as NewSessionMeta | undefined;
|
|
383
|
-
const injectedParams = this.
|
|
430
|
+
const injectedParams = this.applyLocalTools(
|
|
431
|
+
this.applyStructuredOutput(params, meta),
|
|
432
|
+
meta,
|
|
433
|
+
);
|
|
384
434
|
const response = await this.codexConnection.loadSession(injectedParams);
|
|
385
435
|
response.configOptions = normalizeCodexConfigOptions(
|
|
386
436
|
response.configOptions,
|
|
@@ -396,7 +446,7 @@ export class CodexAcpAgent extends BaseAcpAgent {
|
|
|
396
446
|
// not, which silently broke task-completion tracking on re-attach.
|
|
397
447
|
this.sessionState = createSessionState(params.sessionId, params.cwd, {
|
|
398
448
|
taskRunId: meta?.taskRunId,
|
|
399
|
-
taskId: meta
|
|
449
|
+
taskId: resolveTaskId(meta),
|
|
400
450
|
modeId: response.modes?.currentModeId ?? "auto",
|
|
401
451
|
permissionMode: currentPermissionMode,
|
|
402
452
|
});
|
|
@@ -418,13 +468,16 @@ export class CodexAcpAgent extends BaseAcpAgent {
|
|
|
418
468
|
params: ResumeSessionRequest,
|
|
419
469
|
): Promise<ResumeSessionResponse> {
|
|
420
470
|
const meta = params._meta as NewSessionMeta | undefined;
|
|
421
|
-
const injectedParams = this.
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
471
|
+
const injectedParams = this.applyLocalTools(
|
|
472
|
+
this.applyStructuredOutput(
|
|
473
|
+
{
|
|
474
|
+
sessionId: params.sessionId,
|
|
475
|
+
cwd: params.cwd,
|
|
476
|
+
mcpServers: params.mcpServers ?? [],
|
|
477
|
+
_meta: params._meta,
|
|
478
|
+
},
|
|
479
|
+
meta,
|
|
480
|
+
),
|
|
428
481
|
meta,
|
|
429
482
|
);
|
|
430
483
|
|
|
@@ -439,7 +492,7 @@ export class CodexAcpAgent extends BaseAcpAgent {
|
|
|
439
492
|
);
|
|
440
493
|
this.sessionState = createSessionState(params.sessionId, params.cwd, {
|
|
441
494
|
taskRunId: meta?.taskRunId,
|
|
442
|
-
taskId: meta
|
|
495
|
+
taskId: resolveTaskId(meta),
|
|
443
496
|
modeId: loadResponse.modes?.currentModeId ?? "auto",
|
|
444
497
|
permissionMode: currentPermissionMode,
|
|
445
498
|
});
|
|
@@ -465,12 +518,15 @@ export class CodexAcpAgent extends BaseAcpAgent {
|
|
|
465
518
|
params: ForkSessionRequest,
|
|
466
519
|
): Promise<ForkSessionResponse> {
|
|
467
520
|
const meta = params._meta as NewSessionMeta | undefined;
|
|
468
|
-
const injectedParams = this.
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
521
|
+
const injectedParams = this.applyLocalTools(
|
|
522
|
+
this.applyStructuredOutput(
|
|
523
|
+
{
|
|
524
|
+
cwd: params.cwd,
|
|
525
|
+
mcpServers: params.mcpServers ?? [],
|
|
526
|
+
_meta: params._meta,
|
|
527
|
+
},
|
|
528
|
+
meta,
|
|
529
|
+
),
|
|
474
530
|
meta,
|
|
475
531
|
);
|
|
476
532
|
|
|
@@ -483,7 +539,7 @@ export class CodexAcpAgent extends BaseAcpAgent {
|
|
|
483
539
|
const requestedPermissionMode = toCodexPermissionMode(meta?.permissionMode);
|
|
484
540
|
this.sessionState = createSessionState(newResponse.sessionId, params.cwd, {
|
|
485
541
|
taskRunId: meta?.taskRunId,
|
|
486
|
-
taskId: meta
|
|
542
|
+
taskId: resolveTaskId(meta),
|
|
487
543
|
modeId: newResponse.modes?.currentModeId ?? "auto",
|
|
488
544
|
permissionMode: requestedPermissionMode,
|
|
489
545
|
});
|
|
@@ -531,6 +587,45 @@ export class CodexAcpAgent extends BaseAcpAgent {
|
|
|
531
587
|
};
|
|
532
588
|
}
|
|
533
589
|
|
|
590
|
+
/**
|
|
591
|
+
* Injects the stdio general local-tools MCP server. Tools self-gate via the
|
|
592
|
+
* registry (e.g. signed-commit is cloud-only and needs a GH token), so the
|
|
593
|
+
* server is only injected when at least one tool's gate passes. Their
|
|
594
|
+
* instructions already live in the shared cloud system prompt, so only the
|
|
595
|
+
* server needs injecting here.
|
|
596
|
+
*/
|
|
597
|
+
private applyLocalTools<
|
|
598
|
+
T extends { cwd?: string; mcpServers?: McpServer[]; _meta?: unknown },
|
|
599
|
+
>(request: T, meta: NewSessionMeta | undefined): T {
|
|
600
|
+
const cwd = request.cwd;
|
|
601
|
+
if (!cwd) {
|
|
602
|
+
return request;
|
|
603
|
+
}
|
|
604
|
+
const ctx: LocalToolCtx = {
|
|
605
|
+
cwd,
|
|
606
|
+
token: resolveGithubToken(),
|
|
607
|
+
taskId: resolveTaskId(meta),
|
|
608
|
+
};
|
|
609
|
+
const tools = enabledLocalTools(ctx, meta);
|
|
610
|
+
if (tools.length === 0) {
|
|
611
|
+
if (isCloudRun(meta)) {
|
|
612
|
+
this.logger.warn(
|
|
613
|
+
"Cloud run registered no local tools — missing GH_TOKEN/GITHUB_TOKEN? signed commits unavailable",
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
return request;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const mcpServer = buildLocalToolsMcpServer(
|
|
620
|
+
ctx,
|
|
621
|
+
tools.map((t) => t.name),
|
|
622
|
+
);
|
|
623
|
+
return {
|
|
624
|
+
...request,
|
|
625
|
+
mcpServers: [...(request.mcpServers ?? []), mcpServer],
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
|
|
534
629
|
private async applyInitialPermissionMode(
|
|
535
630
|
sessionId: string,
|
|
536
631
|
permissionMode?: string,
|