@wrongstack/acp 0.298.1 → 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.
- package/dist/client/acp-session-callbacks.d.ts +18 -3
- package/dist/client/acp-session.d.ts +3 -0
- package/dist/client.js +152 -57
- package/dist/index.js +152 -57
- package/package.json +2 -2
|
@@ -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
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
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
|
-
|
|
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
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
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
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
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
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
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
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
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
|
-
|
|
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
|
|
2481
|
-
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
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
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
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
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
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
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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,7 +52,7 @@
|
|
|
52
52
|
],
|
|
53
53
|
"dependencies": {
|
|
54
54
|
"@agentclientprotocol/sdk": "^1.3.0",
|
|
55
|
-
"@wrongstack/core": "0.298.
|
|
55
|
+
"@wrongstack/core": "0.298.2"
|
|
56
56
|
},
|
|
57
57
|
"devDependencies": {
|
|
58
58
|
"@types/node": "^26.1.2",
|