@wrongstack/acp 0.298.0 → 0.298.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.
@@ -6,7 +6,22 @@ export interface ACPResponseSender {
6
6
  sendResult(id: string | number, result: unknown): Promise<void>;
7
7
  sendErrorResponse(id: string | number, code: number, message: string): Promise<void>;
8
8
  }
9
- export declare function handleAcpPermissionRequest(msg: ACPMessage, permissionPolicy: PermissionPolicy, sender: ACPResponseSender): Promise<void>;
10
- export declare function handleAcpFsRequest(msg: ACPMessage, fileServer: FileServer, permissionPolicy: PermissionPolicy, sender: ACPResponseSender): Promise<void>;
11
- export declare function handleAcpTerminalRequest(msg: ACPMessage, terminalServer: TerminalServer, permissionPolicy: PermissionPolicy, sender: ACPResponseSender): Promise<void>;
9
+ export interface ACPCallbackOptions {
10
+ /** Abort when the owning ACP session/prompt is closed or cancelled. */
11
+ signal?: AbortSignal | undefined;
12
+ /**
13
+ * Maximum time to wait for callback authorization before failing closed.
14
+ * Defaults to {@link DEFAULT_PERMISSION_TIMEOUT_MS}. Pass
15
+ * `Number.POSITIVE_INFINITY` to opt out of the wall-clock deadline (e.g.
16
+ * hosts whose permission policy waits for a human decision, where a short
17
+ * cap would auto-fail approvals the user is still considering); the
18
+ * host's `signal` still bounds the wait and fails closed on teardown.
19
+ * Non-positive or non-finite values (other than the opt-out) fall back to
20
+ * the fail-closed default.
21
+ */
22
+ permissionTimeoutMs?: number | undefined;
23
+ }
24
+ export declare function handleAcpPermissionRequest(msg: ACPMessage, permissionPolicy: PermissionPolicy, sender: ACPResponseSender, callbackOptions?: ACPCallbackOptions): Promise<void>;
25
+ export declare function handleAcpFsRequest(msg: ACPMessage, fileServer: FileServer, permissionPolicy: PermissionPolicy, sender: ACPResponseSender, callbackOptions?: ACPCallbackOptions): Promise<void>;
26
+ export declare function handleAcpTerminalRequest(msg: ACPMessage, terminalServer: TerminalServer, permissionPolicy: PermissionPolicy, sender: ACPResponseSender, callbackOptions?: ACPCallbackOptions): Promise<void>;
12
27
  //# sourceMappingURL=acp-session-callbacks.d.ts.map
