@wrongstack/acp 0.295.1 → 0.296.3

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/index.js CHANGED
@@ -974,6 +974,9 @@ var ClientTransport = class {
974
974
  };
975
975
  const waitForMarker = (chunk) => {
976
976
  this.buffer += chunk;
977
+ if (this.buffer.length > this.maxFrameChars) {
978
+ this.buffer = this.buffer.slice(-this.maxFrameChars);
979
+ }
977
980
  const idx = this.buffer.indexOf("[wstack-acp]\n");
978
981
  if (idx !== -1) {
979
982
  this.buffer = this.buffer.slice(idx + "[wstack-acp]\n".length);
@@ -1203,8 +1206,9 @@ function toolToPriority(tool) {
1203
1206
 
1204
1207
  // src/agent/wrongstack-acp-agent.ts
1205
1208
  import { createServer } from "node:http";
1209
+ import { isIP } from "node:net";
1206
1210
  import { fileURLToPath } from "node:url";
1207
- import { writeErr as writeErr2 } from "@wrongstack/core/utils";
1211
+ import { expandIPv6, writeErr as writeErr2 } from "@wrongstack/core/utils";
1208
1212
  var WrongStackACPServer = class {
1209
1213
  transport;
1210
1214
  handler;
@@ -1260,7 +1264,10 @@ var WrongStackACPServer = class {
1260
1264
  async startHttp(port) {
1261
1265
  const host = this.options.host ?? "127.0.0.1";
1262
1266
  const handler = this.handler;
1263
- const authToken = this.options.authToken;
1267
+ const authToken = this.options.authToken?.trim();
1268
+ if (!authToken && !isLoopbackHost(host)) {
1269
+ throw new Error("ACP HTTP transport requires authToken for non-loopback hosts");
1270
+ }
1264
1271
  let httpChain = Promise.resolve();
1265
1272
  this.httpServer = createServer(async (req, res) => {
1266
1273
  if (authToken) {
@@ -1388,6 +1395,16 @@ function headerValue(value) {
1388
1395
  function requestPath(value) {
1389
1396
  return value ?? "/";
1390
1397
  }
1398
+ function isLoopbackHost(host) {
1399
+ const normalized = host.trim().toLowerCase();
1400
+ if (normalized === "localhost") return true;
1401
+ const literal = normalized.startsWith("[") && normalized.endsWith("]") ? normalized.slice(1, -1) : normalized;
1402
+ const version = isIP(literal);
1403
+ if (version === 4) return literal.startsWith("127.");
1404
+ if (version !== 6) return false;
1405
+ const groups = expandIPv6(literal);
1406
+ return groups !== null && groups.slice(0, 7).every((group) => group === 0) && groups[7] === 1;
1407
+ }
1391
1408
  async function main() {
1392
1409
  const server = new WrongStackACPServer();
1393
1410
  await server.start();
@@ -1401,6 +1418,39 @@ if (isEntrypoint) {
1401
1418
  });
1402
1419
  }
1403
1420
 
1421
+ // src/client/acp-session-content.ts
1422
+ function textContent(text) {
1423
+ return { type: "text", text };
1424
+ }
1425
+ function imageContent(mimeType, data) {
1426
+ return { type: "image", mimeType, data };
1427
+ }
1428
+ function audioContent(mimeType, data) {
1429
+ return { type: "audio", mimeType, data };
1430
+ }
1431
+ function extractText(block) {
1432
+ if (typeof block !== "object" || block === null) return "";
1433
+ const b = block;
1434
+ if (b.type === "text" && typeof b.text === "string") return b.text;
1435
+ if (b.type === "resource" && b.resource && typeof b.resource === "object" && typeof b.resource.text === "string") {
1436
+ return b.resource.text;
1437
+ }
1438
+ return "";
1439
+ }
1440
+ function isRecord(v) {
1441
+ return typeof v === "object" && v !== null && !Array.isArray(v);
1442
+ }
1443
+ function emptyRunResult(stopReason) {
1444
+ return {
1445
+ text: "",
1446
+ stopReason,
1447
+ hasText: false,
1448
+ toolCalls: [],
1449
+ diffs: [],
1450
+ thoughts: ""
1451
+ };
1452
+ }
1453
+
1404
1454
  // src/client/file-server.ts
1405
1455
  import { randomBytes } from "node:crypto";
1406
1456
  import { realpathSync } from "node:fs";
@@ -2076,7 +2126,7 @@ function finitePositiveLimit(value, fallback) {
2076
2126
  return value !== void 0 && Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
2077
2127
  }
2078
2128
 
2079
- // src/client/acp-session.ts
2129
+ // src/client/acp-session-errors.ts
2080
2130
  var ACPSessionError = class extends Error {
2081
2131
  kind;
2082
2132
  cause;
@@ -2090,6 +2140,264 @@ var ACPSessionError = class extends Error {
2090
2140
  function isJsonRpcError(v) {
2091
2141
  return typeof v === "object" && v !== null && typeof v.code === "number" && typeof v.message === "string";
2092
2142
  }
2143
+
2144
+ // src/client/acp-session-updates.ts
2145
+ function createSessionScratch() {
2146
+ return { text: "", thoughts: "", toolCalls: /* @__PURE__ */ new Map(), diffs: [] };
2147
+ }
2148
+ function handleAcpSessionUpdate(msg, scratch, emitProgress) {
2149
+ const update = msg.params?.update;
2150
+ if (typeof update !== "object" || update === null) return;
2151
+ const u = update;
2152
+ emitProgress({ type: "raw", update: u });
2153
+ switch (u.sessionUpdate) {
2154
+ case "agent_message_chunk": {
2155
+ const text = extractText(u.content);
2156
+ if (text) {
2157
+ scratch.text += text;
2158
+ emitProgress({ type: "message", text });
2159
+ }
2160
+ return;
2161
+ }
2162
+ case "thought_chunk": {
2163
+ const text = extractText(u.content);
2164
+ if (text) {
2165
+ scratch.thoughts += text;
2166
+ emitProgress({ type: "thought", text });
2167
+ }
2168
+ return;
2169
+ }
2170
+ case "tool_call":
2171
+ case "tool_call_update":
2172
+ captureToolCall(u, u.sessionUpdate === "tool_call", scratch, emitProgress);
2173
+ return;
2174
+ case "plan":
2175
+ if (Array.isArray(u.entries)) {
2176
+ scratch.plan = u.entries;
2177
+ emitProgress({ type: "plan", entries: u.entries });
2178
+ }
2179
+ return;
2180
+ case "usage_update":
2181
+ if (typeof u.used === "number" && typeof u.size === "number") {
2182
+ const usage = {
2183
+ used: u.used,
2184
+ size: u.size,
2185
+ ...typeof u.cost === "object" && u.cost !== null ? { cost: u.cost } : {}
2186
+ };
2187
+ scratch.usage = usage;
2188
+ emitProgress({ type: "usage", usage });
2189
+ }
2190
+ return;
2191
+ case "available_commands_update":
2192
+ case "current_mode_update":
2193
+ case "config_option_update":
2194
+ case "session_info_update":
2195
+ case "user_message_chunk":
2196
+ case "next_edit_suggestions":
2197
+ case "elicitation":
2198
+ return;
2199
+ default:
2200
+ return;
2201
+ }
2202
+ }
2203
+ function captureToolCall(u, isNew, scratch, emitProgress) {
2204
+ const toolCallId = typeof u.toolCallId === "string" ? u.toolCallId : "";
2205
+ if (!toolCallId) return;
2206
+ const prev = scratch.toolCalls.get(toolCallId);
2207
+ const record = {
2208
+ toolCallId,
2209
+ title: typeof u.title === "string" ? u.title : prev?.title ?? toolCallId,
2210
+ kind: typeof u.kind === "string" ? u.kind : prev?.kind,
2211
+ status: typeof u.status === "string" ? u.status : prev?.status ?? (isNew ? "pending" : "in_progress"),
2212
+ rawInput: isRecord(u.rawInput) ? u.rawInput : prev?.rawInput,
2213
+ rawOutput: isRecord(u.rawOutput) ? u.rawOutput : prev?.rawOutput
2214
+ };
2215
+ scratch.toolCalls.set(toolCallId, record);
2216
+ if (Array.isArray(u.content)) {
2217
+ for (const c of u.content) {
2218
+ if (c && typeof c === "object" && c.type === "diff") {
2219
+ const diff = {
2220
+ path: c.path,
2221
+ oldText: c.oldText,
2222
+ newText: c.newText
2223
+ };
2224
+ scratch.diffs.push(diff);
2225
+ emitProgress({ type: "diff", diff });
2226
+ }
2227
+ }
2228
+ }
2229
+ emitProgress({
2230
+ type: isNew ? "tool_call" : "tool_call_update",
2231
+ toolCall: record
2232
+ });
2233
+ }
2234
+
2235
+ // src/client/acp-session-callbacks.ts
2236
+ async function handleAcpPermissionRequest(msg, permissionPolicy, sender) {
2237
+ const id = msg.id;
2238
+ if (id === void 0) return;
2239
+ const params = msg.params;
2240
+ const toolCall = params?.toolCall;
2241
+ const options = Array.isArray(params?.options) ? params.options : [];
2242
+ if (!toolCall) {
2243
+ await sender.sendErrorResponse(id, -32602, "toolCall is required");
2244
+ return;
2245
+ }
2246
+ const policyAbort = new AbortController();
2247
+ try {
2248
+ const outcome = await permissionPolicy({
2249
+ toolCall,
2250
+ options,
2251
+ signal: policyAbort.signal
2252
+ });
2253
+ await sender.sendResult(id, { outcome });
2254
+ } catch (err) {
2255
+ const message = err instanceof Error ? err.message : String(err);
2256
+ await sender.sendErrorResponse(id, -32603, `permission policy failed: ${message}`);
2257
+ }
2258
+ }
2259
+ async function handleAcpFsRequest(msg, fileServer, permissionPolicy, sender) {
2260
+ const id = msg.id;
2261
+ if (id === void 0) return;
2262
+ const params = msg.params;
2263
+ if (!params?.path) {
2264
+ await sender.sendErrorResponse(id, -32602, "path is required");
2265
+ return;
2266
+ }
2267
+ if (msg.method === "fs/write_text_file") {
2268
+ const allowed = await authorizeAcpCallback(permissionPolicy, {
2269
+ toolCallId: `acp-fs-write-${id}`,
2270
+ title: `Write file: ${params.path}`,
2271
+ kind: "edit",
2272
+ rawInput: { path: params.path, sessionId: params.sessionId }
2273
+ });
2274
+ if (!allowed) {
2275
+ await sender.sendErrorResponse(id, -32602, "filesystem write denied by permission policy");
2276
+ return;
2277
+ }
2278
+ }
2279
+ try {
2280
+ if (msg.method === "fs/read_text_file") {
2281
+ const result = await fileServer.readTextFile({
2282
+ sessionId: params.sessionId ?? "",
2283
+ path: params.path
2284
+ });
2285
+ await sender.sendResult(id, result);
2286
+ } else {
2287
+ await fileServer.writeTextFile({
2288
+ sessionId: params.sessionId ?? "",
2289
+ path: params.path,
2290
+ content: params.content ?? ""
2291
+ });
2292
+ await sender.sendResult(id, {});
2293
+ }
2294
+ } catch (err) {
2295
+ const code = err instanceof FsError ? -32602 : -32603;
2296
+ const message = err instanceof Error ? err.message : String(err);
2297
+ await sender.sendErrorResponse(id, code, message);
2298
+ }
2299
+ }
2300
+ async function handleAcpTerminalRequest(msg, terminalServer, permissionPolicy, sender) {
2301
+ const id = msg.id;
2302
+ if (id === void 0) return;
2303
+ const params = msg.params ?? {};
2304
+ try {
2305
+ switch (msg.method) {
2306
+ case "terminal/create": {
2307
+ const allowed = await authorizeAcpCallback(permissionPolicy, {
2308
+ toolCallId: `acp-terminal-create-${id}`,
2309
+ title: `Run command: ${String(params.command ?? "")} ${(Array.isArray(params.args) ? params.args : []).join(" ")}`.trim(),
2310
+ kind: "execute",
2311
+ rawInput: {
2312
+ command: params.command,
2313
+ args: params.args,
2314
+ cwd: params.cwd,
2315
+ sessionId: params.sessionId
2316
+ }
2317
+ });
2318
+ if (!allowed) {
2319
+ await sender.sendErrorResponse(id, -32602, "terminal create denied by permission policy");
2320
+ return;
2321
+ }
2322
+ const createOpts = {
2323
+ sessionId: String(params.sessionId ?? ""),
2324
+ command: String(params.command ?? ""),
2325
+ args: Array.isArray(params.args) ? params.args : []
2326
+ };
2327
+ if (Array.isArray(params.env)) {
2328
+ createOpts.env = params.env;
2329
+ }
2330
+ if (typeof params.cwd === "string") {
2331
+ createOpts.cwd = params.cwd;
2332
+ }
2333
+ if (typeof params.outputByteLimit === "number") {
2334
+ createOpts.outputByteLimit = params.outputByteLimit;
2335
+ }
2336
+ const result = terminalServer.create(createOpts);
2337
+ await sender.sendResult(id, result);
2338
+ return;
2339
+ }
2340
+ case "terminal/output": {
2341
+ const terminalId = String(params.terminalId ?? "");
2342
+ const out = terminalServer.output(terminalId);
2343
+ await sender.sendResult(id, out);
2344
+ return;
2345
+ }
2346
+ case "terminal/wait_for_exit": {
2347
+ const terminalId = String(params.terminalId ?? "");
2348
+ const exit = await terminalServer.waitForExit(terminalId);
2349
+ await sender.sendResult(id, exit);
2350
+ return;
2351
+ }
2352
+ case "terminal/kill": {
2353
+ const terminalId = String(params.terminalId ?? "");
2354
+ terminalServer.kill(terminalId);
2355
+ await sender.sendResult(id, {});
2356
+ return;
2357
+ }
2358
+ case "terminal/release": {
2359
+ const terminalId = String(params.terminalId ?? "");
2360
+ terminalServer.release(terminalId);
2361
+ await sender.sendResult(id, {});
2362
+ return;
2363
+ }
2364
+ default:
2365
+ await sender.sendErrorResponse(id, -32601, `unknown method: ${msg.method}`);
2366
+ }
2367
+ } catch (err) {
2368
+ const message = err instanceof Error ? err.message : String(err);
2369
+ await sender.sendErrorResponse(id, -32603, message);
2370
+ }
2371
+ }
2372
+ async function authorizeAcpCallback(permissionPolicy, partial) {
2373
+ try {
2374
+ const outcome = await permissionPolicy({
2375
+ toolCall: {
2376
+ sessionUpdate: "tool_call_update",
2377
+ toolCallId: partial.toolCallId,
2378
+ title: partial.title,
2379
+ kind: partial.kind,
2380
+ status: "pending",
2381
+ ...partial.rawInput ? { rawInput: partial.rawInput } : {}
2382
+ },
2383
+ options: [
2384
+ { optionId: "allow", name: "Allow", kind: "allow_once" },
2385
+ { optionId: "reject", name: "Reject", kind: "reject_once" }
2386
+ ],
2387
+ signal: new AbortController().signal
2388
+ });
2389
+ return outcome.outcome === "selected" && outcome.optionId !== "reject" && outcome.optionId !== "reject_once" && outcome.optionId !== "reject_always";
2390
+ } catch {
2391
+ return false;
2392
+ }
2393
+ }
2394
+
2395
+ // src/client/acp-message-routing.ts
2396
+ function isBestEffortAckMethod(method) {
2397
+ return method === "mcp/connect" || method === "mcp/message" || method === "mcp/disconnect" || method === "elicitation/create" || method === "elicitation/complete";
2398
+ }
2399
+
2400
+ // src/client/acp-session.ts
2093
2401
  var ACPSession = class _ACPSession {
2094
2402
  transport;
2095
2403
  fileServer;
@@ -2783,6 +3091,12 @@ var ACPSession = class _ACPSession {
2783
3091
  error: { code, message }
2784
3092
  });
2785
3093
  }
3094
+ responseSender() {
3095
+ return {
3096
+ sendResult: (id, result) => this.sendResult(id, result),
3097
+ sendErrorResponse: (id, code, message) => this.sendErrorResponse(id, code, message)
3098
+ };
3099
+ }
2786
3100
  handleMessage(msg) {
2787
3101
  if (msg.id !== void 0 && (msg.result !== void 0 || msg.error !== void 0)) {
2788
3102
  const pending = this.pending.get(msg.id);
@@ -2797,29 +3111,32 @@ var ACPSession = class _ACPSession {
2797
3111
  return;
2798
3112
  }
2799
3113
  if (msg.method === "session/update") {
2800
- this.handleUpdate(msg);
3114
+ handleAcpSessionUpdate(msg, this.scratch, (event) => this.emitProgress(event));
2801
3115
  return;
2802
3116
  }
2803
3117
  if (msg.method === "session/request_permission") {
2804
- void this.handlePermissionRequest(msg);
3118
+ void handleAcpPermissionRequest(msg, this.permissionPolicy, this.responseSender());
2805
3119
  return;
2806
3120
  }
2807
3121
  if (msg.method === "fs/read_text_file" || msg.method === "fs/write_text_file") {
2808
- void this.handleFsRequest(msg);
3122
+ void handleAcpFsRequest(
3123
+ msg,
3124
+ this.fileServer,
3125
+ this.permissionPolicy,
3126
+ this.responseSender()
3127
+ );
2809
3128
  return;
2810
3129
  }
2811
3130
  if (msg.method?.startsWith("terminal/")) {
2812
- void this.handleTerminalRequest(msg);
2813
- return;
2814
- }
2815
- if (msg.method === "mcp/connect" || msg.method === "mcp/message" || msg.method === "mcp/disconnect") {
2816
- if (msg.id !== void 0) {
2817
- this.sendResult(msg.id, {}).catch(() => {
2818
- });
2819
- }
3131
+ void handleAcpTerminalRequest(
3132
+ msg,
3133
+ this.terminalServer,
3134
+ this.permissionPolicy,
3135
+ this.responseSender()
3136
+ );
2820
3137
  return;
2821
3138
  }
2822
- if (msg.method === "elicitation/create" || msg.method === "elicitation/complete") {
3139
+ if (isBestEffortAckMethod(msg.method)) {
2823
3140
  if (msg.id !== void 0) {
2824
3141
  this.sendResult(msg.id, {}).catch(() => {
2825
3142
  });
@@ -2840,98 +3157,6 @@ var ACPSession = class _ACPSession {
2840
3157
  );
2841
3158
  }
2842
3159
  }
2843
- handleUpdate(msg) {
2844
- const update = msg.params?.update;
2845
- if (typeof update !== "object" || update === null) return;
2846
- const u = update;
2847
- this.emitProgress({ type: "raw", update: u });
2848
- switch (u.sessionUpdate) {
2849
- case "agent_message_chunk": {
2850
- const text = extractText(u.content);
2851
- if (text) {
2852
- this.scratch.text += text;
2853
- this.emitProgress({ type: "message", text });
2854
- }
2855
- return;
2856
- }
2857
- case "thought_chunk": {
2858
- const text = extractText(u.content);
2859
- if (text) {
2860
- this.scratch.thoughts += text;
2861
- this.emitProgress({ type: "thought", text });
2862
- }
2863
- return;
2864
- }
2865
- case "tool_call":
2866
- case "tool_call_update": {
2867
- this.captureToolCall(u, u.sessionUpdate === "tool_call");
2868
- return;
2869
- }
2870
- case "plan":
2871
- if (Array.isArray(u.entries)) {
2872
- this.scratch.plan = u.entries;
2873
- this.emitProgress({ type: "plan", entries: u.entries });
2874
- }
2875
- return;
2876
- case "usage_update":
2877
- if (typeof u.used === "number" && typeof u.size === "number") {
2878
- const usage = {
2879
- used: u.used,
2880
- size: u.size,
2881
- ...typeof u.cost === "object" && u.cost !== null ? { cost: u.cost } : {}
2882
- };
2883
- this.scratch.usage = usage;
2884
- this.emitProgress({ type: "usage", usage });
2885
- }
2886
- return;
2887
- case "available_commands_update":
2888
- case "current_mode_update":
2889
- case "config_option_update":
2890
- case "session_info_update":
2891
- case "user_message_chunk":
2892
- case "next_edit_suggestions":
2893
- case "elicitation":
2894
- return;
2895
- default:
2896
- return;
2897
- }
2898
- }
2899
- /**
2900
- * Fold a `tool_call` / `tool_call_update` notification into the scratch
2901
- * tool-call map (deduped by toolCallId), extract any `diff` content into
2902
- * the diffs list, and emit live progress.
2903
- */
2904
- captureToolCall(u, isNew) {
2905
- const toolCallId = typeof u.toolCallId === "string" ? u.toolCallId : "";
2906
- if (!toolCallId) return;
2907
- const prev = this.scratch.toolCalls.get(toolCallId);
2908
- const record = {
2909
- toolCallId,
2910
- title: typeof u.title === "string" ? u.title : prev?.title ?? toolCallId,
2911
- kind: typeof u.kind === "string" ? u.kind : prev?.kind,
2912
- status: typeof u.status === "string" ? u.status : prev?.status ?? (isNew ? "pending" : "in_progress"),
2913
- rawInput: isRecord(u.rawInput) ? u.rawInput : prev?.rawInput,
2914
- rawOutput: isRecord(u.rawOutput) ? u.rawOutput : prev?.rawOutput
2915
- };
2916
- this.scratch.toolCalls.set(toolCallId, record);
2917
- if (Array.isArray(u.content)) {
2918
- for (const c of u.content) {
2919
- if (c && typeof c === "object" && c.type === "diff") {
2920
- const diff = {
2921
- path: c.path,
2922
- oldText: c.oldText,
2923
- newText: c.newText
2924
- };
2925
- this.scratch.diffs.push(diff);
2926
- this.emitProgress({ type: "diff", diff });
2927
- }
2928
- }
2929
- }
2930
- this.emitProgress({
2931
- type: isNew ? "tool_call" : "tool_call_update",
2932
- toolCall: record
2933
- });
2934
- }
2935
3160
  emitProgress(event) {
2936
3161
  if (!this.progressHandler) return;
2937
3162
  try {
@@ -2942,217 +3167,11 @@ var ACPSession = class _ACPSession {
2942
3167
  /** Live progress handler installed for the duration of a `prompt()` turn. */
2943
3168
  progressHandler = null;
2944
3169
  // Per-prompt scratch state
2945
- scratch = { text: "", thoughts: "", toolCalls: /* @__PURE__ */ new Map(), diffs: [] };
3170
+ scratch = createSessionScratch();
2946
3171
  resetScratch() {
2947
- this.scratch = { text: "", thoughts: "", toolCalls: /* @__PURE__ */ new Map(), diffs: [] };
2948
- }
2949
- async handlePermissionRequest(msg) {
2950
- const id = msg.id;
2951
- if (id === void 0) return;
2952
- const params = msg.params;
2953
- const toolCall = params?.toolCall;
2954
- const options = Array.isArray(params?.options) ? params.options : [];
2955
- if (!toolCall) {
2956
- await this.sendErrorResponse(id, -32602, "toolCall is required");
2957
- return;
2958
- }
2959
- const policyAbort = new AbortController();
2960
- try {
2961
- const outcome = await this.permissionPolicy({
2962
- toolCall,
2963
- options,
2964
- signal: policyAbort.signal
2965
- });
2966
- await this.sendResult(id, { outcome });
2967
- } catch (err) {
2968
- const message = err instanceof Error ? err.message : String(err);
2969
- await this.sendErrorResponse(id, -32603, `permission policy failed: ${message}`);
2970
- }
2971
- }
2972
- /**
2973
- * Enforce authorization at privileged callback sinks (fs/write,
2974
- * terminal/create). Unlike `handlePermissionRequest` which responds to
2975
- * agent-initiated `session/request_permission` messages, this method is
2976
- * called by the handler BEFORE dispatching to FileServer/TerminalServer,
2977
- * closing the gap where the agent simply skips the voluntary permission
2978
- * request and sends the privileged callback directly.
2979
- *
2980
- * Uses the session's permission policy. The default
2981
- * (`readOnlyPermissionPolicy`) auto-approves only side-effect-free tool
2982
- * calls (read/search/fetch/think) and rejects everything else — this is
2983
- * the safe-by-default posture. For trusted local agents (CLI `acp spawn`,
2984
- * Director fan-out), inject `defaultPermissionPolicy` to grant
2985
- * write/execute access.
2986
- *
2987
- * Returns true if the callback is authorized, false if denied.
2988
- */
2989
- async authorizeCallback(partial) {
2990
- try {
2991
- const outcome = await this.permissionPolicy({
2992
- toolCall: {
2993
- sessionUpdate: "tool_call_update",
2994
- toolCallId: partial.toolCallId,
2995
- title: partial.title,
2996
- kind: partial.kind,
2997
- status: "pending",
2998
- ...partial.rawInput ? { rawInput: partial.rawInput } : {}
2999
- },
3000
- options: [
3001
- { optionId: "allow", name: "Allow", kind: "allow_once" },
3002
- { optionId: "reject", name: "Reject", kind: "reject_once" }
3003
- ],
3004
- signal: new AbortController().signal
3005
- });
3006
- return outcome.outcome === "selected" && outcome.optionId !== "reject" && outcome.optionId !== "reject_once" && outcome.optionId !== "reject_always";
3007
- } catch {
3008
- return false;
3009
- }
3010
- }
3011
- async handleFsRequest(msg) {
3012
- const id = msg.id;
3013
- if (id === void 0) return;
3014
- const params = msg.params;
3015
- if (!params?.path) {
3016
- await this.sendErrorResponse(id, -32602, "path is required");
3017
- return;
3018
- }
3019
- if (msg.method === "fs/write_text_file") {
3020
- const allowed = await this.authorizeCallback({
3021
- toolCallId: `acp-fs-write-${id}`,
3022
- title: `Write file: ${params.path}`,
3023
- kind: "edit",
3024
- rawInput: { path: params.path, sessionId: params.sessionId }
3025
- });
3026
- if (!allowed) {
3027
- await this.sendErrorResponse(id, -32602, "filesystem write denied by permission policy");
3028
- return;
3029
- }
3030
- }
3031
- try {
3032
- if (msg.method === "fs/read_text_file") {
3033
- const result = await this.fileServer.readTextFile({
3034
- sessionId: params.sessionId ?? "",
3035
- path: params.path
3036
- });
3037
- await this.sendResult(id, result);
3038
- } else {
3039
- await this.fileServer.writeTextFile({
3040
- sessionId: params.sessionId ?? "",
3041
- path: params.path,
3042
- content: params.content ?? ""
3043
- });
3044
- await this.sendResult(id, {});
3045
- }
3046
- } catch (err) {
3047
- const code = err instanceof FsError ? -32602 : -32603;
3048
- const message = err instanceof Error ? err.message : String(err);
3049
- await this.sendErrorResponse(id, code, message);
3050
- }
3051
- }
3052
- async handleTerminalRequest(msg) {
3053
- const id = msg.id;
3054
- if (id === void 0) return;
3055
- const params = msg.params ?? {};
3056
- try {
3057
- switch (msg.method) {
3058
- case "terminal/create": {
3059
- const allowed = await this.authorizeCallback({
3060
- toolCallId: `acp-terminal-create-${id}`,
3061
- title: `Run command: ${String(params.command ?? "")} ${(Array.isArray(params.args) ? params.args : []).join(" ")}`.trim(),
3062
- kind: "execute",
3063
- rawInput: {
3064
- command: params.command,
3065
- args: params.args,
3066
- cwd: params.cwd,
3067
- sessionId: params.sessionId
3068
- }
3069
- });
3070
- if (!allowed) {
3071
- await this.sendErrorResponse(id, -32602, "terminal create denied by permission policy");
3072
- return;
3073
- }
3074
- const createOpts = {
3075
- sessionId: String(params.sessionId ?? ""),
3076
- command: String(params.command ?? ""),
3077
- args: Array.isArray(params.args) ? params.args : []
3078
- };
3079
- if (Array.isArray(params.env)) {
3080
- createOpts.env = params.env;
3081
- }
3082
- if (typeof params.cwd === "string") {
3083
- createOpts.cwd = params.cwd;
3084
- }
3085
- if (typeof params.outputByteLimit === "number") {
3086
- createOpts.outputByteLimit = params.outputByteLimit;
3087
- }
3088
- const result = this.terminalServer.create(createOpts);
3089
- await this.sendResult(id, result);
3090
- return;
3091
- }
3092
- case "terminal/output": {
3093
- const terminalId = String(params.terminalId ?? "");
3094
- const out = this.terminalServer.output(terminalId);
3095
- await this.sendResult(id, out);
3096
- return;
3097
- }
3098
- case "terminal/wait_for_exit": {
3099
- const terminalId = String(params.terminalId ?? "");
3100
- const exit = await this.terminalServer.waitForExit(terminalId);
3101
- await this.sendResult(id, exit);
3102
- return;
3103
- }
3104
- case "terminal/kill": {
3105
- const terminalId = String(params.terminalId ?? "");
3106
- this.terminalServer.kill(terminalId);
3107
- await this.sendResult(id, {});
3108
- return;
3109
- }
3110
- case "terminal/release": {
3111
- const terminalId = String(params.terminalId ?? "");
3112
- this.terminalServer.release(terminalId);
3113
- await this.sendResult(id, {});
3114
- return;
3115
- }
3116
- default:
3117
- await this.sendErrorResponse(id, -32601, `unknown method: ${msg.method}`);
3118
- }
3119
- } catch (err) {
3120
- const message = err instanceof Error ? err.message : String(err);
3121
- await this.sendErrorResponse(id, -32603, message);
3122
- }
3172
+ this.scratch = createSessionScratch();
3123
3173
  }
3124
3174
  };
3125
- function textContent(text) {
3126
- return { type: "text", text };
3127
- }
3128
- function imageContent(mimeType, data) {
3129
- return { type: "image", mimeType, data };
3130
- }
3131
- function audioContent(mimeType, data) {
3132
- return { type: "audio", mimeType, data };
3133
- }
3134
- function extractText(block) {
3135
- if (typeof block !== "object" || block === null) return "";
3136
- const b = block;
3137
- if (b.type === "text" && typeof b.text === "string") return b.text;
3138
- if (b.type === "resource" && b.resource && typeof b.resource === "object" && typeof b.resource.text === "string") {
3139
- return b.resource.text;
3140
- }
3141
- return "";
3142
- }
3143
- function isRecord(v) {
3144
- return typeof v === "object" && v !== null && !Array.isArray(v);
3145
- }
3146
- function emptyRunResult(stopReason) {
3147
- return {
3148
- text: "",
3149
- stopReason,
3150
- hasText: false,
3151
- toolCalls: [],
3152
- diffs: [],
3153
- thoughts: ""
3154
- };
3155
- }
3156
3175
 
3157
3176
  // src/client/tool-translator.ts
3158
3177
  import { expectDefined as expectDefined2 } from "@wrongstack/core/utils";