@wrongstack/acp 0.287.0 → 0.291.0
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/agent/server-agent-turn.d.ts +2 -1
- package/dist/agent/server-agent-turn.d.ts.map +1 -1
- package/dist/agent/session-store.d.ts +2 -1
- package/dist/agent/session-store.d.ts.map +1 -1
- package/dist/agent/wrongstack-acp-agent.d.ts +7 -0
- package/dist/agent/wrongstack-acp-agent.d.ts.map +1 -1
- package/dist/agent.js +62 -18
- package/dist/agent.js.map +2 -2
- package/dist/client/acp-session.d.ts +17 -0
- package/dist/client/acp-session.d.ts.map +1 -1
- package/dist/client/file-server.d.ts +27 -5
- package/dist/client/file-server.d.ts.map +1 -1
- package/dist/client/terminal-server.d.ts +15 -0
- package/dist/client/terminal-server.d.ts.map +1 -1
- package/dist/client.js +189 -25
- package/dist/client.js.map +3 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +279 -45
- package/dist/index.js.map +3 -3
- package/dist/integration/acp-subagent-runner.d.ts.map +1 -1
- package/dist/registry/acp-registry-fetch.d.ts.map +1 -1
- package/dist/registry/agents.catalog.d.ts.map +1 -1
- package/dist/registry/ensemble-registry.d.ts +1 -1
- package/dist/registry/ensemble-registry.d.ts.map +1 -1
- package/dist/sdk.d.ts +5 -37
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +0 -3485
- package/dist/sdk.js.map +4 -4
- package/dist/types/acp-messages.d.ts +1 -39
- package/dist/types/acp-messages.d.ts.map +1 -1
- package/dist/wrongstack-acp-agent.js +62 -18
- package/dist/wrongstack-acp-agent.js.map +2 -2
- package/package.json +5 -5
package/dist/index.d.ts
CHANGED
|
@@ -26,7 +26,7 @@ export { resolveAcpAgentCommand, runOneAcpTask, probeAcpAgent, probeAcpAgents, R
|
|
|
26
26
|
export type { AcpAgentCommandOverrides, AcpLiveCatalog, RunOneAcpTaskOptions, RunOneAcpTaskResult, AcpProbeResult, ProbeAcpAgentsOptions, } from './integration/acp-subagent-runner.js';
|
|
27
27
|
export { AGENTS_CATALOG, findAgentDescriptor } from './registry/agents.catalog.js';
|
|
28
28
|
export { EnsembleRegistry } from './registry/ensemble-registry.js';
|
|
29
|
-
export { ACP_REGISTRY_URL,
|
|
29
|
+
export { ACP_REGISTRY_URL, fetchAcpRegistry, mapRegistryEntry, } from './registry/acp-registry-fetch.js';
|
|
30
30
|
export type { FetchAcpRegistryOptions, FetchAcpRegistryResult, RegistryAgentEntry, } from './registry/acp-registry-fetch.js';
|
|
31
31
|
export type { ACPAgentDescriptor, ACPAgentVendor, ACPIntegration, DetectedAgent, EnsembleRegistryOptions, } from './registry/ensemble-registry.js';
|
|
32
32
|
export { ACPSession, ACPSessionError, textContent, imageContent, audioContent } from './client/acp-session.js';
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EAAC,cAAc,EAAC,MAAM,4BAA4B,CAAC;AAC1D,YAAY,EAAC,oBAAoB,EAAC,MAAM,4BAA4B,CAAC;AACrE,OAAO,EAAC,gBAAgB,EAAC,MAAM,2BAA2B,CAAC;AAC3D,OAAO,EAAC,kBAAkB,EAAC,MAAM,6BAA6B,CAAC;AAC/D,OAAO,EAAC,mBAAmB,EAAC,MAAM,iCAAiC,CAAC;AACpE,YAAY,EAAC,0BAA0B,EAAC,MAAM,iCAAiC,CAAC;AAGhF,OAAO,EAAC,eAAe,EAAC,MAAM,4BAA4B,CAAC;AAC3D,YAAY,EAAC,sBAAsB,EAAE,eAAe,EAAE,kBAAkB,EAAC,MAAM,4BAA4B,CAAC;AAC5G,OAAO,EAAC,wBAAwB,EAAC,MAAM,iCAAiC,CAAC;AACzE,YAAY,EAAC,+BAA+B,EAAC,MAAM,iCAAiC,CAAC;AACrF,OAAO,EAAC,cAAc,EAAC,MAAM,6BAA6B,CAAC;AAC3D,YAAY,EAAC,qBAAqB,EAAC,MAAM,6BAA6B,CAAC;AACvE,OAAO,EAAC,qBAAqB,EAAE,6BAA6B,EAAC,MAAM,sCAAsC,CAAC;AAC1G,YAAY,EAAC,wBAAwB,EAAC,MAAM,sCAAsC,CAAC;AACnF,OAAO,EAAC,kBAAkB,EAAC,MAAM,sCAAsC,CAAC;AACxE,OAAO,EACL,sBAAsB,EACtB,aAAa,EACb,aAAa,EACb,cAAc,EACd,mBAAmB,GACpB,MAAM,sCAAsC,CAAC;AAC9C,YAAY,EACV,wBAAwB,EACxB,cAAc,EACd,oBAAoB,EACpB,mBAAmB,EACnB,cAAc,EACd,qBAAqB,GACtB,MAAM,sCAAsC,CAAC;AAG9C,OAAO,EAAC,cAAc,EAAE,mBAAmB,EAAC,MAAM,8BAA8B,CAAC;AACjF,OAAO,EAAC,gBAAgB,EAAC,MAAM,iCAAiC,CAAC;AAEjE,OAAO,EACL,gBAAgB,EAChB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -130,7 +130,7 @@ var ClientTransport = class {
|
|
|
130
130
|
}
|
|
131
131
|
async start() {
|
|
132
132
|
if (this.child) return;
|
|
133
|
-
const [{ spawn: spawn3 }, { buildChildEnv }, os] = await Promise.all([
|
|
133
|
+
const [{ spawn: spawn3 }, { buildChildEnv: buildChildEnv2 }, os] = await Promise.all([
|
|
134
134
|
import("node:child_process"),
|
|
135
135
|
import("@wrongstack/core"),
|
|
136
136
|
import("node:os")
|
|
@@ -147,7 +147,7 @@ var ClientTransport = class {
|
|
|
147
147
|
const childArgs = this.opts.args ?? [];
|
|
148
148
|
const shim = process.platform === "win32" ? buildWin32CmdShimInvocation(this.opts.command, childArgs) : null;
|
|
149
149
|
this.child = spawn3(shim?.command ?? this.opts.command, shim?.args ?? childArgs, {
|
|
150
|
-
env: { ...
|
|
150
|
+
env: { ...buildChildEnv2(), ...this.opts.env },
|
|
151
151
|
cwd: spawnCwd,
|
|
152
152
|
stdio: ["pipe", "pipe", "pipe"],
|
|
153
153
|
windowsHide: true,
|
|
@@ -1132,7 +1132,21 @@ var WrongStackACPServer = class {
|
|
|
1132
1132
|
async startHttp(port) {
|
|
1133
1133
|
const host = this.options.host ?? "127.0.0.1";
|
|
1134
1134
|
const handler = this.handler;
|
|
1135
|
+
const authToken = this.options.authToken;
|
|
1136
|
+
let httpChain = Promise.resolve();
|
|
1135
1137
|
this.httpServer = createServer(async (req, res) => {
|
|
1138
|
+
if (authToken) {
|
|
1139
|
+
const url = new URL(req.url ?? "/", `http://${host}:${port}`);
|
|
1140
|
+
const queryToken = url.searchParams.get("token");
|
|
1141
|
+
const authHeader = req.headers["authorization"];
|
|
1142
|
+
const bearerToken = Array.isArray(authHeader) ? authHeader[0]?.replace(/^Bearer\s+/i, "") : authHeader?.replace(/^Bearer\s+/i, "");
|
|
1143
|
+
const supplied = queryToken ?? bearerToken ?? "";
|
|
1144
|
+
if (supplied !== authToken) {
|
|
1145
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1146
|
+
res.end(JSON.stringify({ error: { code: -32001, message: "Unauthorized" } }));
|
|
1147
|
+
return;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1136
1150
|
const selfOrigin = `http://${host}:${port}`;
|
|
1137
1151
|
const reqOrigin = Array.isArray(req.headers.origin) ? req.headers.origin[0] : req.headers.origin;
|
|
1138
1152
|
if (reqOrigin && reqOrigin !== selfOrigin) {
|
|
@@ -1142,7 +1156,7 @@ var WrongStackACPServer = class {
|
|
|
1142
1156
|
}
|
|
1143
1157
|
if (reqOrigin) res.setHeader("Access-Control-Allow-Origin", reqOrigin);
|
|
1144
1158
|
res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
|
|
1145
|
-
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id");
|
|
1159
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id, Authorization");
|
|
1146
1160
|
if (req.method === "OPTIONS") {
|
|
1147
1161
|
res.writeHead(204);
|
|
1148
1162
|
res.end();
|
|
@@ -1153,10 +1167,23 @@ var WrongStackACPServer = class {
|
|
|
1153
1167
|
res.end(JSON.stringify({ error: "method not allowed" }));
|
|
1154
1168
|
return;
|
|
1155
1169
|
}
|
|
1170
|
+
const MAX_HTTP_BODY = 10 * 1024 * 1024;
|
|
1156
1171
|
let body = "";
|
|
1172
|
+
let bodyBytes = 0;
|
|
1173
|
+
let tooLarge = false;
|
|
1157
1174
|
for await (const chunk of req) {
|
|
1175
|
+
bodyBytes += chunk.length;
|
|
1176
|
+
if (bodyBytes > MAX_HTTP_BODY) {
|
|
1177
|
+
tooLarge = true;
|
|
1178
|
+
break;
|
|
1179
|
+
}
|
|
1158
1180
|
body += chunk;
|
|
1159
1181
|
}
|
|
1182
|
+
if (tooLarge) {
|
|
1183
|
+
res.writeHead(413, { "Content-Type": "application/json" });
|
|
1184
|
+
res.end(JSON.stringify({ error: { code: -32700, message: "Request body too large" } }));
|
|
1185
|
+
return;
|
|
1186
|
+
}
|
|
1160
1187
|
let msg;
|
|
1161
1188
|
try {
|
|
1162
1189
|
msg = JSON.parse(body);
|
|
@@ -1165,26 +1192,43 @@ var WrongStackACPServer = class {
|
|
|
1165
1192
|
res.end(JSON.stringify({ error: { code: -32700, message: "Parse error" } }));
|
|
1166
1193
|
return;
|
|
1167
1194
|
}
|
|
1168
|
-
const
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
response = m;
|
|
1174
|
-
} else if (m.method === "session/update") {
|
|
1175
|
-
notifications.push(m.params);
|
|
1176
|
-
} else {
|
|
1177
|
-
notifications.push(m);
|
|
1195
|
+
const isNotification = typeof msg === "object" && msg !== null && msg.id === void 0 && typeof msg.method === "string";
|
|
1196
|
+
if (isNotification) {
|
|
1197
|
+
try {
|
|
1198
|
+
await handler.handleMessage(msg);
|
|
1199
|
+
} catch {
|
|
1178
1200
|
}
|
|
1179
|
-
|
|
1201
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1202
|
+
res.end(JSON.stringify({ notifications: [] }));
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
const requestPromise = httpChain.then(async () => {
|
|
1206
|
+
const notifications = [];
|
|
1207
|
+
let response = null;
|
|
1208
|
+
const originalSend = this.transport.send.bind(this.transport);
|
|
1209
|
+
this.transport.send = async (m) => {
|
|
1210
|
+
if (m.id !== void 0 && (m.result !== void 0 || m.error !== void 0)) {
|
|
1211
|
+
response = m;
|
|
1212
|
+
} else if (m.method === "session/update") {
|
|
1213
|
+
notifications.push(m.params);
|
|
1214
|
+
} else {
|
|
1215
|
+
notifications.push(m);
|
|
1216
|
+
}
|
|
1217
|
+
};
|
|
1218
|
+
try {
|
|
1219
|
+
await handler.handleMessage(msg);
|
|
1220
|
+
} finally {
|
|
1221
|
+
this.transport.send = originalSend;
|
|
1222
|
+
}
|
|
1223
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1224
|
+
const responseBody = response !== null ? { ...response, notifications } : { notifications };
|
|
1225
|
+
res.end(JSON.stringify(responseBody));
|
|
1226
|
+
});
|
|
1227
|
+
httpChain = requestPromise.catch(() => void 0);
|
|
1180
1228
|
try {
|
|
1181
|
-
await
|
|
1182
|
-
}
|
|
1183
|
-
this.transport.send = originalSend;
|
|
1229
|
+
await requestPromise;
|
|
1230
|
+
} catch {
|
|
1184
1231
|
}
|
|
1185
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1186
|
-
const responseBody = response !== null ? { ...response, notifications } : { notifications };
|
|
1187
|
-
res.end(JSON.stringify(responseBody));
|
|
1188
1232
|
});
|
|
1189
1233
|
return new Promise((resolve3) => {
|
|
1190
1234
|
this.httpServer.listen(port, host, () => {
|
|
@@ -1396,8 +1440,12 @@ var ToolTranslator = class {
|
|
|
1396
1440
|
};
|
|
1397
1441
|
|
|
1398
1442
|
// src/client/file-server.ts
|
|
1443
|
+
import { randomBytes } from "node:crypto";
|
|
1444
|
+
import { realpathSync } from "node:fs";
|
|
1399
1445
|
import * as fsp from "node:fs/promises";
|
|
1400
1446
|
import * as path from "node:path";
|
|
1447
|
+
var DEFAULT_MAX_READ_BYTES = 5 * 1024 * 1024;
|
|
1448
|
+
var DEFAULT_MAX_WRITE_BYTES = 5 * 1024 * 1024;
|
|
1401
1449
|
var FsError = class extends Error {
|
|
1402
1450
|
code;
|
|
1403
1451
|
path;
|
|
@@ -1410,23 +1458,40 @@ var FsError = class extends Error {
|
|
|
1410
1458
|
};
|
|
1411
1459
|
var FileServer = class {
|
|
1412
1460
|
root;
|
|
1461
|
+
realRoot;
|
|
1413
1462
|
timeoutMs;
|
|
1463
|
+
maxReadBytes;
|
|
1464
|
+
maxWriteBytes;
|
|
1414
1465
|
constructor(opts) {
|
|
1415
1466
|
this.root = path.resolve(opts.projectRoot);
|
|
1467
|
+
this.realRoot = safeRealpathSync(this.root);
|
|
1416
1468
|
this.timeoutMs = opts.timeoutMs ?? 3e4;
|
|
1469
|
+
this.maxReadBytes = opts.maxReadBytes ?? DEFAULT_MAX_READ_BYTES;
|
|
1470
|
+
this.maxWriteBytes = opts.maxWriteBytes ?? DEFAULT_MAX_WRITE_BYTES;
|
|
1417
1471
|
}
|
|
1418
1472
|
/** Read a text file. Returns the content as a string. */
|
|
1419
1473
|
async readTextFile(params) {
|
|
1420
|
-
const safe = this.resolveInside(params.path);
|
|
1474
|
+
const safe = await this.resolveInside(params.path);
|
|
1421
1475
|
const controller = new AbortController();
|
|
1422
1476
|
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
1423
1477
|
try {
|
|
1478
|
+
const stat2 = await fsp.stat(safe).catch((err) => {
|
|
1479
|
+
throw mapFsError(err, safe);
|
|
1480
|
+
});
|
|
1481
|
+
if (stat2.size > this.maxReadBytes) {
|
|
1482
|
+
throw new FsError(
|
|
1483
|
+
"TOO_LARGE",
|
|
1484
|
+
safe,
|
|
1485
|
+
`file is ${stat2.size} bytes, max read is ${this.maxReadBytes} bytes`
|
|
1486
|
+
);
|
|
1487
|
+
}
|
|
1424
1488
|
const content = await fsp.readFile(safe, {
|
|
1425
1489
|
encoding: "utf8",
|
|
1426
1490
|
signal: controller.signal
|
|
1427
1491
|
});
|
|
1428
1492
|
return { content };
|
|
1429
1493
|
} catch (err) {
|
|
1494
|
+
if (err instanceof FsError) throw err;
|
|
1430
1495
|
if (controller.signal.aborted) {
|
|
1431
1496
|
throw new FsError("TIMEOUT", safe, `readTextFile timed out after ${this.timeoutMs}ms`);
|
|
1432
1497
|
}
|
|
@@ -1437,17 +1502,31 @@ var FileServer = class {
|
|
|
1437
1502
|
}
|
|
1438
1503
|
/** Write a text file. Atomic via write-then-rename. */
|
|
1439
1504
|
async writeTextFile(params) {
|
|
1440
|
-
const
|
|
1505
|
+
const byteLength = Buffer.byteLength(params.content, "utf8");
|
|
1506
|
+
if (byteLength > this.maxWriteBytes) {
|
|
1507
|
+
throw new FsError(
|
|
1508
|
+
"TOO_LARGE",
|
|
1509
|
+
params.path,
|
|
1510
|
+
`content is ${byteLength} bytes, max write is ${this.maxWriteBytes} bytes`
|
|
1511
|
+
);
|
|
1512
|
+
}
|
|
1513
|
+
const safe = await this.resolveInside(params.path);
|
|
1441
1514
|
const controller = new AbortController();
|
|
1442
1515
|
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
1443
|
-
const tmp = `${safe}.${
|
|
1516
|
+
const tmp = `${safe}.${randomBytes(6).toString("hex")}.tmp`;
|
|
1444
1517
|
try {
|
|
1445
1518
|
await fsp.writeFile(tmp, params.content, {
|
|
1446
1519
|
encoding: "utf8",
|
|
1447
1520
|
signal: controller.signal
|
|
1448
1521
|
});
|
|
1522
|
+
await this.assertRealInside(tmp);
|
|
1523
|
+
await this.assertRealInside(path.dirname(safe));
|
|
1449
1524
|
await fsp.rename(tmp, safe);
|
|
1450
1525
|
} catch (err) {
|
|
1526
|
+
if (err instanceof FsError) {
|
|
1527
|
+
await fsp.unlink(tmp).catch(() => void 0);
|
|
1528
|
+
throw err;
|
|
1529
|
+
}
|
|
1451
1530
|
try {
|
|
1452
1531
|
await fsp.unlink(tmp);
|
|
1453
1532
|
} catch {
|
|
@@ -1461,12 +1540,14 @@ var FileServer = class {
|
|
|
1461
1540
|
}
|
|
1462
1541
|
}
|
|
1463
1542
|
/**
|
|
1464
|
-
* Resolve a path
|
|
1465
|
-
*
|
|
1466
|
-
*
|
|
1467
|
-
*
|
|
1543
|
+
* Resolve a path and verify it is inside the project root by realpath.
|
|
1544
|
+
* Rejects with `FsError` if the textual path, the resolved path, or the
|
|
1545
|
+
* real (symlink-resolved) path escapes the project root.
|
|
1546
|
+
*
|
|
1547
|
+
* For files that don't exist yet (e.g. a write to a new file), the
|
|
1548
|
+
* nearest existing ancestor directory is realpath-checked instead.
|
|
1468
1549
|
*/
|
|
1469
|
-
resolveInside(p) {
|
|
1550
|
+
async resolveInside(p) {
|
|
1470
1551
|
if (typeof p !== "string" || p.length === 0) {
|
|
1471
1552
|
throw new FsError("INVALID_PATH", p, "path is empty or not a string");
|
|
1472
1553
|
}
|
|
@@ -1478,8 +1559,38 @@ var FileServer = class {
|
|
|
1478
1559
|
if (resolved !== this.root && !resolved.startsWith(rootWithSep)) {
|
|
1479
1560
|
throw new FsError("OUTSIDE_ROOT", resolved, "path is outside the project root");
|
|
1480
1561
|
}
|
|
1562
|
+
await this.assertRealInside(resolved);
|
|
1481
1563
|
return resolved;
|
|
1482
1564
|
}
|
|
1565
|
+
/**
|
|
1566
|
+
* Resolve `resolvedPath` through `fs.realpath` and verify the result is
|
|
1567
|
+
* inside `realRoot`. For non-existent paths (new files), walk up to the
|
|
1568
|
+
* nearest existing ancestor and check that instead.
|
|
1569
|
+
*/
|
|
1570
|
+
async assertRealInside(resolvedPath) {
|
|
1571
|
+
let probe = resolvedPath;
|
|
1572
|
+
for (; ; ) {
|
|
1573
|
+
let real;
|
|
1574
|
+
try {
|
|
1575
|
+
real = await fsp.realpath(probe);
|
|
1576
|
+
} catch (err) {
|
|
1577
|
+
const code = err.code;
|
|
1578
|
+
if (code === "ENOENT") {
|
|
1579
|
+
const parent = path.dirname(probe);
|
|
1580
|
+
if (parent === probe) return;
|
|
1581
|
+
probe = parent;
|
|
1582
|
+
continue;
|
|
1583
|
+
}
|
|
1584
|
+
throw mapFsError(err, resolvedPath);
|
|
1585
|
+
}
|
|
1586
|
+
if (real === this.realRoot || real.startsWith(this.realRoot + path.sep)) return;
|
|
1587
|
+
throw new FsError(
|
|
1588
|
+
"OUTSIDE_ROOT",
|
|
1589
|
+
resolvedPath,
|
|
1590
|
+
"path resolves through a symlink outside the project root"
|
|
1591
|
+
);
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1483
1594
|
};
|
|
1484
1595
|
function mapFsError(err, p) {
|
|
1485
1596
|
const code = err?.code;
|
|
@@ -1490,12 +1601,12 @@ function mapFsError(err, p) {
|
|
|
1490
1601
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1491
1602
|
return new FsError("INVALID_PATH", p, msg);
|
|
1492
1603
|
}
|
|
1493
|
-
function
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1604
|
+
function safeRealpathSync(p) {
|
|
1605
|
+
try {
|
|
1606
|
+
return realpathSync(p);
|
|
1607
|
+
} catch {
|
|
1608
|
+
return p;
|
|
1497
1609
|
}
|
|
1498
|
-
return out;
|
|
1499
1610
|
}
|
|
1500
1611
|
|
|
1501
1612
|
// src/client/permission.ts
|
|
@@ -1544,17 +1655,21 @@ function makePermissionPolicy(decide) {
|
|
|
1544
1655
|
|
|
1545
1656
|
// src/client/terminal-server.ts
|
|
1546
1657
|
import { spawn } from "node:child_process";
|
|
1658
|
+
import { realpathSync as realpathSync2 } from "node:fs";
|
|
1547
1659
|
import * as path2 from "node:path";
|
|
1660
|
+
import { buildChildEnv } from "@wrongstack/core/utils";
|
|
1548
1661
|
var TerminalServer = class {
|
|
1549
1662
|
terminals = /* @__PURE__ */ new Map();
|
|
1550
1663
|
projectRoot;
|
|
1551
1664
|
commandTimeoutMs;
|
|
1552
1665
|
outputByteLimit;
|
|
1666
|
+
maxOutputByteLimit;
|
|
1553
1667
|
nextId = 1;
|
|
1554
1668
|
constructor(opts) {
|
|
1555
1669
|
this.projectRoot = path2.resolve(opts.projectRoot);
|
|
1556
1670
|
this.commandTimeoutMs = opts.commandTimeoutMs ?? 5 * 6e4;
|
|
1557
1671
|
this.outputByteLimit = opts.outputByteLimit ?? 1024 * 1024;
|
|
1672
|
+
this.maxOutputByteLimit = opts.maxOutputByteLimit ?? 16 * 1024 * 1024;
|
|
1558
1673
|
if (opts.signal) {
|
|
1559
1674
|
opts.signal.addEventListener("abort", () => this.releaseAll());
|
|
1560
1675
|
}
|
|
@@ -1608,12 +1723,15 @@ var TerminalServer = class {
|
|
|
1608
1723
|
state.exitStatus = exitStatus;
|
|
1609
1724
|
state.output += `[spawn error] ${err.message}
|
|
1610
1725
|
`;
|
|
1611
|
-
state.retainedBytes
|
|
1726
|
+
state.retainedBytes = Buffer.byteLength(state.output, "utf8");
|
|
1612
1727
|
resolve3(exitStatus);
|
|
1613
1728
|
});
|
|
1614
1729
|
})
|
|
1615
1730
|
};
|
|
1616
|
-
const perCallByteLimit =
|
|
1731
|
+
const perCallByteLimit = Math.min(
|
|
1732
|
+
Math.max(1, this.clampFiniteInt(params.outputByteLimit, this.outputByteLimit)),
|
|
1733
|
+
this.maxOutputByteLimit
|
|
1734
|
+
);
|
|
1617
1735
|
proc.stdout?.setEncoding("utf8");
|
|
1618
1736
|
proc.stderr?.setEncoding("utf8");
|
|
1619
1737
|
const onData = (chunk) => {
|
|
@@ -1693,24 +1811,57 @@ var TerminalServer = class {
|
|
|
1693
1811
|
if (resolved !== this.projectRoot && !resolved.startsWith(rootWithSep)) {
|
|
1694
1812
|
return this.projectRoot;
|
|
1695
1813
|
}
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
if (env.PATHEXT !== void 0 && env.PATHEXT_CASE === void 0) {
|
|
1703
|
-
env.PATHEXT_CASE = env.PATHEXT;
|
|
1814
|
+
try {
|
|
1815
|
+
const realRoot = realpathSync2(this.projectRoot);
|
|
1816
|
+
const realCwd = realpathSync2(resolved);
|
|
1817
|
+
const realRootWithSep = realRoot.endsWith(path2.sep) ? realRoot : realRoot + path2.sep;
|
|
1818
|
+
if (realCwd !== realRoot && !realCwd.startsWith(realRootWithSep)) {
|
|
1819
|
+
return realRoot;
|
|
1704
1820
|
}
|
|
1821
|
+
return realCwd;
|
|
1822
|
+
} catch {
|
|
1823
|
+
return this.projectRoot;
|
|
1705
1824
|
}
|
|
1825
|
+
}
|
|
1826
|
+
buildEnv(agentEnv) {
|
|
1827
|
+
const env = buildChildEnv();
|
|
1706
1828
|
if (agentEnv) {
|
|
1707
1829
|
for (const { name, value } of agentEnv) {
|
|
1830
|
+
const upper = name.toUpperCase();
|
|
1831
|
+
if (DENIED_AGENT_ENV_KEYS.has(upper)) continue;
|
|
1708
1832
|
env[name] = value;
|
|
1709
1833
|
}
|
|
1710
1834
|
}
|
|
1711
1835
|
return env;
|
|
1712
1836
|
}
|
|
1837
|
+
/**
|
|
1838
|
+
* Clamp an agent-supplied numeric to a finite positive safe integer, falling
|
|
1839
|
+
* back to `defaultValue` for undefined/NaN/non-finite values. Prevents
|
|
1840
|
+
* negative, NaN, or Infinity values from disabling output caps or causing
|
|
1841
|
+
* unbounded memory growth.
|
|
1842
|
+
*/
|
|
1843
|
+
clampFiniteInt(value, defaultValue) {
|
|
1844
|
+
if (value === void 0 || !Number.isFinite(value) || value < 1) {
|
|
1845
|
+
return defaultValue;
|
|
1846
|
+
}
|
|
1847
|
+
return Math.trunc(value);
|
|
1848
|
+
}
|
|
1713
1849
|
};
|
|
1850
|
+
var DENIED_AGENT_ENV_KEYS = /* @__PURE__ */ new Set([
|
|
1851
|
+
"NODE_OPTIONS",
|
|
1852
|
+
"LD_PRELOAD",
|
|
1853
|
+
"LD_LIBRARY_PATH",
|
|
1854
|
+
"DYLD_INSERT_LIBRARIES",
|
|
1855
|
+
"DYLD_LIBRARY_PATH",
|
|
1856
|
+
"DYLD_FALLBACK_LIBRARY_PATH",
|
|
1857
|
+
"PATH",
|
|
1858
|
+
"PYTHONPATH",
|
|
1859
|
+
"PYTHONSTARTUP",
|
|
1860
|
+
"PERL5OPT",
|
|
1861
|
+
"PERLLIB",
|
|
1862
|
+
"RUBYOPT",
|
|
1863
|
+
"RUBYLIB"
|
|
1864
|
+
]);
|
|
1714
1865
|
|
|
1715
1866
|
// src/client/acp-session.ts
|
|
1716
1867
|
var ACPSessionError = class extends Error {
|
|
@@ -2572,6 +2723,43 @@ var ACPSession = class _ACPSession {
|
|
|
2572
2723
|
await this.sendErrorResponse(id, -32603, `permission policy failed: ${message}`);
|
|
2573
2724
|
}
|
|
2574
2725
|
}
|
|
2726
|
+
/**
|
|
2727
|
+
* Enforce authorization at privileged callback sinks (fs/write,
|
|
2728
|
+
* terminal/create). Unlike `handlePermissionRequest` which responds to
|
|
2729
|
+
* agent-initiated `session/request_permission` messages, this method is
|
|
2730
|
+
* called by the handler BEFORE dispatching to FileServer/TerminalServer,
|
|
2731
|
+
* closing the gap where the agent simply skips the voluntary permission
|
|
2732
|
+
* request and sends the privileged callback directly.
|
|
2733
|
+
*
|
|
2734
|
+
* Uses the session's permission policy. The default policy
|
|
2735
|
+
* (`defaultPermissionPolicy`) auto-approves everything — this is correct
|
|
2736
|
+
* for trusted local agents (CLI `acp spawn`, Director fan-out). For
|
|
2737
|
+
* untrusted/remote agents, the host should inject
|
|
2738
|
+
* `readOnlyPermissionPolicy` or an interactive policy.
|
|
2739
|
+
*
|
|
2740
|
+
* Returns true if the callback is authorized, false if denied.
|
|
2741
|
+
*/
|
|
2742
|
+
async authorizeCallback(partial) {
|
|
2743
|
+
try {
|
|
2744
|
+
const outcome = await this.permissionPolicy({
|
|
2745
|
+
toolCall: {
|
|
2746
|
+
sessionUpdate: "tool_call_update",
|
|
2747
|
+
toolCallId: partial.toolCallId,
|
|
2748
|
+
title: partial.title,
|
|
2749
|
+
kind: partial.kind,
|
|
2750
|
+
status: "pending"
|
|
2751
|
+
},
|
|
2752
|
+
options: [
|
|
2753
|
+
{ optionId: "allow", name: "Allow", kind: "allow_once" },
|
|
2754
|
+
{ optionId: "reject", name: "Reject", kind: "reject_once" }
|
|
2755
|
+
],
|
|
2756
|
+
signal: new AbortController().signal
|
|
2757
|
+
});
|
|
2758
|
+
return outcome.outcome === "selected" && outcome.optionId !== "reject" && outcome.optionId !== "reject_once" && outcome.optionId !== "reject_always";
|
|
2759
|
+
} catch {
|
|
2760
|
+
return false;
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2575
2763
|
async handleFsRequest(msg) {
|
|
2576
2764
|
const id = msg.id;
|
|
2577
2765
|
if (id === void 0) return;
|
|
@@ -2580,6 +2768,17 @@ var ACPSession = class _ACPSession {
|
|
|
2580
2768
|
await this.sendErrorResponse(id, -32602, "path is required");
|
|
2581
2769
|
return;
|
|
2582
2770
|
}
|
|
2771
|
+
if (msg.method === "fs/write_text_file") {
|
|
2772
|
+
const allowed = await this.authorizeCallback({
|
|
2773
|
+
toolCallId: `acp-fs-write-${id}`,
|
|
2774
|
+
title: `Write file: ${params.path}`,
|
|
2775
|
+
kind: "edit"
|
|
2776
|
+
});
|
|
2777
|
+
if (!allowed) {
|
|
2778
|
+
await this.sendErrorResponse(id, -32602, "filesystem write denied by permission policy");
|
|
2779
|
+
return;
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2583
2782
|
try {
|
|
2584
2783
|
if (msg.method === "fs/read_text_file") {
|
|
2585
2784
|
const result = await this.fileServer.readTextFile({
|
|
@@ -2608,6 +2807,15 @@ var ACPSession = class _ACPSession {
|
|
|
2608
2807
|
try {
|
|
2609
2808
|
switch (msg.method) {
|
|
2610
2809
|
case "terminal/create": {
|
|
2810
|
+
const allowed = await this.authorizeCallback({
|
|
2811
|
+
toolCallId: `acp-terminal-create-${id}`,
|
|
2812
|
+
title: `Run command: ${String(params.command ?? "")} ${(Array.isArray(params.args) ? params.args : []).join(" ")}`.trim(),
|
|
2813
|
+
kind: "execute"
|
|
2814
|
+
});
|
|
2815
|
+
if (!allowed) {
|
|
2816
|
+
await this.sendErrorResponse(id, -32602, "terminal create denied by permission policy");
|
|
2817
|
+
return;
|
|
2818
|
+
}
|
|
2611
2819
|
const createOpts = {
|
|
2612
2820
|
sessionId: String(params.sessionId ?? ""),
|
|
2613
2821
|
command: String(params.command ?? ""),
|
|
@@ -2899,6 +3107,28 @@ var AGENTS_CATALOG = [
|
|
|
2899
3107
|
},
|
|
2900
3108
|
integration: "experimental",
|
|
2901
3109
|
docs: "https://cursor.com"
|
|
3110
|
+
},
|
|
3111
|
+
// ── Moonshot AI (Kimi) ─────────────────────────────────────────────
|
|
3112
|
+
{
|
|
3113
|
+
id: "kimi",
|
|
3114
|
+
displayName: "Kimi Code CLI",
|
|
3115
|
+
vendor: "moonshot",
|
|
3116
|
+
probe: { command: "kimi", args: ["--version"] },
|
|
3117
|
+
// Kimi Code CLI speaks ACP behind `kimi acp`. The user must complete
|
|
3118
|
+
// terminal login (`kimi` → `/login`) before launching `kimi acp`;
|
|
3119
|
+
// otherwise session creation fails with `Authentication required`.
|
|
3120
|
+
// The adapter reuses the CLI's existing auth state — WrongStack does
|
|
3121
|
+
// NOT capture or replay the Kimi OAuth tokens.
|
|
3122
|
+
// Docs: https://www.kimi.com/code/docs/en/kimi-code-cli/reference/kimi-acp.html
|
|
3123
|
+
acp: { command: "kimi", args: ["acp"] },
|
|
3124
|
+
supports: {
|
|
3125
|
+
loadSession: true,
|
|
3126
|
+
promptImages: true,
|
|
3127
|
+
terminal: true,
|
|
3128
|
+
fs: true
|
|
3129
|
+
},
|
|
3130
|
+
integration: "native",
|
|
3131
|
+
docs: "https://www.kimi.com/code/docs/en/kimi-code-cli/guides/ides.html"
|
|
2902
3132
|
}
|
|
2903
3133
|
];
|
|
2904
3134
|
function findAgentDescriptor(id) {
|
|
@@ -3115,7 +3345,11 @@ var REGISTRY_ID_ALIASES = {
|
|
|
3115
3345
|
"claude-code": "claude-acp",
|
|
3116
3346
|
"gemini-cli": "gemini",
|
|
3117
3347
|
"codex-cli": "codex-acp",
|
|
3118
|
-
copilot: "github-copilot-cli"
|
|
3348
|
+
copilot: "github-copilot-cli",
|
|
3349
|
+
// Kimi's live registry id is `kimi` — same as our catalog id, so the
|
|
3350
|
+
// alias is identity. Listed explicitly so `resolveAcpAgentCommand`
|
|
3351
|
+
// finds the live entry when the registry is synced.
|
|
3352
|
+
kimi: "kimi"
|
|
3119
3353
|
};
|
|
3120
3354
|
function resolveAcpAgentCommand(id, overrides, live) {
|
|
3121
3355
|
const ov = overrides?.[id];
|
|
@@ -3461,6 +3695,7 @@ function inferVendor(entry) {
|
|
|
3461
3695
|
if (hay.includes("google") || hay.includes("gemini")) return "google";
|
|
3462
3696
|
if (hay.includes("openai") || hay.includes("codex")) return "openai";
|
|
3463
3697
|
if (hay.includes("github") || hay.includes("copilot")) return "github";
|
|
3698
|
+
if (hay.includes("moonshot") || hay.includes("kimi")) return "moonshot";
|
|
3464
3699
|
return "community";
|
|
3465
3700
|
}
|
|
3466
3701
|
async function fetchAcpRegistry(opts = {}) {
|
|
@@ -3940,7 +4175,6 @@ export {
|
|
|
3940
4175
|
WebSocketClientTransport,
|
|
3941
4176
|
WrongStackACPServer,
|
|
3942
4177
|
audioContent,
|
|
3943
|
-
currentPlatformKey,
|
|
3944
4178
|
defaultEnsembleCmdResolver,
|
|
3945
4179
|
defaultPermissionPolicy,
|
|
3946
4180
|
fetchAcpRegistry,
|