@wrongstack/acp 0.286.0 → 0.289.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/README.md +27 -23
- 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 +201 -32
- package/dist/client.js.map +3 -3
- package/dist/index.js +291 -51
- 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.js +290 -51
- package/dist/sdk.js.map +3 -3
- package/dist/wrongstack-acp-agent.js +62 -18
- package/dist/wrongstack-acp-agent.js.map +2 -2
- package/package.json +2 -2
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 {
|
|
@@ -1862,7 +2013,7 @@ var ACPSession = class _ACPSession {
|
|
|
1862
2013
|
fs: { readTextFile: true, writeTextFile: true },
|
|
1863
2014
|
terminal: true
|
|
1864
2015
|
},
|
|
1865
|
-
clientInfo: { name: "wrongstack", title: "WrongStack", version: "0.
|
|
2016
|
+
clientInfo: { name: "wrongstack", title: "WrongStack", version: "0.287.0" }
|
|
1866
2017
|
});
|
|
1867
2018
|
if (isJsonRpcError(result)) {
|
|
1868
2019
|
throw new ACPSessionError("init_failed", `initialize failed: ${result.message}`, result);
|
|
@@ -2560,12 +2711,54 @@ var ACPSession = class _ACPSession {
|
|
|
2560
2711
|
return;
|
|
2561
2712
|
}
|
|
2562
2713
|
const policyAbort = new AbortController();
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2714
|
+
try {
|
|
2715
|
+
const outcome = await this.permissionPolicy({
|
|
2716
|
+
toolCall,
|
|
2717
|
+
options,
|
|
2718
|
+
signal: policyAbort.signal
|
|
2719
|
+
});
|
|
2720
|
+
await this.sendResult(id, { outcome });
|
|
2721
|
+
} catch (err) {
|
|
2722
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2723
|
+
await this.sendErrorResponse(id, -32603, `permission policy failed: ${message}`);
|
|
2724
|
+
}
|
|
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
|
+
}
|
|
2569
2762
|
}
|
|
2570
2763
|
async handleFsRequest(msg) {
|
|
2571
2764
|
const id = msg.id;
|
|
@@ -2575,6 +2768,17 @@ var ACPSession = class _ACPSession {
|
|
|
2575
2768
|
await this.sendErrorResponse(id, -32602, "path is required");
|
|
2576
2769
|
return;
|
|
2577
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
|
+
}
|
|
2578
2782
|
try {
|
|
2579
2783
|
if (msg.method === "fs/read_text_file") {
|
|
2580
2784
|
const result = await this.fileServer.readTextFile({
|
|
@@ -2603,6 +2807,15 @@ var ACPSession = class _ACPSession {
|
|
|
2603
2807
|
try {
|
|
2604
2808
|
switch (msg.method) {
|
|
2605
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
|
+
}
|
|
2606
2819
|
const createOpts = {
|
|
2607
2820
|
sessionId: String(params.sessionId ?? ""),
|
|
2608
2821
|
command: String(params.command ?? ""),
|
|
@@ -2894,6 +3107,28 @@ var AGENTS_CATALOG = [
|
|
|
2894
3107
|
},
|
|
2895
3108
|
integration: "experimental",
|
|
2896
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"
|
|
2897
3132
|
}
|
|
2898
3133
|
];
|
|
2899
3134
|
function findAgentDescriptor(id) {
|
|
@@ -3110,7 +3345,11 @@ var REGISTRY_ID_ALIASES = {
|
|
|
3110
3345
|
"claude-code": "claude-acp",
|
|
3111
3346
|
"gemini-cli": "gemini",
|
|
3112
3347
|
"codex-cli": "codex-acp",
|
|
3113
|
-
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"
|
|
3114
3353
|
};
|
|
3115
3354
|
function resolveAcpAgentCommand(id, overrides, live) {
|
|
3116
3355
|
const ov = overrides?.[id];
|
|
@@ -3456,6 +3695,7 @@ function inferVendor(entry) {
|
|
|
3456
3695
|
if (hay.includes("google") || hay.includes("gemini")) return "google";
|
|
3457
3696
|
if (hay.includes("openai") || hay.includes("codex")) return "openai";
|
|
3458
3697
|
if (hay.includes("github") || hay.includes("copilot")) return "github";
|
|
3698
|
+
if (hay.includes("moonshot") || hay.includes("kimi")) return "moonshot";
|
|
3459
3699
|
return "community";
|
|
3460
3700
|
}
|
|
3461
3701
|
async function fetchAcpRegistry(opts = {}) {
|