-
Notifications
You must be signed in to change notification settings - Fork 53
Expand file tree
/
Copy pathbin.ts
More file actions
244 lines (223 loc) · 7.83 KB
/
Copy pathbin.ts
File metadata and controls
244 lines (223 loc) · 7.83 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
#!/usr/bin/env node
import { existsSync } from "node:fs";
import { resolve } from "node:path";
import { Command } from "commander";
import { z } from "zod/v4";
import { isSupportedReasoningEffort } from "../adapters/reasoning-effort";
import { AgentServer } from "./agent-server";
import { claudeCodeConfigSchema, mcpServersSchema } from "./schemas";
/**
* Point RTK output-compression at the binary we vendor into `dist/rtk/`
* (built here at dist/server/bin.cjs, so the sibling is `../rtk`). This makes
* RTK available for cloud runs with a consistent version instead of depending
* on rtk being on the sandbox PATH. Respect an existing POSTHOG_RTK (explicit
* path or `0`/`false` opt-out) and stay silent when the bundle is absent so the
* runtime resolver falls back to PATH.
*/
function applyBundledRtkDefault(): void {
if (process.env.POSTHOG_RTK) return;
const binName = process.platform === "win32" ? "rtk.exe" : "rtk";
const bundled = resolve(__dirname, "..", "rtk", binName);
if (existsSync(bundled)) process.env.POSTHOG_RTK = bundled;
}
const envSchema = z.object({
JWT_PUBLIC_KEY: z
.string({
error: "JWT_PUBLIC_KEY is required for authenticating client connections",
})
.min(1, "JWT_PUBLIC_KEY cannot be empty"),
POSTHOG_API_URL: z.url({
error: "POSTHOG_API_URL is required for LLM gateway communication",
}),
POSTHOG_PERSONAL_API_KEY: z
.string({
error:
"POSTHOG_PERSONAL_API_KEY is required for authenticating with PostHog services",
})
.min(1, "POSTHOG_PERSONAL_API_KEY cannot be empty"),
POSTHOG_PROJECT_ID: z
.string({
error:
"POSTHOG_PROJECT_ID is required for routing requests to the correct project",
})
.regex(/^\d+$/, "POSTHOG_PROJECT_ID must be a numeric string")
.transform((val) => parseInt(val, 10)),
POSTHOG_CODE_RUNTIME_ADAPTER: z.enum(["claude", "codex"]).optional(),
POSTHOG_CODE_MODEL: z.string().optional(),
POSTHOG_CODE_REASONING_EFFORT: z
.enum(["low", "medium", "high", "xhigh", "max"])
.optional(),
POSTHOG_TASK_RUN_EVENT_INGEST_TOKEN: z.string().min(1).optional(),
// Base URL for the event-ingest POST only; falls back to POSTHOG_API_URL when unset.
POSTHOG_TASK_RUN_EVENT_INGEST_URL: z.url().optional(),
POSTHOG_TASK_RUN_EVENT_INGEST_STREAM_WINDOW_MS: z
.string()
.regex(
/^[1-9]\d*$/,
"POSTHOG_TASK_RUN_EVENT_INGEST_STREAM_WINDOW_MS must be a positive integer",
)
.transform((value) => parseInt(value, 10))
.optional(),
POSTHOG_TASK_RUN_EVENT_INGEST_KEEP_STREAM_OPEN: z
.enum(["true", "false"])
.transform((value) => value === "true")
.optional(),
});
const program = new Command();
function parseBooleanOption(
raw: string | undefined,
flag: string,
): boolean | undefined {
if (raw === undefined) return undefined;
if (raw === "true") return true;
if (raw === "false") return false;
program.error(`${flag} must be either "true" or "false"`);
}
function parseJsonOption<S extends z.ZodType>(
raw: string | undefined,
schema: S,
flag: string,
): z.output<S> | undefined {
if (!raw) return undefined;
let parsed: unknown;
try {
parsed = JSON.parse(raw);
} catch {
program.error(`${flag} must be valid JSON`);
}
const result = schema.safeParse(parsed);
if (!result.success) {
const errors = result.error.issues
.map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`)
.join("\n");
program.error(`${flag} validation failed:\n${errors}`);
}
return result.data;
}
program
.name("agent-server")
.description("PostHog cloud agent server - runs in sandbox environments")
.option("--port <port>", "HTTP server port", "3001")
.option(
"--mode <mode>",
"Execution mode: interactive or background",
"interactive",
)
.option("--repositoryPath <path>", "Path to the repository")
.option(
"--repoReadyFile <path>",
"Sentinel file; session creation blocks until it exists (set while cloning concurrently)",
)
.requiredOption("--taskId <id>", "Task ID")
.requiredOption("--runId <id>", "Task run ID")
.option(
"--mcpServers <json>",
"MCP servers config as JSON array (ACP McpServer[] format)",
)
.option("--createPr <boolean>", "Whether this run may publish changes")
.option("--baseBranch <branch>", "Base branch for PR creation")
.option(
"--claudeCodeConfig <json>",
"Claude Code config as JSON (systemPrompt, systemPromptAppend, plugins)",
)
.option(
"--allowedDomains <domains>",
"Comma-separated list of domains allowed for web tools (WebFetch, WebSearch)",
)
.action(async (options) => {
applyBundledRtkDefault();
const envResult = envSchema.safeParse(process.env);
if (!envResult.success) {
const errors = envResult.error.issues
.map((issue) => ` - ${issue.message}`)
.join("\n");
program.error(`Environment validation failed:\n${errors}`);
return;
}
const env = envResult.data;
const mode = options.mode === "background" ? "background" : "interactive";
const createPr = parseBooleanOption(options.createPr, "--createPr");
const mcpServers = parseJsonOption(
options.mcpServers,
mcpServersSchema,
"--mcpServers",
);
const claudeCode = parseJsonOption(
options.claudeCodeConfig,
claudeCodeConfigSchema,
"--claudeCodeConfig",
);
const allowedDomains = options.allowedDomains
? options.allowedDomains
.split(",")
.map((d: string) => d.trim())
.filter(Boolean)
: undefined;
if (
env.POSTHOG_CODE_RUNTIME_ADAPTER &&
env.POSTHOG_CODE_MODEL &&
env.POSTHOG_CODE_REASONING_EFFORT &&
!isSupportedReasoningEffort(
env.POSTHOG_CODE_RUNTIME_ADAPTER,
env.POSTHOG_CODE_MODEL,
env.POSTHOG_CODE_REASONING_EFFORT,
)
) {
program.error(
`POSTHOG_CODE_REASONING_EFFORT '${env.POSTHOG_CODE_REASONING_EFFORT}' is not supported for ${env.POSTHOG_CODE_RUNTIME_ADAPTER} model '${env.POSTHOG_CODE_MODEL}'.`,
);
}
const server = new AgentServer({
port: parseInt(options.port, 10),
jwtPublicKey: env.JWT_PUBLIC_KEY,
eventIngestToken: env.POSTHOG_TASK_RUN_EVENT_INGEST_TOKEN,
eventIngestBaseUrl: env.POSTHOG_TASK_RUN_EVENT_INGEST_URL,
eventIngestStreamWindowMs:
env.POSTHOG_TASK_RUN_EVENT_INGEST_STREAM_WINDOW_MS,
eventIngestKeepStreamOpen:
env.POSTHOG_TASK_RUN_EVENT_INGEST_KEEP_STREAM_OPEN,
repositoryPath: options.repositoryPath,
repoReadyFile: options.repoReadyFile,
apiUrl: env.POSTHOG_API_URL,
apiKey: env.POSTHOG_PERSONAL_API_KEY,
projectId: env.POSTHOG_PROJECT_ID,
mode,
taskId: options.taskId,
runId: options.runId,
createPr,
mcpServers,
baseBranch: options.baseBranch,
claudeCode,
allowedDomains,
runtimeAdapter: env.POSTHOG_CODE_RUNTIME_ADAPTER,
model: env.POSTHOG_CODE_MODEL,
reasoningEffort: env.POSTHOG_CODE_REASONING_EFFORT,
});
process.on("SIGINT", async () => {
await server.stop();
process.exit(0);
});
process.on("SIGTERM", async () => {
await server.stop();
process.exit(0);
});
// Mark the run failed before exiting so a hard crash surfaces a real error instead of a
// silent stall. The deadline guarantees we exit even if the report hangs at crash time.
const FATAL_ERROR_REPORT_DEADLINE_MS = 5_000;
const handleFatalError = async (error: unknown) => {
try {
await Promise.race([
server.reportFatalError(error),
new Promise((resolve) =>
setTimeout(resolve, FATAL_ERROR_REPORT_DEADLINE_MS).unref(),
),
]);
} finally {
process.exit(1);
}
};
process.on("uncaughtException", handleFatalError);
process.on("unhandledRejection", handleFatalError);
await server.start();
});
program.parse();