@posthog/agent 2.0.0 → 2.0.2
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/LICENSE +1 -1
- package/README.md +221 -219
- package/dist/adapters/claude/conversion/tool-use-to-acp.d.ts +21 -0
- package/dist/adapters/claude/conversion/tool-use-to-acp.js +547 -0
- package/dist/adapters/claude/conversion/tool-use-to-acp.js.map +1 -0
- package/dist/adapters/claude/permissions/permission-options.d.ts +13 -0
- package/dist/adapters/claude/permissions/permission-options.js +117 -0
- package/dist/adapters/claude/permissions/permission-options.js.map +1 -0
- package/dist/adapters/claude/questions/utils.d.ts +132 -0
- package/dist/adapters/claude/questions/utils.js +63 -0
- package/dist/adapters/claude/questions/utils.js.map +1 -0
- package/dist/adapters/claude/tools.d.ts +18 -0
- package/dist/adapters/claude/tools.js +95 -0
- package/dist/adapters/claude/tools.js.map +1 -0
- package/dist/agent-DBQY1BfC.d.ts +123 -0
- package/dist/agent.d.ts +5 -0
- package/dist/agent.js +3656 -0
- package/dist/agent.js.map +1 -0
- package/dist/claude-cli/cli.js +3695 -2746
- package/dist/claude-cli/vendor/ripgrep/COPYING +3 -0
- package/dist/claude-cli/vendor/ripgrep/arm64-darwin/rg +0 -0
- package/dist/claude-cli/vendor/ripgrep/arm64-darwin/ripgrep.node +0 -0
- package/dist/claude-cli/vendor/ripgrep/arm64-linux/rg +0 -0
- package/dist/claude-cli/vendor/ripgrep/arm64-linux/ripgrep.node +0 -0
- package/dist/claude-cli/vendor/ripgrep/x64-darwin/rg +0 -0
- package/dist/claude-cli/vendor/ripgrep/x64-darwin/ripgrep.node +0 -0
- package/dist/claude-cli/vendor/ripgrep/x64-linux/rg +0 -0
- package/dist/claude-cli/vendor/ripgrep/x64-linux/ripgrep.node +0 -0
- package/dist/claude-cli/vendor/ripgrep/x64-win32/rg.exe +0 -0
- package/dist/claude-cli/vendor/ripgrep/x64-win32/ripgrep.node +0 -0
- package/dist/gateway-models.d.ts +24 -0
- package/dist/gateway-models.js +93 -0
- package/dist/gateway-models.js.map +1 -0
- package/dist/index.d.ts +170 -1157
- package/dist/index.js +9373 -5135
- package/dist/index.js.map +1 -1
- package/dist/logger-DDBiMOOD.d.ts +24 -0
- package/dist/posthog-api.d.ts +40 -0
- package/dist/posthog-api.js +175 -0
- package/dist/posthog-api.js.map +1 -0
- package/dist/server/agent-server.d.ts +41 -0
- package/dist/server/agent-server.js +10503 -0
- package/dist/server/agent-server.js.map +1 -0
- package/dist/server/bin.d.ts +1 -0
- package/dist/server/bin.js +10558 -0
- package/dist/server/bin.js.map +1 -0
- package/dist/types.d.ts +129 -0
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -0
- package/package.json +65 -13
- package/src/acp-extensions.ts +98 -16
- package/src/adapters/acp-connection.ts +494 -0
- package/src/adapters/base-acp-agent.ts +150 -0
- package/src/adapters/claude/claude-agent.ts +596 -0
- package/src/adapters/claude/conversion/acp-to-sdk.ts +102 -0
- package/src/adapters/claude/conversion/sdk-to-acp.ts +571 -0
- package/src/adapters/claude/conversion/tool-use-to-acp.ts +618 -0
- package/src/adapters/claude/hooks.ts +64 -0
- package/src/adapters/claude/mcp/tool-metadata.ts +102 -0
- package/src/adapters/claude/permissions/permission-handlers.ts +433 -0
- package/src/adapters/claude/permissions/permission-options.ts +103 -0
- package/src/adapters/claude/plan/utils.ts +56 -0
- package/src/adapters/claude/questions/utils.ts +92 -0
- package/src/adapters/claude/session/commands.ts +38 -0
- package/src/adapters/claude/session/mcp-config.ts +37 -0
- package/src/adapters/claude/session/models.ts +12 -0
- package/src/adapters/claude/session/options.ts +236 -0
- package/src/adapters/claude/tool-meta.ts +143 -0
- package/src/adapters/claude/tools.ts +53 -688
- package/src/adapters/claude/types.ts +61 -0
- package/src/adapters/codex/spawn.ts +130 -0
- package/src/agent.ts +96 -587
- package/src/execution-mode.ts +43 -0
- package/src/gateway-models.ts +135 -0
- package/src/index.ts +79 -0
- package/src/otel-log-writer.test.ts +105 -0
- package/src/otel-log-writer.ts +94 -0
- package/src/posthog-api.ts +75 -235
- package/src/resume.ts +115 -0
- package/src/sagas/apply-snapshot-saga.test.ts +690 -0
- package/src/sagas/apply-snapshot-saga.ts +88 -0
- package/src/sagas/capture-tree-saga.test.ts +892 -0
- package/src/sagas/capture-tree-saga.ts +141 -0
- package/src/sagas/resume-saga.test.ts +558 -0
- package/src/sagas/resume-saga.ts +332 -0
- package/src/sagas/test-fixtures.ts +250 -0
- package/src/server/agent-server.test.ts +220 -0
- package/src/server/agent-server.ts +748 -0
- package/src/server/bin.ts +88 -0
- package/src/server/jwt.ts +65 -0
- package/src/server/schemas.ts +47 -0
- package/src/server/types.ts +13 -0
- package/src/server/utils/retry.test.ts +122 -0
- package/src/server/utils/retry.ts +61 -0
- package/src/server/utils/sse-parser.test.ts +93 -0
- package/src/server/utils/sse-parser.ts +46 -0
- package/src/session-log-writer.test.ts +140 -0
- package/src/session-log-writer.ts +137 -0
- package/src/test/assertions.ts +114 -0
- package/src/test/controllers/sse-controller.ts +107 -0
- package/src/test/fixtures/api.ts +111 -0
- package/src/test/fixtures/config.ts +33 -0
- package/src/test/fixtures/notifications.ts +92 -0
- package/src/test/mocks/claude-sdk.ts +251 -0
- package/src/test/mocks/msw-handlers.ts +48 -0
- package/src/test/setup.ts +114 -0
- package/src/test/wait.ts +41 -0
- package/src/tree-tracker.ts +173 -0
- package/src/types.ts +54 -137
- package/src/utils/acp-content.ts +58 -0
- package/src/utils/async-mutex.test.ts +104 -0
- package/src/utils/async-mutex.ts +31 -0
- package/src/utils/common.ts +15 -0
- package/src/utils/gateway.ts +9 -6
- package/src/utils/logger.ts +0 -30
- package/src/utils/streams.ts +220 -0
- package/CLAUDE.md +0 -331
- package/src/adapters/claude/claude.ts +0 -1947
- package/src/adapters/claude/mcp-server.ts +0 -810
- package/src/adapters/claude/utils.ts +0 -267
- package/src/adapters/connection.ts +0 -95
- package/src/file-manager.ts +0 -273
- package/src/git-manager.ts +0 -577
- package/src/schemas.ts +0 -241
- package/src/session-store.ts +0 -259
- package/src/task-manager.ts +0 -163
- package/src/todo-manager.ts +0 -180
- package/src/tools/registry.ts +0 -134
- package/src/tools/types.ts +0 -133
- package/src/utils/tapped-stream.ts +0 -60
- package/src/worktree-manager.ts +0 -974
|
@@ -0,0 +1,748 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ClientSideConnection,
|
|
3
|
+
ndJsonStream,
|
|
4
|
+
PROTOCOL_VERSION,
|
|
5
|
+
} from "@agentclientprotocol/sdk";
|
|
6
|
+
import { type ServerType, serve } from "@hono/node-server";
|
|
7
|
+
import { Hono } from "hono";
|
|
8
|
+
import { POSTHOG_NOTIFICATIONS } from "../acp-extensions.js";
|
|
9
|
+
import {
|
|
10
|
+
createAcpConnection,
|
|
11
|
+
type InProcessAcpConnection,
|
|
12
|
+
} from "../adapters/acp-connection.js";
|
|
13
|
+
import { PostHogAPIClient } from "../posthog-api.js";
|
|
14
|
+
import { SessionLogWriter } from "../session-log-writer.js";
|
|
15
|
+
import { TreeTracker } from "../tree-tracker.js";
|
|
16
|
+
import type { AgentMode, DeviceInfo, TreeSnapshotEvent } from "../types.js";
|
|
17
|
+
import { AsyncMutex } from "../utils/async-mutex.js";
|
|
18
|
+
import { getLlmGatewayUrl } from "../utils/gateway.js";
|
|
19
|
+
import { Logger } from "../utils/logger.js";
|
|
20
|
+
import { type JwtPayload, JwtValidationError, validateJwt } from "./jwt.js";
|
|
21
|
+
import { jsonRpcRequestSchema, validateCommandParams } from "./schemas.js";
|
|
22
|
+
import type { AgentServerConfig } from "./types.js";
|
|
23
|
+
|
|
24
|
+
type MessageCallback = (message: unknown) => void;
|
|
25
|
+
|
|
26
|
+
class NdJsonTap {
|
|
27
|
+
private decoder = new TextDecoder();
|
|
28
|
+
private buffer = "";
|
|
29
|
+
|
|
30
|
+
constructor(private onMessage: MessageCallback) {}
|
|
31
|
+
|
|
32
|
+
process(chunk: Uint8Array): void {
|
|
33
|
+
this.buffer += this.decoder.decode(chunk, { stream: true });
|
|
34
|
+
const lines = this.buffer.split("\n");
|
|
35
|
+
this.buffer = lines.pop() ?? "";
|
|
36
|
+
|
|
37
|
+
for (const line of lines) {
|
|
38
|
+
if (!line.trim()) continue;
|
|
39
|
+
try {
|
|
40
|
+
this.onMessage(JSON.parse(line));
|
|
41
|
+
} catch {
|
|
42
|
+
// Not valid JSON, skip
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function createTappedReadableStream(
|
|
49
|
+
underlying: ReadableStream<Uint8Array>,
|
|
50
|
+
onMessage: MessageCallback,
|
|
51
|
+
logger?: Logger,
|
|
52
|
+
): ReadableStream<Uint8Array> {
|
|
53
|
+
const reader = underlying.getReader();
|
|
54
|
+
const tap = new NdJsonTap(onMessage);
|
|
55
|
+
|
|
56
|
+
return new ReadableStream<Uint8Array>({
|
|
57
|
+
async pull(controller) {
|
|
58
|
+
try {
|
|
59
|
+
const { value, done } = await reader.read();
|
|
60
|
+
if (done) {
|
|
61
|
+
controller.close();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
tap.process(value);
|
|
65
|
+
controller.enqueue(value);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
logger?.debug("Read failed, closing stream", error);
|
|
68
|
+
controller.close();
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
cancel() {
|
|
72
|
+
reader.releaseLock();
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function createTappedWritableStream(
|
|
78
|
+
underlying: WritableStream<Uint8Array>,
|
|
79
|
+
onMessage: MessageCallback,
|
|
80
|
+
logger?: Logger,
|
|
81
|
+
): WritableStream<Uint8Array> {
|
|
82
|
+
const tap = new NdJsonTap(onMessage);
|
|
83
|
+
const mutex = new AsyncMutex();
|
|
84
|
+
|
|
85
|
+
return new WritableStream<Uint8Array>({
|
|
86
|
+
async write(chunk) {
|
|
87
|
+
tap.process(chunk);
|
|
88
|
+
await mutex.acquire();
|
|
89
|
+
try {
|
|
90
|
+
const writer = underlying.getWriter();
|
|
91
|
+
await writer.write(chunk);
|
|
92
|
+
writer.releaseLock();
|
|
93
|
+
} catch (error) {
|
|
94
|
+
logger?.debug("Write failed (stream may be closed)", error);
|
|
95
|
+
} finally {
|
|
96
|
+
mutex.release();
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
async close() {
|
|
100
|
+
await mutex.acquire();
|
|
101
|
+
try {
|
|
102
|
+
const writer = underlying.getWriter();
|
|
103
|
+
await writer.close();
|
|
104
|
+
writer.releaseLock();
|
|
105
|
+
} catch (error) {
|
|
106
|
+
logger?.debug("Close failed (stream may be closed)", error);
|
|
107
|
+
} finally {
|
|
108
|
+
mutex.release();
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
async abort(reason) {
|
|
112
|
+
await mutex.acquire();
|
|
113
|
+
try {
|
|
114
|
+
const writer = underlying.getWriter();
|
|
115
|
+
await writer.abort(reason);
|
|
116
|
+
writer.releaseLock();
|
|
117
|
+
} catch (error) {
|
|
118
|
+
logger?.debug("Abort failed (stream may be closed)", error);
|
|
119
|
+
} finally {
|
|
120
|
+
mutex.release();
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
interface SseController {
|
|
127
|
+
send: (data: unknown) => void;
|
|
128
|
+
close: () => void;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
interface ActiveSession {
|
|
132
|
+
payload: JwtPayload;
|
|
133
|
+
acpConnection: InProcessAcpConnection;
|
|
134
|
+
clientConnection: ClientSideConnection;
|
|
135
|
+
treeTracker: TreeTracker;
|
|
136
|
+
sseController: SseController | null;
|
|
137
|
+
deviceInfo: DeviceInfo;
|
|
138
|
+
logWriter: SessionLogWriter;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export class AgentServer {
|
|
142
|
+
private config: AgentServerConfig;
|
|
143
|
+
private logger: Logger;
|
|
144
|
+
private server: ServerType | null = null;
|
|
145
|
+
private session: ActiveSession | null = null;
|
|
146
|
+
private app: Hono;
|
|
147
|
+
private posthogAPI: PostHogAPIClient;
|
|
148
|
+
|
|
149
|
+
constructor(config: AgentServerConfig) {
|
|
150
|
+
this.config = config;
|
|
151
|
+
this.logger = new Logger({ debug: true, prefix: "[AgentServer]" });
|
|
152
|
+
this.posthogAPI = new PostHogAPIClient({
|
|
153
|
+
apiUrl: config.apiUrl,
|
|
154
|
+
projectId: config.projectId,
|
|
155
|
+
getApiKey: () => config.apiKey,
|
|
156
|
+
});
|
|
157
|
+
this.app = this.createApp();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private getEffectiveMode(payload: JwtPayload): AgentMode {
|
|
161
|
+
return payload.mode ?? this.config.mode;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
private createApp(): Hono {
|
|
165
|
+
const app = new Hono();
|
|
166
|
+
|
|
167
|
+
app.get("/health", (c) => {
|
|
168
|
+
return c.json({ status: "ok", hasSession: !!this.session });
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
app.get("/events", async (c) => {
|
|
172
|
+
let payload: JwtPayload;
|
|
173
|
+
|
|
174
|
+
try {
|
|
175
|
+
payload = this.authenticateRequest(c.req.header.bind(c.req));
|
|
176
|
+
} catch (error) {
|
|
177
|
+
return c.json(
|
|
178
|
+
{
|
|
179
|
+
error:
|
|
180
|
+
error instanceof JwtValidationError
|
|
181
|
+
? error.message
|
|
182
|
+
: "Invalid token",
|
|
183
|
+
code:
|
|
184
|
+
error instanceof JwtValidationError
|
|
185
|
+
? error.code
|
|
186
|
+
: "invalid_token",
|
|
187
|
+
},
|
|
188
|
+
401,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const stream = new ReadableStream({
|
|
193
|
+
start: async (controller) => {
|
|
194
|
+
const sseController: SseController = {
|
|
195
|
+
send: (data: unknown) => {
|
|
196
|
+
try {
|
|
197
|
+
controller.enqueue(
|
|
198
|
+
new TextEncoder().encode(`data: ${JSON.stringify(data)}\n\n`),
|
|
199
|
+
);
|
|
200
|
+
} catch (error) {
|
|
201
|
+
this.logger.debug(
|
|
202
|
+
"SSE send failed (stream may be closed)",
|
|
203
|
+
error,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
},
|
|
207
|
+
close: () => {
|
|
208
|
+
try {
|
|
209
|
+
controller.close();
|
|
210
|
+
} catch (error) {
|
|
211
|
+
this.logger.debug("SSE close failed (already closed)", error);
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
if (!this.session || this.session.payload.run_id !== payload.run_id) {
|
|
217
|
+
await this.initializeSession(payload, sseController);
|
|
218
|
+
} else {
|
|
219
|
+
this.session.sseController = sseController;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
this.sendSseEvent(sseController, {
|
|
223
|
+
type: "connected",
|
|
224
|
+
run_id: payload.run_id,
|
|
225
|
+
});
|
|
226
|
+
},
|
|
227
|
+
cancel: () => {
|
|
228
|
+
this.logger.info("SSE connection closed");
|
|
229
|
+
if (this.session?.sseController) {
|
|
230
|
+
this.session.sseController = null;
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
return new Response(stream, {
|
|
236
|
+
headers: {
|
|
237
|
+
"Content-Type": "text/event-stream",
|
|
238
|
+
"Cache-Control": "no-cache",
|
|
239
|
+
Connection: "keep-alive",
|
|
240
|
+
},
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
app.post("/command", async (c) => {
|
|
245
|
+
let payload: JwtPayload;
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
payload = this.authenticateRequest(c.req.header.bind(c.req));
|
|
249
|
+
} catch (error) {
|
|
250
|
+
return c.json(
|
|
251
|
+
{
|
|
252
|
+
error:
|
|
253
|
+
error instanceof JwtValidationError
|
|
254
|
+
? error.message
|
|
255
|
+
: "Invalid token",
|
|
256
|
+
},
|
|
257
|
+
401,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!this.session || this.session.payload.run_id !== payload.run_id) {
|
|
262
|
+
return c.json({ error: "No active session for this run" }, 400);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const rawBody = await c.req.json().catch(() => null);
|
|
266
|
+
const parseResult = jsonRpcRequestSchema.safeParse(rawBody);
|
|
267
|
+
|
|
268
|
+
if (!parseResult.success) {
|
|
269
|
+
return c.json({ error: "Invalid JSON-RPC request" }, 400);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const command = parseResult.data;
|
|
273
|
+
const paramsValidation = validateCommandParams(
|
|
274
|
+
command.method,
|
|
275
|
+
command.params ?? {},
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
if (!paramsValidation.success) {
|
|
279
|
+
return c.json(
|
|
280
|
+
{
|
|
281
|
+
jsonrpc: "2.0",
|
|
282
|
+
id: command.id,
|
|
283
|
+
error: {
|
|
284
|
+
code: -32602,
|
|
285
|
+
message: paramsValidation.error,
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
200,
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
try {
|
|
293
|
+
const result = await this.executeCommand(
|
|
294
|
+
command.method,
|
|
295
|
+
(command.params as Record<string, unknown>) || {},
|
|
296
|
+
);
|
|
297
|
+
return c.json({
|
|
298
|
+
jsonrpc: "2.0",
|
|
299
|
+
id: command.id,
|
|
300
|
+
result,
|
|
301
|
+
});
|
|
302
|
+
} catch (error) {
|
|
303
|
+
return c.json({
|
|
304
|
+
jsonrpc: "2.0",
|
|
305
|
+
id: command.id,
|
|
306
|
+
error: {
|
|
307
|
+
code: -32000,
|
|
308
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
app.notFound((c) => {
|
|
315
|
+
return c.json({ error: "Not found" }, 404);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
return app;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async start(): Promise<void> {
|
|
322
|
+
await new Promise<void>((resolve) => {
|
|
323
|
+
this.server = serve(
|
|
324
|
+
{
|
|
325
|
+
fetch: this.app.fetch,
|
|
326
|
+
port: this.config.port,
|
|
327
|
+
},
|
|
328
|
+
() => {
|
|
329
|
+
this.logger.info(`HTTP server listening on port ${this.config.port}`);
|
|
330
|
+
resolve();
|
|
331
|
+
},
|
|
332
|
+
);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
await this.autoInitializeSession();
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
private async autoInitializeSession(): Promise<void> {
|
|
339
|
+
const { taskId, runId, mode, projectId } = this.config;
|
|
340
|
+
|
|
341
|
+
this.logger.info("Auto-initializing session", { taskId, runId, mode });
|
|
342
|
+
|
|
343
|
+
// Create a synthetic payload from config (no JWT needed for auto-init)
|
|
344
|
+
const payload: JwtPayload = {
|
|
345
|
+
task_id: taskId,
|
|
346
|
+
run_id: runId,
|
|
347
|
+
team_id: projectId,
|
|
348
|
+
user_id: 0, // System-initiated
|
|
349
|
+
distinct_id: "agent-server",
|
|
350
|
+
mode,
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
await this.initializeSession(payload, null);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async stop(): Promise<void> {
|
|
357
|
+
this.logger.info("Stopping agent server...");
|
|
358
|
+
|
|
359
|
+
if (this.session) {
|
|
360
|
+
await this.cleanupSession();
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (this.server) {
|
|
364
|
+
this.server.close();
|
|
365
|
+
this.server = null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
this.logger.info("Agent server stopped");
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private authenticateRequest(
|
|
372
|
+
getHeader: (name: string) => string | undefined,
|
|
373
|
+
): JwtPayload {
|
|
374
|
+
// Always require JWT validation - never trust unverified headers
|
|
375
|
+
if (!this.config.jwtPublicKey) {
|
|
376
|
+
throw new JwtValidationError(
|
|
377
|
+
"Server not configured with JWT public key",
|
|
378
|
+
"server_error",
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const authHeader = getHeader("authorization");
|
|
383
|
+
if (!authHeader?.startsWith("Bearer ")) {
|
|
384
|
+
throw new JwtValidationError(
|
|
385
|
+
"Missing authorization header",
|
|
386
|
+
"invalid_token",
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const token = authHeader.slice(7);
|
|
391
|
+
return validateJwt(token, this.config.jwtPublicKey);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
private async executeCommand(
|
|
395
|
+
method: string,
|
|
396
|
+
params: Record<string, unknown>,
|
|
397
|
+
): Promise<unknown> {
|
|
398
|
+
if (!this.session) {
|
|
399
|
+
throw new Error("No active session");
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
switch (method) {
|
|
403
|
+
case POSTHOG_NOTIFICATIONS.USER_MESSAGE:
|
|
404
|
+
case "user_message": {
|
|
405
|
+
const content = params.content as string;
|
|
406
|
+
|
|
407
|
+
this.logger.info(
|
|
408
|
+
`Processing user message: ${content.substring(0, 100)}...`,
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
const result = await this.session.clientConnection.prompt({
|
|
412
|
+
sessionId: this.session.payload.run_id,
|
|
413
|
+
prompt: [{ type: "text", text: content }],
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
return { stopReason: result.stopReason };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
case POSTHOG_NOTIFICATIONS.CANCEL:
|
|
420
|
+
case "cancel": {
|
|
421
|
+
this.logger.info("Cancel requested");
|
|
422
|
+
await this.session.clientConnection.cancel({
|
|
423
|
+
sessionId: this.session.payload.run_id,
|
|
424
|
+
});
|
|
425
|
+
return { cancelled: true };
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
case POSTHOG_NOTIFICATIONS.CLOSE:
|
|
429
|
+
case "close": {
|
|
430
|
+
this.logger.info("Close requested");
|
|
431
|
+
await this.cleanupSession();
|
|
432
|
+
return { closed: true };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
default:
|
|
436
|
+
throw new Error(`Unknown method: ${method}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
private async initializeSession(
|
|
441
|
+
payload: JwtPayload,
|
|
442
|
+
sseController: SseController | null,
|
|
443
|
+
): Promise<void> {
|
|
444
|
+
if (this.session) {
|
|
445
|
+
await this.cleanupSession();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
this.logger.info("Initializing session", {
|
|
449
|
+
runId: payload.run_id,
|
|
450
|
+
taskId: payload.task_id,
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
const deviceInfo: DeviceInfo = {
|
|
454
|
+
type: "cloud",
|
|
455
|
+
name: process.env.HOSTNAME || "cloud-sandbox",
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
this.configureEnvironment();
|
|
459
|
+
|
|
460
|
+
const treeTracker = new TreeTracker({
|
|
461
|
+
repositoryPath: this.config.repositoryPath,
|
|
462
|
+
taskId: payload.task_id,
|
|
463
|
+
runId: payload.run_id,
|
|
464
|
+
logger: new Logger({ debug: true, prefix: "[TreeTracker]" }),
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
const _posthogAPI = new PostHogAPIClient({
|
|
468
|
+
apiUrl: this.config.apiUrl,
|
|
469
|
+
projectId: this.config.projectId,
|
|
470
|
+
getApiKey: () => this.config.apiKey,
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
const logWriter = new SessionLogWriter({
|
|
474
|
+
otelConfig: {
|
|
475
|
+
posthogHost: this.config.apiUrl,
|
|
476
|
+
apiKey: this.config.apiKey,
|
|
477
|
+
logsPath: "/i/v1/agent-logs",
|
|
478
|
+
},
|
|
479
|
+
logger: new Logger({ debug: true, prefix: "[SessionLogWriter]" }),
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
const acpConnection = createAcpConnection({
|
|
483
|
+
taskRunId: payload.run_id,
|
|
484
|
+
taskId: payload.task_id,
|
|
485
|
+
deviceType: deviceInfo.type,
|
|
486
|
+
logWriter,
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
// Tap both streams to broadcast all ACP messages via SSE (mimics local transport)
|
|
490
|
+
const onAcpMessage = (message: unknown) => {
|
|
491
|
+
this.broadcastEvent({
|
|
492
|
+
type: "notification",
|
|
493
|
+
timestamp: new Date().toISOString(),
|
|
494
|
+
notification: message,
|
|
495
|
+
});
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
const tappedReadable = createTappedReadableStream(
|
|
499
|
+
acpConnection.clientStreams.readable as ReadableStream<Uint8Array>,
|
|
500
|
+
onAcpMessage,
|
|
501
|
+
this.logger,
|
|
502
|
+
);
|
|
503
|
+
|
|
504
|
+
const tappedWritable = createTappedWritableStream(
|
|
505
|
+
acpConnection.clientStreams.writable as WritableStream<Uint8Array>,
|
|
506
|
+
onAcpMessage,
|
|
507
|
+
this.logger,
|
|
508
|
+
);
|
|
509
|
+
|
|
510
|
+
const clientStream = ndJsonStream(tappedWritable, tappedReadable);
|
|
511
|
+
|
|
512
|
+
const clientConnection = new ClientSideConnection(
|
|
513
|
+
() => this.createCloudClient(payload),
|
|
514
|
+
clientStream,
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
await clientConnection.initialize({
|
|
518
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
519
|
+
clientCapabilities: {},
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
await clientConnection.newSession({
|
|
523
|
+
cwd: this.config.repositoryPath,
|
|
524
|
+
mcpServers: [],
|
|
525
|
+
_meta: { sessionId: payload.run_id },
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
this.session = {
|
|
529
|
+
payload,
|
|
530
|
+
acpConnection,
|
|
531
|
+
clientConnection,
|
|
532
|
+
treeTracker,
|
|
533
|
+
sseController,
|
|
534
|
+
deviceInfo,
|
|
535
|
+
logWriter,
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
this.logger.info("Session initialized successfully");
|
|
539
|
+
|
|
540
|
+
await this.sendInitialTaskMessage(payload);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
private async sendInitialTaskMessage(payload: JwtPayload): Promise<void> {
|
|
544
|
+
if (!this.session) return;
|
|
545
|
+
|
|
546
|
+
try {
|
|
547
|
+
this.logger.info("Fetching task details", { taskId: payload.task_id });
|
|
548
|
+
const task = await this.posthogAPI.getTask(payload.task_id);
|
|
549
|
+
|
|
550
|
+
if (!task.description) {
|
|
551
|
+
this.logger.warn("Task has no description, skipping initial message");
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
this.logger.info("Sending initial task message", {
|
|
556
|
+
taskId: payload.task_id,
|
|
557
|
+
descriptionLength: task.description.length,
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
const result = await this.session.clientConnection.prompt({
|
|
561
|
+
sessionId: payload.run_id,
|
|
562
|
+
prompt: [{ type: "text", text: task.description }],
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
this.logger.info("Initial task message completed", {
|
|
566
|
+
stopReason: result.stopReason,
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
// Only auto-complete for background mode
|
|
570
|
+
const mode = this.getEffectiveMode(payload);
|
|
571
|
+
if (mode === "background") {
|
|
572
|
+
await this.signalTaskComplete(payload, result.stopReason);
|
|
573
|
+
} else {
|
|
574
|
+
this.logger.info("Interactive mode - staying open for conversation");
|
|
575
|
+
}
|
|
576
|
+
} catch (error) {
|
|
577
|
+
this.logger.error("Failed to send initial task message", error);
|
|
578
|
+
// Signal failure for background mode
|
|
579
|
+
const mode = this.getEffectiveMode(payload);
|
|
580
|
+
if (mode === "background") {
|
|
581
|
+
await this.signalTaskComplete(payload, "error");
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
private async signalTaskComplete(
|
|
587
|
+
payload: JwtPayload,
|
|
588
|
+
stopReason: string,
|
|
589
|
+
): Promise<void> {
|
|
590
|
+
const status =
|
|
591
|
+
stopReason === "cancelled"
|
|
592
|
+
? "cancelled"
|
|
593
|
+
: stopReason === "error"
|
|
594
|
+
? "failed"
|
|
595
|
+
: "completed";
|
|
596
|
+
|
|
597
|
+
try {
|
|
598
|
+
await this.posthogAPI.updateTaskRun(payload.task_id, payload.run_id, {
|
|
599
|
+
status,
|
|
600
|
+
error_message: stopReason === "error" ? "Agent error" : undefined,
|
|
601
|
+
});
|
|
602
|
+
this.logger.info("Task completion signaled", { status, stopReason });
|
|
603
|
+
} catch (error) {
|
|
604
|
+
this.logger.error("Failed to signal task completion", error);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
private configureEnvironment(): void {
|
|
609
|
+
const { apiKey, apiUrl, projectId } = this.config;
|
|
610
|
+
const gatewayUrl = process.env.LLM_GATEWAY_URL || getLlmGatewayUrl(apiUrl);
|
|
611
|
+
const openaiBaseUrl = gatewayUrl.endsWith("/v1")
|
|
612
|
+
? gatewayUrl
|
|
613
|
+
: `${gatewayUrl}/v1`;
|
|
614
|
+
|
|
615
|
+
Object.assign(process.env, {
|
|
616
|
+
// PostHog
|
|
617
|
+
POSTHOG_API_KEY: apiKey,
|
|
618
|
+
POSTHOG_API_URL: apiUrl,
|
|
619
|
+
POSTHOG_API_HOST: apiUrl,
|
|
620
|
+
POSTHOG_AUTH_HEADER: `Bearer ${apiKey}`,
|
|
621
|
+
POSTHOG_PROJECT_ID: String(projectId),
|
|
622
|
+
// Anthropic
|
|
623
|
+
ANTHROPIC_API_KEY: apiKey,
|
|
624
|
+
ANTHROPIC_AUTH_TOKEN: apiKey,
|
|
625
|
+
ANTHROPIC_BASE_URL: gatewayUrl,
|
|
626
|
+
// OpenAI (for models like GPT-4, o1, etc.)
|
|
627
|
+
OPENAI_API_KEY: apiKey,
|
|
628
|
+
OPENAI_BASE_URL: openaiBaseUrl,
|
|
629
|
+
// Generic gateway
|
|
630
|
+
LLM_GATEWAY_URL: gatewayUrl,
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
private createCloudClient(payload: JwtPayload) {
|
|
635
|
+
const mode = this.getEffectiveMode(payload);
|
|
636
|
+
|
|
637
|
+
return {
|
|
638
|
+
requestPermission: async (params: {
|
|
639
|
+
options: Array<{ kind: string; optionId: string }>;
|
|
640
|
+
}) => {
|
|
641
|
+
// Background mode: always auto-approve permissions
|
|
642
|
+
// Interactive mode: also auto-approve for now (user can monitor via SSE)
|
|
643
|
+
// Future: interactive mode could pause and wait for user approval via SSE
|
|
644
|
+
this.logger.debug("Permission request", {
|
|
645
|
+
mode,
|
|
646
|
+
options: params.options,
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
const allowOption = params.options.find(
|
|
650
|
+
(o) => o.kind === "allow_once" || o.kind === "allow_always",
|
|
651
|
+
);
|
|
652
|
+
return {
|
|
653
|
+
outcome: {
|
|
654
|
+
outcome: "selected" as const,
|
|
655
|
+
optionId: allowOption?.optionId ?? params.options[0].optionId,
|
|
656
|
+
},
|
|
657
|
+
};
|
|
658
|
+
},
|
|
659
|
+
sessionUpdate: async (params: {
|
|
660
|
+
sessionId: string;
|
|
661
|
+
update?: Record<string, unknown>;
|
|
662
|
+
}) => {
|
|
663
|
+
// session/update notifications flow through the tapped stream (like local transport)
|
|
664
|
+
// Only handle tree state capture for file changes here
|
|
665
|
+
if (params.update?.sessionUpdate === "tool_call_update") {
|
|
666
|
+
const meta = (params.update?._meta as Record<string, unknown>)
|
|
667
|
+
?.claudeCode as Record<string, unknown> | undefined;
|
|
668
|
+
const toolName = meta?.toolName as string | undefined;
|
|
669
|
+
const toolResponse = meta?.toolResponse as
|
|
670
|
+
| Record<string, unknown>
|
|
671
|
+
| undefined;
|
|
672
|
+
|
|
673
|
+
if (
|
|
674
|
+
(toolName === "Write" || toolName === "Edit") &&
|
|
675
|
+
toolResponse?.filePath
|
|
676
|
+
) {
|
|
677
|
+
await this.captureTreeState();
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
},
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
private async cleanupSession(): Promise<void> {
|
|
685
|
+
if (!this.session) return;
|
|
686
|
+
|
|
687
|
+
this.logger.info("Cleaning up session");
|
|
688
|
+
|
|
689
|
+
try {
|
|
690
|
+
await this.captureTreeState();
|
|
691
|
+
} catch (error) {
|
|
692
|
+
this.logger.error("Failed to capture final tree state", error);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
try {
|
|
696
|
+
await this.session.logWriter.flush(this.session.payload.run_id);
|
|
697
|
+
} catch (error) {
|
|
698
|
+
this.logger.error("Failed to flush session logs", error);
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
try {
|
|
702
|
+
await this.session.acpConnection.cleanup();
|
|
703
|
+
} catch (error) {
|
|
704
|
+
this.logger.error("Failed to cleanup ACP connection", error);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
if (this.session.sseController) {
|
|
708
|
+
this.session.sseController.close();
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
this.session = null;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
private async captureTreeState(): Promise<void> {
|
|
715
|
+
if (!this.session?.treeTracker) return;
|
|
716
|
+
|
|
717
|
+
try {
|
|
718
|
+
const snapshot = await this.session.treeTracker.captureTree({});
|
|
719
|
+
if (snapshot) {
|
|
720
|
+
const snapshotWithDevice: TreeSnapshotEvent = {
|
|
721
|
+
...snapshot,
|
|
722
|
+
device: this.session.deviceInfo,
|
|
723
|
+
};
|
|
724
|
+
this.broadcastEvent({
|
|
725
|
+
type: "notification",
|
|
726
|
+
timestamp: new Date().toISOString(),
|
|
727
|
+
notification: {
|
|
728
|
+
jsonrpc: "2.0",
|
|
729
|
+
method: POSTHOG_NOTIFICATIONS.TREE_SNAPSHOT,
|
|
730
|
+
params: snapshotWithDevice,
|
|
731
|
+
},
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
} catch (error) {
|
|
735
|
+
this.logger.error("Failed to capture tree state", error);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
private broadcastEvent(event: Record<string, unknown>): void {
|
|
740
|
+
if (this.session?.sseController) {
|
|
741
|
+
this.sendSseEvent(this.session.sseController, event);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
private sendSseEvent(controller: SseController, data: unknown): void {
|
|
746
|
+
controller.send(data);
|
|
747
|
+
}
|
|
748
|
+
}
|