@@ -23,6 +23,8 @@ export declare class ACPSession {
23
23
  private readonly timeoutMs;
24
24
  private readonly opts;
25
25
  private transportOff;
26
+ private readonly callbackAbort;
27
+ private promptCallbackAbort;
26
28
  private state;
27
29
  private sessionId;
28
30
  /** Pending outbound requests (initialize, session/new, session/prompt, etc). */
@@ -204,6 +206,7 @@ export declare class ACPSession {
204
206
  /** Send a JSON-RPC 2.0 error response (no `method` field, per spec). */
205
207
  private sendErrorResponse;
206
208
  private responseSender;
209
+ private callbackOptions;
207
210
  private handleMessage;
208
211
  private emitProgress;
209
212
  /** Live progress handler installed for the duration of a `prompt()` turn. */
package/dist/client.js CHANGED
@@ -1109,30 +1109,34 @@ function captureToolCall(u, isNew, scratch, emitProgress) {
1109
1109
  }
1110
1110
 
1111
1111
  // src/client/acp-session-callbacks.ts
1112
- async function handleAcpPermissionRequest(msg, permissionPolicy, sender) {
1112
+ var DEFAULT_PERMISSION_TIMEOUT_MS = 6e4;
1113
+ async function handleAcpPermissionRequest(msg, permissionPolicy, sender, callbackOptions = {}) {
1113
1114
  const id = msg.id;
1114
1115
  if (id === void 0) return;
1115
1116
  const params = msg.params;
1116
1117
  const toolCall = params?.toolCall;
1117
- const options = Array.isArray(params?.options) ? params.options : [];
1118
+ const permissionOptions = Array.isArray(params?.options) ? params.options : [];
1118
1119
  if (!toolCall) {
1119
1120
  await sender.sendErrorResponse(id, -32602, "toolCall is required");
1120
1121
  return;
1121
1122
  }
1122
- const policyAbort = new AbortController();
1123
1123
  try {
1124
- const outcome = await permissionPolicy({
1125
- toolCall,
1126
- options,
1127
- signal: policyAbort.signal
1128
- });
1124
+ const outcome = await runPermissionWithDeadline(
1125
+ permissionPolicy,
1126
+ {
1127
+ toolCall,
1128
+ options: permissionOptions
1129
+ },
1130
+ callbackOptions
1131
+ );
1129
1132
  await sender.sendResult(id, { outcome });
1130
1133
  } catch (err) {
1131
1134
  const message = err instanceof Error ? err.message : String(err);
1132
- await sender.sendErrorResponse(id, -32603, `permission policy failed: ${message}`);
1135
+ const code = isAbortLikeError(err) ? -32800 : -32603;
1136
+ await sender.sendErrorResponse(id, code, `permission policy failed: ${message}`);
1133
1137
  }
1134
1138
  }
1135
- async function handleAcpFsRequest(msg, fileServer, permissionPolicy, sender) {
1139
+ async function handleAcpFsRequest(msg, fileServer, permissionPolicy, sender, callbackOptions = {}) {
1136
1140
  const id = msg.id;
1137
1141
  if (id === void 0) return;
1138
1142
  const params = msg.params;
@@ -1141,14 +1145,23 @@ async function handleAcpFsRequest(msg, fileServer, permissionPolicy, sender) {
1141
1145
  return;
1142
1146
  }
1143
1147
  if (msg.method === "fs/write_text_file") {
1144
- const allowed = await authorizeAcpCallback(permissionPolicy, {
1145
- toolCallId: `acp-fs-write-${id}`,
1146
- title: `Write file: ${params.path}`,
1147
- kind: "edit",
1148
- rawInput: { path: params.path, sessionId: params.sessionId }
1149
- });
1150
- if (!allowed) {
1151
- await sender.sendErrorResponse(id, -32602, "filesystem write denied by permission policy");
1148
+ const authorization = await authorizeAcpCallback(
1149
+ permissionPolicy,
1150
+ {
1151
+ toolCallId: `acp-fs-write-${id}`,
1152
+ title: `Write file: ${params.path}`,
1153
+ kind: "edit",
1154
+ rawInput: { path: params.path, sessionId: params.sessionId }
1155
+ },
1156
+ callbackOptions
1157
+ );
1158
+ if (authorization !== "allowed") {
1159
+ const isCancelled = authorization === "cancelled";
1160
+ await sender.sendErrorResponse(
1161
+ id,
1162
+ isCancelled ? -32800 : -32602,
1163
+ isCancelled ? "filesystem write permission request cancelled or timed out" : "filesystem write denied by permission policy"
1164
+ );
1152
1165
  return;
1153
1166
  }
1154
1167
  }
@@ -1173,26 +1186,35 @@ async function handleAcpFsRequest(msg, fileServer, permissionPolicy, sender) {
1173
1186
  await sender.sendErrorResponse(id, code, message);
1174
1187
  }
1175
1188
  }
1176
- async function handleAcpTerminalRequest(msg, terminalServer, permissionPolicy, sender) {
1189
+ async function handleAcpTerminalRequest(msg, terminalServer, permissionPolicy, sender, callbackOptions = {}) {
1177
1190
  const id = msg.id;
1178
1191
  if (id === void 0) return;
1179
1192
  const params = msg.params ?? {};
1180
1193
  try {
1181
1194
  switch (msg.method) {
1182
1195
  case "terminal/create": {
1183
- const allowed = await authorizeAcpCallback(permissionPolicy, {
1184
- toolCallId: `acp-terminal-create-${id}`,
1185
- title: `Run command: ${String(params.command ?? "")} ${(Array.isArray(params.args) ? params.args : []).join(" ")}`.trim(),
1186
- kind: "execute",
1187
- rawInput: {
1188
- command: params.command,
1189
- args: params.args,
1190
- cwd: params.cwd,
1191
- sessionId: params.sessionId
1192
- }
1193
- });
1194
- if (!allowed) {
1195
- await sender.sendErrorResponse(id, -32602, "terminal create denied by permission policy");
1196
+ const authorization = await authorizeAcpCallback(
1197
+ permissionPolicy,
1198
+ {
1199
+ toolCallId: `acp-terminal-create-${id}`,
1200
+ title: `Run command: ${String(params.command ?? "")} ${(Array.isArray(params.args) ? params.args : []).join(" ")}`.trim(),
1201
+ kind: "execute",
1202
+ rawInput: {
1203
+ command: params.command,
1204
+ args: params.args,
1205
+ cwd: params.cwd,
1206
+ sessionId: params.sessionId
1207
+ }
1208
+ },
1209
+ callbackOptions
1210
+ );
1211
+ if (authorization !== "allowed") {
1212
+ const isCancelled = authorization === "cancelled";
1213
+ await sender.sendErrorResponse(
1214
+ id,
1215
+ isCancelled ? -32800 : -32602,
1216
+ isCancelled ? "terminal create permission request cancelled or timed out" : "terminal create denied by permission policy"
1217
+ );
1196
1218
  return;
1197
1219
  }
1198
1220
  const createOpts = {
@@ -1245,28 +1267,77 @@ async function handleAcpTerminalRequest(msg, terminalServer, permissionPolicy, s
1245
1267
  await sender.sendErrorResponse(id, -32603, message);
1246
1268
  }
1247
1269
  }
1248
- async function authorizeAcpCallback(permissionPolicy, partial) {
1270
+ async function authorizeAcpCallback(permissionPolicy, partial, callbackOptions) {
1249
1271
  try {
1250
- const outcome = await permissionPolicy({
1251
- toolCall: {
1252
- sessionUpdate: "tool_call_update",
1253
- toolCallId: partial.toolCallId,
1254
- title: partial.title,
1255
- kind: partial.kind,
1256
- status: "pending",
1257
- ...partial.rawInput ? { rawInput: partial.rawInput } : {}
1272
+ const outcome = await runPermissionWithDeadline(
1273
+ permissionPolicy,
1274
+ {
1275
+ toolCall: {
1276
+ sessionUpdate: "tool_call_update",
1277
+ toolCallId: partial.toolCallId,
1278
+ title: partial.title,
1279
+ kind: partial.kind,
1280
+ status: "pending",
1281
+ ...partial.rawInput ? { rawInput: partial.rawInput } : {}
1282
+ },
1283
+ options: [
1284
+ { optionId: "allow", name: "Allow", kind: "allow_once" },
1285
+ { optionId: "reject", name: "Reject", kind: "reject_once" }
1286
+ ]
1258
1287
  },
1259
- options: [
1260
- { optionId: "allow", name: "Allow", kind: "allow_once" },
1261
- { optionId: "reject", name: "Reject", kind: "reject_once" }
1262
- ],
1263
- signal: new AbortController().signal
1264
- });
1265
- return outcome.outcome === "selected" && outcome.optionId !== "reject" && outcome.optionId !== "reject_once" && outcome.optionId !== "reject_always";
1266
- } catch {
1267
- return false;
1288
+ callbackOptions
1289
+ );
1290
+ return outcome.outcome === "selected" && outcome.optionId !== "reject" && outcome.optionId !== "reject_once" && outcome.optionId !== "reject_always" ? "allowed" : "denied";
1291
+ } catch (err) {
1292
+ return isAbortLikeError(err) ? "cancelled" : "denied";
1293
+ }
1294
+ }
1295
+ async function runPermissionWithDeadline(permissionPolicy, call, callbackOptions) {
1296
+ const timeoutMs = resolvePermissionDeadline(callbackOptions.permissionTimeoutMs);
1297
+ const controller = new AbortController();
1298
+ const abort = () => controller.abort();
1299
+ const timer = timeoutMs === null ? null : setTimeout(abort, timeoutMs);
1300
+ let removeAbort;
1301
+ if (callbackOptions.signal) {
1302
+ if (callbackOptions.signal.aborted) {
1303
+ abort();
1304
+ } else {
1305
+ callbackOptions.signal.addEventListener("abort", abort, { once: true });
1306
+ removeAbort = () => callbackOptions.signal?.removeEventListener("abort", abort);
1307
+ }
1308
+ }
1309
+ try {
1310
+ return await Promise.race([
1311
+ permissionPolicy({
1312
+ toolCall: call.toolCall,
1313
+ options: call.options,
1314
+ signal: controller.signal
1315
+ }),
1316
+ rejectOnAbort(controller.signal)
1317
+ ]);
1318
+ } finally {
1319
+ if (timer !== null) clearTimeout(timer);
1320
+ removeAbort?.();
1268
1321
  }
1269
1322
  }
1323
+ function rejectOnAbort(signal) {
1324
+ return new Promise((_, reject) => {
1325
+ const rejectAbort = () => reject(new Error("permission request cancelled or timed out"));
1326
+ if (signal.aborted) {
1327
+ rejectAbort();
1328
+ return;
1329
+ }
1330
+ signal.addEventListener("abort", rejectAbort, { once: true });
1331
+ });
1332
+ }
1333
+ function isAbortLikeError(err) {
1334
+ return err instanceof Error && /cancelled|canceled|timed out|aborted/i.test(err.message);
1335
+ }
1336
+ function resolvePermissionDeadline(value) {
1337
+ if (value === Number.POSITIVE_INFINITY) return null;
1338
+ if (value !== void 0 && Number.isFinite(value) && value > 0) return Math.trunc(value);
1339
+ return DEFAULT_PERMISSION_TIMEOUT_MS;
1340
+ }
1270
1341
 
1271
1342
  // src/client/acp-message-routing.ts
1272
1343
  function isBestEffortAckMethod(method) {
@@ -1282,6 +1353,8 @@ var ACPSession = class _ACPSession {
1282
1353
  timeoutMs;
1283
1354
  opts;
1284
1355
  transportOff = null;
1356
+ callbackAbort = new AbortController();
1357
+ promptCallbackAbort = null;
1285
1358
  state = "init";
1286
1359
  sessionId = null;
1287
1360
  /** Pending outbound requests (initialize, session/new, session/prompt, etc). */
@@ -1796,8 +1869,10 @@ var ACPSession = class _ACPSession {
1796
1869
  this.timeoutMs
1797
1870
  );
1798
1871
  let cancelled = false;
1872
+ this.promptCallbackAbort = new AbortController();
1799
1873
  const onAbort = () => {
1800
1874
  cancelled = true;
1875
+ this.promptCallbackAbort?.abort();
1801
1876
  this.transport.send({
1802
1877
  jsonrpc: "2.0",
1803
1878
  method: "session/cancel",
@@ -1820,6 +1895,8 @@ var ACPSession = class _ACPSession {
1820
1895
  throw new ACPSessionError("prompt_failed", `session/prompt failed: ${msg}`, err);
1821
1896
  } finally {
1822
1897
  signal.removeEventListener("abort", onAbort);
1898
+ this.promptCallbackAbort?.abort();
1899
+ this.promptCallbackAbort = null;
1823
1900
  this.progressHandler = null;
1824
1901
  }
1825
1902
  this.state = "done";
@@ -1886,6 +1963,9 @@ var ACPSession = class _ACPSession {
1886
1963
  if (this.closed) return;
1887
1964
  this.closed = true;
1888
1965
  this.state = "closed";
1966
+ this.callbackAbort.abort();
1967
+ this.promptCallbackAbort?.abort();
1968
+ this.promptCallbackAbort = null;
1889
1969
  this.terminalServer.releaseAll();
1890
1970
  if (this.sessionId && this.agentCapabilities.sessionCapabilities?.close) {
1891
1971
  try {
@@ -1981,6 +2061,11 @@ var ACPSession = class _ACPSession {
1981
2061
  sendErrorResponse: (id, code, message) => this.sendErrorResponse(id, code, message)
1982
2062
  };
1983
2063
  }
2064
+ callbackOptions() {
2065
+ const promptSignal = this.promptCallbackAbort?.signal;
2066
+ const signal = promptSignal ? AbortSignal.any([this.callbackAbort.signal, promptSignal]) : this.callbackAbort.signal;
2067
+ return { signal, permissionTimeoutMs: Number.POSITIVE_INFINITY };
2068
+ }
1984
2069
  handleMessage(msg) {
1985
2070
  if (msg.id !== void 0 && (msg.result !== void 0 || msg.error !== void 0)) {
1986
2071
  const pending = this.pending.get(msg.id);
@@ -1999,25 +2084,35 @@ var ACPSession = class _ACPSession {
1999
2084
  return;
2000
2085
  }
2001
2086
  if (msg.method === "session/request_permission") {
2002
- void handleAcpPermissionRequest(msg, this.permissionPolicy, this.responseSender());
2087
+ handleAcpPermissionRequest(
2088
+ msg,
2089
+ this.permissionPolicy,
2090
+ this.responseSender(),
2091
+ this.callbackOptions()
2092
+ ).catch(() => {
2093
+ });
2003
2094
  return;
2004
2095
  }
2005
2096
  if (msg.method === "fs/read_text_file" || msg.method === "fs/write_text_file") {
2006
- void handleAcpFsRequest(
2097
+ handleAcpFsRequest(
2007
2098
  msg,
2008
2099
  this.fileServer,
2009
2100
  this.permissionPolicy,
2010
- this.responseSender()
2011
- );
2101
+ this.responseSender(),
2102
+ this.callbackOptions()
2103
+ ).catch(() => {
2104
+ });
2012
2105
  return;
2013
2106
  }
2014
2107
  if (msg.method?.startsWith("terminal/")) {
2015
- void handleAcpTerminalRequest(
2108
+ handleAcpTerminalRequest(
2016
2109
  msg,
2017
2110
  this.terminalServer,
2018
2111
  this.permissionPolicy,
2019
- this.responseSender()
2020
- );
2112
+ this.responseSender(),
2113
+ this.callbackOptions()
2114
+ ).catch(() => {
2115
+ });
2021
2116
  return;
2022
2117
  }
2023
2118
  if (isBestEffortAckMethod(msg.method)) {
package/dist/index.js CHANGED
@@ -2445,30 +2445,34 @@ function captureToolCall(u, isNew, scratch, emitProgress) {
2445
2445
  }
2446
2446
 
2447
2447
  // src/client/acp-session-callbacks.ts
2448
- async function handleAcpPermissionRequest(msg, permissionPolicy, sender) {
2448
+ var DEFAULT_PERMISSION_TIMEOUT_MS = 6e4;
2449
+ async function handleAcpPermissionRequest(msg, permissionPolicy, sender, callbackOptions = {}) {
2449
2450
  const id = msg.id;
2450
2451
  if (id === void 0) return;
2451
2452
  const params = msg.params;
2452
2453
  const toolCall = params?.toolCall;
2453
- const options = Array.isArray(params?.options) ? params.options : [];
2454
+ const permissionOptions = Array.isArray(params?.options) ? params.options : [];
2454
2455
  if (!toolCall) {
2455
2456
  await sender.sendErrorResponse(id, -32602, "toolCall is required");
2456
2457
  return;
2457
2458
  }
2458
- const policyAbort = new AbortController();
2459
2459
  try {
2460
- const outcome = await permissionPolicy({
2461
- toolCall,
2462
- options,
2463
- signal: policyAbort.signal
2464
- });
2460
+ const outcome = await runPermissionWithDeadline(
2461
+ permissionPolicy,
2462
+ {
2463
+ toolCall,
2464
+ options: permissionOptions
2465
+ },
2466
+ callbackOptions
2467
+ );
2465
2468
  await sender.sendResult(id, { outcome });
2466
2469
  } catch (err) {
2467
2470
  const message = err instanceof Error ? err.message : String(err);
2468
- await sender.sendErrorResponse(id, -32603, `permission policy failed: ${message}`);
2471
+ const code = isAbortLikeError(err) ? -32800 : -32603;
2472
+ await sender.sendErrorResponse(id, code, `permission policy failed: ${message}`);
2469
2473
  }
2470
2474
  }
2471
- async function handleAcpFsRequest(msg, fileServer, permissionPolicy, sender) {
2475
+ async function handleAcpFsRequest(msg, fileServer, permissionPolicy, sender, callbackOptions = {}) {
2472
2476
  const id = msg.id;
2473
2477
  if (id === void 0) return;
2474
2478
  const params = msg.params;
@@ -2477,14 +2481,23 @@ async function handleAcpFsRequest(msg, fileServer, permissionPolicy, sender) {
2477
2481
  return;
2478
2482
  }
2479
2483
  if (msg.method === "fs/write_text_file") {
2480
- const allowed = await authorizeAcpCallback(permissionPolicy, {
2481
- toolCallId: `acp-fs-write-${id}`,
2482
- title: `Write file: ${params.path}`,
2483
- kind: "edit",
2484
- rawInput: { path: params.path, sessionId: params.sessionId }
2485
- });
2486
- if (!allowed) {
2487
- await sender.sendErrorResponse(id, -32602, "filesystem write denied by permission policy");
2484
+ const authorization = await authorizeAcpCallback(
2485
+ permissionPolicy,
2486
+ {
2487
+ toolCallId: `acp-fs-write-${id}`,
2488
+ title: `Write file: ${params.path}`,
2489
+ kind: "edit",
2490
+ rawInput: { path: params.path, sessionId: params.sessionId }
2491
+ },
2492
+ callbackOptions
2493
+ );
2494
+ if (authorization !== "allowed") {
2495
+ const isCancelled = authorization === "cancelled";
2496
+ await sender.sendErrorResponse(
2497
+ id,
2498
+ isCancelled ? -32800 : -32602,
2499
+ isCancelled ? "filesystem write permission request cancelled or timed out" : "filesystem write denied by permission policy"
2500
+ );
2488
2501
  return;
2489
2502
  }
2490
2503
  }
@@ -2509,26 +2522,35 @@ async function handleAcpFsRequest(msg, fileServer, permissionPolicy, sender) {
2509
2522
  await sender.sendErrorResponse(id, code, message);
2510
2523
  }
2511
2524
  }
2512
- async function handleAcpTerminalRequest(msg, terminalServer, permissionPolicy, sender) {
2525
+ async function handleAcpTerminalRequest(msg, terminalServer, permissionPolicy, sender, callbackOptions = {}) {
2513
2526
  const id = msg.id;
2514
2527
  if (id === void 0) return;
2515
2528
  const params = msg.params ?? {};
2516
2529
  try {
2517
2530
  switch (msg.method) {
2518
2531
  case "terminal/create": {
2519
- const allowed = await authorizeAcpCallback(permissionPolicy, {
2520
- toolCallId: `acp-terminal-create-${id}`,
2521
- title: `Run command: ${String(params.command ?? "")} ${(Array.isArray(params.args) ? params.args : []).join(" ")}`.trim(),
2522
- kind: "execute",
2523
- rawInput: {
2524
- command: params.command,
2525
- args: params.args,
2526
- cwd: params.cwd,
2527
- sessionId: params.sessionId
2528
- }
2529
- });
2530
- if (!allowed) {
2531
- await sender.sendErrorResponse(id, -32602, "terminal create denied by permission policy");
2532
+ const authorization = await authorizeAcpCallback(
2533
+ permissionPolicy,
2534
+ {
2535
+ toolCallId: `acp-terminal-create-${id}`,
2536
+ title: `Run command: ${String(params.command ?? "")} ${(Array.isArray(params.args) ? params.args : []).join(" ")}`.trim(),
2537
+ kind: "execute",
2538
+ rawInput: {
2539
+ command: params.command,
2540
+ args: params.args,
2541
+ cwd: params.cwd,
2542
+ sessionId: params.sessionId
2543
+ }
2544
+ },
2545
+ callbackOptions
2546
+ );
2547
+ if (authorization !== "allowed") {
2548
+ const isCancelled = authorization === "cancelled";
2549
+ await sender.sendErrorResponse(
2550
+ id,
2551
+ isCancelled ? -32800 : -32602,
2552
+ isCancelled ? "terminal create permission request cancelled or timed out" : "terminal create denied by permission policy"
2553
+ );
2532
2554
  return;
2533
2555
  }
2534
2556
  const createOpts = {
@@ -2581,28 +2603,77 @@ async function handleAcpTerminalRequest(msg, terminalServer, permissionPolicy, s
2581
2603
  await sender.sendErrorResponse(id, -32603, message);
2582
2604
  }
2583
2605
  }
2584
- async function authorizeAcpCallback(permissionPolicy, partial) {
2606
+ async function authorizeAcpCallback(permissionPolicy, partial, callbackOptions) {
2585
2607
  try {
2586
- const outcome = await permissionPolicy({
2587
- toolCall: {
2588
- sessionUpdate: "tool_call_update",
2589
- toolCallId: partial.toolCallId,
2590
- title: partial.title,
2591
- kind: partial.kind,
2592
- status: "pending",
2593
- ...partial.rawInput ? { rawInput: partial.rawInput } : {}
2608
+ const outcome = await runPermissionWithDeadline(
2609
+ permissionPolicy,
2610
+ {
2611
+ toolCall: {
2612
+ sessionUpdate: "tool_call_update",
2613
+ toolCallId: partial.toolCallId,
2614
+ title: partial.title,
2615
+ kind: partial.kind,
2616
+ status: "pending",
2617
+ ...partial.rawInput ? { rawInput: partial.rawInput } : {}
2618
+ },
2619
+ options: [
2620
+ { optionId: "allow", name: "Allow", kind: "allow_once" },
2621
+ { optionId: "reject", name: "Reject", kind: "reject_once" }
2622
+ ]
2594
2623
  },
2595
- options: [
2596
- { optionId: "allow", name: "Allow", kind: "allow_once" },
2597
- { optionId: "reject", name: "Reject", kind: "reject_once" }
2598
- ],
2599
- signal: new AbortController().signal
2600
- });
2601
- return outcome.outcome === "selected" && outcome.optionId !== "reject" && outcome.optionId !== "reject_once" && outcome.optionId !== "reject_always";
2602
- } catch {
2603
- return false;
2624
+ callbackOptions
2625
+ );
2626
+ return outcome.outcome === "selected" && outcome.optionId !== "reject" && outcome.optionId !== "reject_once" && outcome.optionId !== "reject_always" ? "allowed" : "denied";
2627
+ } catch (err) {
2628
+ return isAbortLikeError(err) ? "cancelled" : "denied";
2629
+ }
2630
+ }
2631
+ async function runPermissionWithDeadline(permissionPolicy, call, callbackOptions) {
2632
+ const timeoutMs = resolvePermissionDeadline(callbackOptions.permissionTimeoutMs);
2633
+ const controller = new AbortController();
2634
+ const abort = () => controller.abort();
2635
+ const timer = timeoutMs === null ? null : setTimeout(abort, timeoutMs);
2636
+ let removeAbort;
2637
+ if (callbackOptions.signal) {
2638
+ if (callbackOptions.signal.aborted) {
2639
+ abort();
2640
+ } else {
2641
+ callbackOptions.signal.addEventListener("abort", abort, { once: true });
2642
+ removeAbort = () => callbackOptions.signal?.removeEventListener("abort", abort);
2643
+ }
2644
+ }
2645
+ try {
2646
+ return await Promise.race([
2647
+ permissionPolicy({
2648
+ toolCall: call.toolCall,
2649
+ options: call.options,
2650
+ signal: controller.signal
2651
+ }),
2652
+ rejectOnAbort(controller.signal)
2653
+ ]);
2654
+ } finally {
2655
+ if (timer !== null) clearTimeout(timer);
2656
+ removeAbort?.();
2604
2657
  }
2605
2658
  }
2659
+ function rejectOnAbort(signal) {
2660
+ return new Promise((_, reject) => {
2661
+ const rejectAbort = () => reject(new Error("permission request cancelled or timed out"));
2662
+ if (signal.aborted) {
2663
+ rejectAbort();
2664
+ return;
2665
+ }
2666
+ signal.addEventListener("abort", rejectAbort, { once: true });
2667
+ });
2668
+ }
2669
+ function isAbortLikeError(err) {
2670
+ return err instanceof Error && /cancelled|canceled|timed out|aborted/i.test(err.message);
2671
+ }
2672
+ function resolvePermissionDeadline(value) {
2673
+ if (value === Number.POSITIVE_INFINITY) return null;
2674
+ if (value !== void 0 && Number.isFinite(value) && value > 0) return Math.trunc(value);
2675
+ return DEFAULT_PERMISSION_TIMEOUT_MS;
2676
+ }
2606
2677
 
2607
2678
  // src/client/acp-message-routing.ts
2608
2679
  function isBestEffortAckMethod(method) {
@@ -2618,6 +2689,8 @@ var ACPSession = class _ACPSession {
2618
2689
  timeoutMs;
2619
2690
  opts;
2620
2691
  transportOff = null;
2692
+ callbackAbort = new AbortController();
2693
+ promptCallbackAbort = null;
2621
2694
  state = "init";
2622
2695
  sessionId = null;
2623
2696
  /** Pending outbound requests (initialize, session/new, session/prompt, etc). */
@@ -3132,8 +3205,10 @@ var ACPSession = class _ACPSession {
3132
3205
  this.timeoutMs
3133
3206
  );
3134
3207
  let cancelled = false;
3208
+ this.promptCallbackAbort = new AbortController();
3135
3209
  const onAbort = () => {
3136
3210
  cancelled = true;
3211
+ this.promptCallbackAbort?.abort();
3137
3212
  this.transport.send({
3138
3213
  jsonrpc: "2.0",
3139
3214
  method: "session/cancel",
@@ -3156,6 +3231,8 @@ var ACPSession = class _ACPSession {
3156
3231
  throw new ACPSessionError("prompt_failed", `session/prompt failed: ${msg}`, err);
3157
3232
  } finally {
3158
3233
  signal.removeEventListener("abort", onAbort);
3234
+ this.promptCallbackAbort?.abort();
3235
+ this.promptCallbackAbort = null;
3159
3236
  this.progressHandler = null;
3160
3237
  }
3161
3238
  this.state = "done";
@@ -3222,6 +3299,9 @@ var ACPSession = class _ACPSession {
3222
3299
  if (this.closed) return;
3223
3300
  this.closed = true;
3224
3301
  this.state = "closed";
3302
+ this.callbackAbort.abort();
3303
+ this.promptCallbackAbort?.abort();
3304
+ this.promptCallbackAbort = null;
3225
3305
  this.terminalServer.releaseAll();
3226
3306
  if (this.sessionId && this.agentCapabilities.sessionCapabilities?.close) {
3227
3307
  try {
@@ -3317,6 +3397,11 @@ var ACPSession = class _ACPSession {
3317
3397
  sendErrorResponse: (id, code, message) => this.sendErrorResponse(id, code, message)
3318
3398
  };
3319
3399
  }
3400
+ callbackOptions() {
3401
+ const promptSignal = this.promptCallbackAbort?.signal;
3402
+ const signal = promptSignal ? AbortSignal.any([this.callbackAbort.signal, promptSignal]) : this.callbackAbort.signal;
3403
+ return { signal, permissionTimeoutMs: Number.POSITIVE_INFINITY };
3404
+ }
3320
3405
  handleMessage(msg) {
3321
3406
  if (msg.id !== void 0 && (msg.result !== void 0 || msg.error !== void 0)) {
3322
3407
  const pending = this.pending.get(msg.id);
@@ -3335,25 +3420,35 @@ var ACPSession = class _ACPSession {
3335
3420
  return;
3336
3421
  }
3337
3422
  if (msg.method === "session/request_permission") {
3338
- void handleAcpPermissionRequest(msg, this.permissionPolicy, this.responseSender());
3423
+ handleAcpPermissionRequest(
3424
+ msg,
3425
+ this.permissionPolicy,
3426
+ this.responseSender(),
3427
+ this.callbackOptions()
3428
+ ).catch(() => {
3429
+ });
3339
3430
  return;
3340
3431
  }
3341
3432
  if (msg.method === "fs/read_text_file" || msg.method === "fs/write_text_file") {
3342
- void handleAcpFsRequest(
3433
+ handleAcpFsRequest(
3343
3434
  msg,
3344
3435
  this.fileServer,
3345
3436
  this.permissionPolicy,
3346
- this.responseSender()
3347
- );
3437
+ this.responseSender(),
3438
+ this.callbackOptions()
3439
+ ).catch(() => {
3440
+ });
3348
3441
  return;
3349
3442
  }
3350
3443
  if (msg.method?.startsWith("terminal/")) {
3351
- void handleAcpTerminalRequest(
3444
+ handleAcpTerminalRequest(
3352
3445
  msg,
3353
3446
  this.terminalServer,
3354
3447
  this.permissionPolicy,
3355
- this.responseSender()
3356
- );
3448
+ this.responseSender(),
3449
+ this.callbackOptions()
3450
+ ).catch(() => {
3451
+ });
3357
3452
  return;
3358
3453
  }
3359
3454
  if (isBestEffortAckMethod(msg.method)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/acp",
3
- "version": "0.298.0",
3
+ "version": "0.298.2",
4
4
  "license": "MIT",
5
5
  "description": "ACP (Agent Client Protocol) integration for WrongStack — client + agent support",
6
6
  "keywords": [
@@ -52,10 +52,10 @@
52
52
  ],
53
53
  "dependencies": {
54
54
  "@agentclientprotocol/sdk": "^1.3.0",
55
- "@wrongstack/core": "0.298.0"
55
+ "@wrongstack/core": "0.298.2"
56
56
  },
57
57
  "devDependencies": {
58
- "@types/node": "^26.1.1",
58
+ "@types/node": "^26.1.2",
59
59
  "typescript": "^7.0.2",
60
60
  "vitest": "^4.1.10"
61
61
  },