@hachej/boring-agent 0.1.12 → 0.1.14
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 +242 -42
- package/dist/DebugDrawer-MCYZ4AWZ.js +6 -0
- package/dist/chunk-F3CE5CNW.js +38 -0
- package/dist/chunk-MMJA3QON.js +295 -0
- package/dist/front/index.d.ts +48 -76
- package/dist/front/index.js +2396 -1795
- package/dist/front/styles.css +2242 -1267
- package/dist/{sandbox-handle-store-DCNEJJ6b.d.ts → sandbox-handle-store-hK76cTjn.d.ts} +2 -0
- package/dist/server/index.d.ts +35 -3
- package/dist/server/index.js +2079 -1577
- package/dist/shared/index.d.ts +11 -7
- package/dist/shared/index.js +10 -1
- package/dist/{harness-DT3ZzdAN.d.ts → tool-ui-DSmWuqGe.d.ts} +33 -4
- package/package.json +4 -4
package/dist/server/index.js
CHANGED
|
@@ -1489,286 +1489,827 @@ function createNodeWorkspace(root) {
|
|
|
1489
1489
|
};
|
|
1490
1490
|
}
|
|
1491
1491
|
|
|
1492
|
-
// src/server/
|
|
1493
|
-
import {
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
var
|
|
1501
|
-
var
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
maxBuffer: 1024 * 1024 * 20,
|
|
1518
|
-
timeout: 5 * 60 * 1e3
|
|
1519
|
-
});
|
|
1492
|
+
// src/server/http/routes/file.ts
|
|
1493
|
+
import { dirname as dirname4, extname, relative as relative4 } from "path/posix";
|
|
1494
|
+
|
|
1495
|
+
// src/server/http/middleware.ts
|
|
1496
|
+
var ERROR_CODE_AUTH_REQUIRED = "auth_required";
|
|
1497
|
+
var ERROR_CODE_AUTH_INVALID = "auth_invalid";
|
|
1498
|
+
var ERROR_CODE_INVALID_PATH = "invalid_path";
|
|
1499
|
+
var ERROR_CODE_VALIDATION_ERROR = "validation_error";
|
|
1500
|
+
var ERROR_CODE_PATH_REJECTED = "path_rejected";
|
|
1501
|
+
var ERROR_CODE_NOT_FOUND = "not_found";
|
|
1502
|
+
var ERROR_CODE_ALREADY_EXISTS = "already_exists";
|
|
1503
|
+
var ERROR_CODE_CONFLICT = "conflict";
|
|
1504
|
+
var ERROR_CODE_RANGE_NOT_SATISFIABLE = "range_not_satisfiable";
|
|
1505
|
+
var ERROR_CODE_INTERNAL = "internal";
|
|
1506
|
+
var ERROR_CODE_NOT_IMPLEMENTED = "not_implemented";
|
|
1507
|
+
var DEV_MODE_WARNING = "No auth token set \u2014 running in dev mode";
|
|
1508
|
+
var DEFAULT_WORKSPACE_ID = "default";
|
|
1509
|
+
function errorPayload(code, message, field) {
|
|
1510
|
+
return {
|
|
1511
|
+
error: {
|
|
1512
|
+
code,
|
|
1513
|
+
message,
|
|
1514
|
+
...field ? { field } : {}
|
|
1515
|
+
}
|
|
1516
|
+
};
|
|
1520
1517
|
}
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
}
|
|
1526
|
-
return false;
|
|
1527
|
-
}
|
|
1518
|
+
function ensureWorkspaceContext(request, workspaceId, authenticated) {
|
|
1519
|
+
request.workspaceContext = {
|
|
1520
|
+
workspaceId,
|
|
1521
|
+
authenticated
|
|
1522
|
+
};
|
|
1528
1523
|
}
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
const
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1524
|
+
function createAuthMiddleware(opts = {}) {
|
|
1525
|
+
let warnedDevMode = false;
|
|
1526
|
+
return async function authMiddleware(request, reply) {
|
|
1527
|
+
const workspaceId = opts.workspaceId ?? DEFAULT_WORKSPACE_ID;
|
|
1528
|
+
const authToken = opts.authToken?.trim() || void 0;
|
|
1529
|
+
ensureWorkspaceContext(request, workspaceId, false);
|
|
1530
|
+
if (opts.publicPaths?.includes(request.url.split("?")[0])) {
|
|
1531
|
+
return;
|
|
1537
1532
|
}
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
async function fingerprint(contributions) {
|
|
1546
|
-
const hash = createHash3("sha256");
|
|
1547
|
-
hash.update(JSON.stringify({ v: PROVISIONING_VERSION }));
|
|
1548
|
-
for (const { id, provisioning } of contributions) {
|
|
1549
|
-
hash.update(`plugin:${id}
|
|
1550
|
-
`);
|
|
1551
|
-
for (const template of provisioning.templateDirs ?? []) {
|
|
1552
|
-
const templatePath = toPath(template.path);
|
|
1553
|
-
hash.update(`template:${template.id}:${template.target ?? ""}:${templatePath}
|
|
1554
|
-
`);
|
|
1555
|
-
if (await exists(templatePath)) await hashPath(templatePath, hash);
|
|
1533
|
+
if (!authToken) {
|
|
1534
|
+
if (!warnedDevMode) {
|
|
1535
|
+
warnedDevMode = true;
|
|
1536
|
+
request.log.warn(DEV_MODE_WARNING);
|
|
1537
|
+
opts.onDevModeWarning?.(DEV_MODE_WARNING);
|
|
1538
|
+
}
|
|
1539
|
+
return;
|
|
1556
1540
|
}
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1541
|
+
const header = request.headers.authorization;
|
|
1542
|
+
if (typeof header !== "string" || !header.startsWith("Bearer ")) {
|
|
1543
|
+
reply.code(401).send(
|
|
1544
|
+
errorPayload(
|
|
1545
|
+
ERROR_CODE_AUTH_REQUIRED,
|
|
1546
|
+
"Missing Bearer token"
|
|
1547
|
+
)
|
|
1548
|
+
);
|
|
1549
|
+
return;
|
|
1564
1550
|
}
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
const env = {};
|
|
1570
|
-
for (const { provisioning } of contributions) {
|
|
1571
|
-
for (const spec of provisioning.python ?? []) {
|
|
1572
|
-
for (const [key, value] of Object.entries(spec.env ?? {})) {
|
|
1573
|
-
env[key] = toPath(value);
|
|
1574
|
-
}
|
|
1551
|
+
const candidateToken = header.slice("Bearer ".length);
|
|
1552
|
+
if (candidateToken !== authToken) {
|
|
1553
|
+
reply.code(403).send(errorPayload(ERROR_CODE_AUTH_INVALID, "Invalid token"));
|
|
1554
|
+
return;
|
|
1575
1555
|
}
|
|
1576
|
-
|
|
1577
|
-
|
|
1556
|
+
ensureWorkspaceContext(request, workspaceId, true);
|
|
1557
|
+
};
|
|
1578
1558
|
}
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1559
|
+
function createBodyValidator(schema) {
|
|
1560
|
+
return async function validateBody(request, reply) {
|
|
1561
|
+
const parsed = schema.safeParse(request.body);
|
|
1562
|
+
if (!parsed.success) {
|
|
1563
|
+
const firstIssue = parsed.error.issues[0];
|
|
1564
|
+
const fieldName = firstIssue?.path?.map((segment) => String(segment)).join(".");
|
|
1565
|
+
reply.code(400).send(
|
|
1566
|
+
errorPayload(
|
|
1567
|
+
ERROR_CODE_VALIDATION_ERROR,
|
|
1568
|
+
firstIssue?.message ?? "Invalid request body",
|
|
1569
|
+
fieldName || void 0
|
|
1570
|
+
)
|
|
1571
|
+
);
|
|
1572
|
+
return;
|
|
1587
1573
|
}
|
|
1574
|
+
request.body = parsed.data;
|
|
1575
|
+
};
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
// src/server/logging.ts
|
|
1579
|
+
var SENSITIVE_KEYS = new Set([
|
|
1580
|
+
"apiKey",
|
|
1581
|
+
"api_key",
|
|
1582
|
+
"token",
|
|
1583
|
+
"secret",
|
|
1584
|
+
"password",
|
|
1585
|
+
"authorization",
|
|
1586
|
+
"cookie",
|
|
1587
|
+
"oidcToken",
|
|
1588
|
+
"accessToken",
|
|
1589
|
+
"refreshToken",
|
|
1590
|
+
"ANTHROPIC_API_KEY",
|
|
1591
|
+
"OPENAI_API_KEY",
|
|
1592
|
+
"VERCEL_OIDC_TOKEN",
|
|
1593
|
+
"VERCEL_TEAM_ID"
|
|
1594
|
+
].map((key) => key.toLowerCase()));
|
|
1595
|
+
function isSensitiveKey(key) {
|
|
1596
|
+
return SENSITIVE_KEYS.has(key.toLowerCase());
|
|
1597
|
+
}
|
|
1598
|
+
function redactValue(key, value, seen) {
|
|
1599
|
+
if (key && isSensitiveKey(key) && value != null) return "***";
|
|
1600
|
+
if (value == null || typeof value !== "object") return value;
|
|
1601
|
+
if (value instanceof Date) return value;
|
|
1602
|
+
if (seen.has(value)) return "[Circular]";
|
|
1603
|
+
seen.add(value);
|
|
1604
|
+
if (Array.isArray(value)) {
|
|
1605
|
+
const out2 = value.map((item) => redactValue(void 0, item, seen));
|
|
1606
|
+
seen.delete(value);
|
|
1607
|
+
return out2;
|
|
1608
|
+
}
|
|
1609
|
+
const out = {};
|
|
1610
|
+
for (const [childKey, childValue] of Object.entries(value)) {
|
|
1611
|
+
out[childKey] = redactValue(childKey, childValue, seen);
|
|
1588
1612
|
}
|
|
1613
|
+
seen.delete(value);
|
|
1614
|
+
return out;
|
|
1589
1615
|
}
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
const
|
|
1593
|
-
const
|
|
1594
|
-
|
|
1595
|
-
if (uv) await run("uv", ["venv", ".venv"], workspaceRoot);
|
|
1596
|
-
else await run("/usr/bin/python3", ["-m", "venv", ".venv"], workspaceRoot);
|
|
1616
|
+
function redact(fields) {
|
|
1617
|
+
const out = {};
|
|
1618
|
+
const seen = /* @__PURE__ */ new WeakSet();
|
|
1619
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
1620
|
+
out[k] = redactValue(k, v, seen);
|
|
1597
1621
|
}
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1622
|
+
return out;
|
|
1623
|
+
}
|
|
1624
|
+
var verbose = typeof process !== "undefined" && process.env?.BORING_AGENT_VERBOSE === "1";
|
|
1625
|
+
function createLogger(prefix) {
|
|
1626
|
+
function emit(level, msg, fields) {
|
|
1627
|
+
const entry = {
|
|
1628
|
+
level,
|
|
1629
|
+
prefix,
|
|
1630
|
+
msg,
|
|
1631
|
+
...fields ? redact(fields) : {},
|
|
1632
|
+
t: (/* @__PURE__ */ new Date()).toISOString()
|
|
1633
|
+
};
|
|
1634
|
+
if (level === "error") {
|
|
1635
|
+
console.error(JSON.stringify(entry));
|
|
1636
|
+
} else if (level === "warn") {
|
|
1637
|
+
console.warn(JSON.stringify(entry));
|
|
1605
1638
|
} else {
|
|
1606
|
-
|
|
1607
|
-
if (spec.extraLibs?.length) await run(venvPython, ["-m", "pip", "install", ...spec.extraLibs], workspaceRoot);
|
|
1639
|
+
console.log(JSON.stringify(entry));
|
|
1608
1640
|
}
|
|
1609
1641
|
}
|
|
1642
|
+
return {
|
|
1643
|
+
debug(msg, fields) {
|
|
1644
|
+
if (verbose) emit("debug", msg, fields);
|
|
1645
|
+
},
|
|
1646
|
+
info(msg, fields) {
|
|
1647
|
+
emit("info", msg, fields);
|
|
1648
|
+
},
|
|
1649
|
+
warn(msg, fields) {
|
|
1650
|
+
emit("warn", msg, fields);
|
|
1651
|
+
},
|
|
1652
|
+
error(msg, fields) {
|
|
1653
|
+
emit("error", msg, fields);
|
|
1654
|
+
}
|
|
1655
|
+
};
|
|
1610
1656
|
}
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
1620
|
-
throw new Error(`Invalid provisioning env key: ${key}`);
|
|
1621
|
-
}
|
|
1657
|
+
|
|
1658
|
+
// src/server/http/routes/file.ts
|
|
1659
|
+
var log = createLogger("boring/workspace-settings");
|
|
1660
|
+
var BORING_SETTINGS_PATH = ".boring/settings";
|
|
1661
|
+
var DEFAULT_MARKDOWN_IMAGE_UPLOAD_DIR = "assets/images";
|
|
1662
|
+
var MAX_UPLOAD_BYTES = 10 * 1024 * 1024;
|
|
1663
|
+
function defaultWorkspaceSettings() {
|
|
1664
|
+
return { markdown: { imageUploadDir: DEFAULT_MARKDOWN_IMAGE_UPLOAD_DIR } };
|
|
1622
1665
|
}
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
if (hasPython && !await exists(join3(workspaceRoot, ".venv", "bin", "python"))) return false;
|
|
1626
|
-
return true;
|
|
1666
|
+
function isPathValidationError(err) {
|
|
1667
|
+
return err instanceof Error && typeof err.reason === "string";
|
|
1627
1668
|
}
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
assertEnvKey(key);
|
|
1634
|
-
return `export ${key}=${bashSingleQuote(value)}`;
|
|
1635
|
-
}).join("\n");
|
|
1636
|
-
const base = `#!/usr/bin/env bash
|
|
1637
|
-
set -euo pipefail
|
|
1638
|
-
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
|
|
1639
|
-
WORKSPACE_ROOT="$(CDPATH= cd -- "$SCRIPT_DIR/../.." && pwd)"
|
|
1640
|
-
export BORING_AGENT_WORKSPACE_ROOT="$WORKSPACE_ROOT"
|
|
1641
|
-
${exports}
|
|
1642
|
-
VENV_BIN="$WORKSPACE_ROOT/.venv/bin"
|
|
1643
|
-
`;
|
|
1644
|
-
await writeExecutable(join3(shimDir, "python"), `${base}exec "$VENV_BIN/python" "$@"
|
|
1645
|
-
`);
|
|
1646
|
-
await writeExecutable(join3(shimDir, "python3"), `${base}exec "$VENV_BIN/python" "$@"
|
|
1647
|
-
`);
|
|
1648
|
-
await writeExecutable(join3(shimDir, "pip"), `${base}exec "$VENV_BIN/python" -m pip "$@"
|
|
1649
|
-
`);
|
|
1650
|
-
await writeExecutable(join3(shimDir, "pip3"), `${base}exec "$VENV_BIN/python" -m pip "$@"
|
|
1651
|
-
`);
|
|
1652
|
-
if (await exists(venvBin)) {
|
|
1653
|
-
for (const entry of await readdir2(venvBin)) {
|
|
1654
|
-
if (["python", "python3", "pip", "pip3"].includes(entry)) continue;
|
|
1655
|
-
const full = join3(venvBin, entry);
|
|
1656
|
-
const info = await stat3(full).catch(() => null);
|
|
1657
|
-
if (!info?.isFile()) continue;
|
|
1658
|
-
await writeExecutable(join3(shimDir, entry), `${base}TARGET="$VENV_BIN"/${bashSingleQuote(entry)}
|
|
1659
|
-
SHEBANG="$(head -n 1 "$TARGET" 2>/dev/null || true)"
|
|
1660
|
-
case "$SHEBANG" in
|
|
1661
|
-
*python*) exec "$VENV_BIN/python" "$TARGET" "$@" ;;
|
|
1662
|
-
esac
|
|
1663
|
-
exec "$TARGET" "$@"
|
|
1664
|
-
`);
|
|
1665
|
-
}
|
|
1669
|
+
function classifyError(err, reply, subject) {
|
|
1670
|
+
if (isPathValidationError(err)) {
|
|
1671
|
+
return reply.code(403).send({
|
|
1672
|
+
error: { code: ERROR_CODE_PATH_REJECTED, message: "path traversal rejected" }
|
|
1673
|
+
});
|
|
1666
1674
|
}
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
}) {
|
|
1674
|
-
const active = [];
|
|
1675
|
-
for (const contribution of contributions ?? []) {
|
|
1676
|
-
if (contribution.provisioning) active.push({ id: contribution.id, provisioning: contribution.provisioning });
|
|
1675
|
+
const message = err instanceof Error ? err.message : "unknown error";
|
|
1676
|
+
const code = err?.code;
|
|
1677
|
+
if (code === "EPERM" || message.toLowerCase().includes("traversal") || message.includes("EPERM")) {
|
|
1678
|
+
return reply.code(403).send({
|
|
1679
|
+
error: { code: ERROR_CODE_PATH_REJECTED, message: "path traversal rejected" }
|
|
1680
|
+
});
|
|
1677
1681
|
}
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
const binDir = join3(workspaceRoot, ".boring-agent", "bin");
|
|
1683
|
-
if (!force && await exists(markerPath)) {
|
|
1684
|
-
try {
|
|
1685
|
-
const marker = JSON.parse(await readFile4(markerPath, "utf8"));
|
|
1686
|
-
if (marker.fingerprint === hash && await isRuntimeMaterialized(workspaceRoot, active)) {
|
|
1687
|
-
await writeShims(workspaceRoot, env);
|
|
1688
|
-
return { fingerprint: hash, changed: false, env, binDir };
|
|
1689
|
-
}
|
|
1690
|
-
} catch {
|
|
1691
|
-
}
|
|
1682
|
+
if (code === "ENOENT" || message.includes("ENOENT")) {
|
|
1683
|
+
return reply.code(404).send({
|
|
1684
|
+
error: { code: ERROR_CODE_NOT_FOUND, message: `${subject} not found` }
|
|
1685
|
+
});
|
|
1692
1686
|
}
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
return {
|
|
1687
|
+
if (code === "EEXIST" || message.includes("EEXIST")) {
|
|
1688
|
+
return reply.code(409).send({
|
|
1689
|
+
error: { code: ERROR_CODE_ALREADY_EXISTS, message: `${subject} already exists` }
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
return reply.code(500).send({
|
|
1693
|
+
error: { code: ERROR_CODE_INTERNAL, message }
|
|
1694
|
+
});
|
|
1699
1695
|
}
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1696
|
+
function requireStringParam(value, field, reply) {
|
|
1697
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
1698
|
+
reply.code(400).send({
|
|
1699
|
+
error: { code: ERROR_CODE_VALIDATION_ERROR, message: `${field} is required`, field }
|
|
1700
|
+
});
|
|
1701
|
+
return null;
|
|
1702
|
+
}
|
|
1703
|
+
if (value.includes("\0")) {
|
|
1704
|
+
reply.code(400).send({
|
|
1705
|
+
error: { code: ERROR_CODE_INVALID_PATH, message: "null bytes not allowed", field }
|
|
1706
|
+
});
|
|
1707
|
+
return null;
|
|
1708
|
+
}
|
|
1709
|
+
return value;
|
|
1710
1710
|
}
|
|
1711
|
-
function
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
return void 0;
|
|
1721
|
-
}
|
|
1722
|
-
entries.delete(key);
|
|
1723
|
-
entries.set(key, entry);
|
|
1724
|
-
return entry.value;
|
|
1725
|
-
},
|
|
1726
|
-
set(key, value) {
|
|
1727
|
-
entries.delete(key);
|
|
1728
|
-
entries.set(key, { value, expiresAt: Date.now() + ttlMs });
|
|
1729
|
-
while (entries.size > maxEntries) {
|
|
1730
|
-
const oldest = entries.keys().next().value;
|
|
1731
|
-
if (!oldest) return;
|
|
1732
|
-
entries.delete(oldest);
|
|
1711
|
+
function parseWorkspaceSettings(raw) {
|
|
1712
|
+
try {
|
|
1713
|
+
const parsed = JSON.parse(raw);
|
|
1714
|
+
const dir = parsed?.markdown?.imageUploadDir;
|
|
1715
|
+
return {
|
|
1716
|
+
...parsed,
|
|
1717
|
+
markdown: {
|
|
1718
|
+
...parsed.markdown ?? {},
|
|
1719
|
+
imageUploadDir: typeof dir === "string" && dir.trim() ? dir.trim() : DEFAULT_MARKDOWN_IMAGE_UPLOAD_DIR
|
|
1733
1720
|
}
|
|
1734
|
-
}
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1721
|
+
};
|
|
1722
|
+
} catch (err) {
|
|
1723
|
+
log.warn("failed to parse .boring/settings \u2014 falling back to defaults", {
|
|
1724
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1725
|
+
});
|
|
1726
|
+
return defaultWorkspaceSettings();
|
|
1727
|
+
}
|
|
1739
1728
|
}
|
|
1740
|
-
function
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1729
|
+
async function readWorkspaceSettings(workspace) {
|
|
1730
|
+
try {
|
|
1731
|
+
return parseWorkspaceSettings(await workspace.readFile(BORING_SETTINGS_PATH));
|
|
1732
|
+
} catch (error) {
|
|
1733
|
+
const code = error?.code;
|
|
1734
|
+
if (code === "ENOENT") return defaultWorkspaceSettings();
|
|
1735
|
+
throw error;
|
|
1736
|
+
}
|
|
1746
1737
|
}
|
|
1747
|
-
function
|
|
1748
|
-
|
|
1738
|
+
function normalizeUploadDir(value) {
|
|
1739
|
+
if (typeof value !== "string") return null;
|
|
1740
|
+
const dir = value.trim().replace(/^\.\/+/, "").replace(/\/+/g, "/");
|
|
1741
|
+
if (!dir || dir.includes("\0") || dir.startsWith("/") || dir.split("/").includes("..")) return null;
|
|
1742
|
+
return dir.replace(/\/+$/, "");
|
|
1749
1743
|
}
|
|
1750
|
-
function
|
|
1751
|
-
|
|
1744
|
+
function extForUpload(filename, contentType) {
|
|
1745
|
+
const fromName = extname(filename).toLowerCase().replace(/^\./, "");
|
|
1746
|
+
if (/^[a-z0-9]{1,12}$/.test(fromName)) return fromName;
|
|
1747
|
+
if (contentType === "image/jpeg") return "jpg";
|
|
1748
|
+
if (contentType === "image/png") return "png";
|
|
1749
|
+
if (contentType === "image/gif") return "gif";
|
|
1750
|
+
if (contentType === "image/webp") return "webp";
|
|
1751
|
+
if (contentType === "image/svg+xml") return "svg";
|
|
1752
|
+
return "bin";
|
|
1752
1753
|
}
|
|
1753
|
-
|
|
1754
|
-
const
|
|
1755
|
-
const
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
]);
|
|
1759
|
-
if ((result.exitCode ?? 1) !== 0) {
|
|
1760
|
-
throw new Error(err || `sandbox command failed with exit code ${result.exitCode}`);
|
|
1761
|
-
}
|
|
1762
|
-
return JSON.parse(out);
|
|
1754
|
+
function basenameForUpload(filename) {
|
|
1755
|
+
const base = filename.split("/").pop()?.split("\\").pop() ?? "image";
|
|
1756
|
+
const withoutExt = base.replace(/\.[^.]*$/, "");
|
|
1757
|
+
const safe = withoutExt.normalize("NFKD").replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
|
|
1758
|
+
return safe || "image";
|
|
1763
1759
|
}
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1760
|
+
function markdownUrlFor(sourcePath, assetPath) {
|
|
1761
|
+
if (!sourcePath) return assetPath;
|
|
1762
|
+
const fromDir = dirname4(sourcePath.replace(/\\/g, "/"));
|
|
1763
|
+
const rel = relative4(fromDir === "." ? "" : fromDir, assetPath);
|
|
1764
|
+
return rel && !rel.startsWith(".") ? rel : rel || assetPath;
|
|
1765
|
+
}
|
|
1766
|
+
function contentTypeForPath(path4) {
|
|
1767
|
+
switch (extname(path4).toLowerCase()) {
|
|
1768
|
+
case ".avif":
|
|
1769
|
+
return "image/avif";
|
|
1770
|
+
case ".gif":
|
|
1771
|
+
return "image/gif";
|
|
1772
|
+
case ".jpg":
|
|
1773
|
+
case ".jpeg":
|
|
1774
|
+
return "image/jpeg";
|
|
1775
|
+
case ".png":
|
|
1776
|
+
return "image/png";
|
|
1777
|
+
case ".svg":
|
|
1778
|
+
return "image/svg+xml";
|
|
1779
|
+
case ".webp":
|
|
1780
|
+
return "image/webp";
|
|
1781
|
+
case ".css":
|
|
1782
|
+
return "text/css; charset=utf-8";
|
|
1783
|
+
case ".html":
|
|
1784
|
+
case ".htm":
|
|
1785
|
+
return "text/html; charset=utf-8";
|
|
1786
|
+
case ".js":
|
|
1787
|
+
return "text/javascript; charset=utf-8";
|
|
1788
|
+
case ".mjs":
|
|
1789
|
+
return "text/javascript; charset=utf-8";
|
|
1790
|
+
case ".pdf":
|
|
1791
|
+
return "application/pdf";
|
|
1792
|
+
default:
|
|
1793
|
+
return "application/octet-stream";
|
|
1769
1794
|
}
|
|
1770
1795
|
}
|
|
1771
|
-
function
|
|
1796
|
+
function fileRoutes(app, opts, done) {
|
|
1797
|
+
async function resolveWorkspace(request) {
|
|
1798
|
+
if (opts.getWorkspace) return await opts.getWorkspace(request);
|
|
1799
|
+
if (opts.workspace) return opts.workspace;
|
|
1800
|
+
throw new Error("file route requires workspace or getWorkspace");
|
|
1801
|
+
}
|
|
1802
|
+
app.get("/api/v1/files/raw", async (request, reply) => {
|
|
1803
|
+
const query = request.query;
|
|
1804
|
+
const path4 = requireStringParam(query.path, "path", reply);
|
|
1805
|
+
if (path4 === null) return;
|
|
1806
|
+
try {
|
|
1807
|
+
const workspace = await resolveWorkspace(request);
|
|
1808
|
+
if (!workspace.readBinaryFile) {
|
|
1809
|
+
return reply.code(501).send({
|
|
1810
|
+
error: { code: ERROR_CODE_INTERNAL, message: "workspace does not support binary reads" }
|
|
1811
|
+
});
|
|
1812
|
+
}
|
|
1813
|
+
const stat7 = await workspace.stat(path4);
|
|
1814
|
+
if (stat7.kind !== "file") {
|
|
1815
|
+
return reply.code(400).send({
|
|
1816
|
+
error: { code: ERROR_CODE_VALIDATION_ERROR, message: "path is not a file", field: "path" }
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
const bytes = await workspace.readBinaryFile(path4);
|
|
1820
|
+
return reply.header("content-type", contentTypeForPath(path4)).header("content-length", String(bytes.byteLength)).header("cache-control", "no-store").send(Buffer.from(bytes));
|
|
1821
|
+
} catch (err) {
|
|
1822
|
+
return classifyError(err, reply, "file");
|
|
1823
|
+
}
|
|
1824
|
+
});
|
|
1825
|
+
app.get("/api/v1/files", async (request, reply) => {
|
|
1826
|
+
const query = request.query;
|
|
1827
|
+
const path4 = requireStringParam(query.path, "path", reply);
|
|
1828
|
+
if (path4 === null) return;
|
|
1829
|
+
try {
|
|
1830
|
+
const workspace = await resolveWorkspace(request);
|
|
1831
|
+
if (workspace.readFileWithStat) {
|
|
1832
|
+
const { content: content2, stat: stat8 } = await workspace.readFileWithStat(path4);
|
|
1833
|
+
return { content: content2, mtimeMs: stat8.kind === "file" ? stat8.mtimeMs : void 0 };
|
|
1834
|
+
}
|
|
1835
|
+
const content = await workspace.readFile(path4);
|
|
1836
|
+
const stat7 = await workspace.stat(path4);
|
|
1837
|
+
return { content, mtimeMs: stat7.kind === "file" ? stat7.mtimeMs : void 0 };
|
|
1838
|
+
} catch (err) {
|
|
1839
|
+
return classifyError(err, reply, "file");
|
|
1840
|
+
}
|
|
1841
|
+
});
|
|
1842
|
+
app.post("/api/v1/files", async (request, reply) => {
|
|
1843
|
+
const body = request.body;
|
|
1844
|
+
const path4 = requireStringParam(body?.path, "path", reply);
|
|
1845
|
+
if (path4 === null) return;
|
|
1846
|
+
if (typeof body.content !== "string") {
|
|
1847
|
+
return reply.code(400).send({
|
|
1848
|
+
error: { code: ERROR_CODE_VALIDATION_ERROR, message: "content is required", field: "content" }
|
|
1849
|
+
});
|
|
1850
|
+
}
|
|
1851
|
+
const expectedMtimeMs = typeof body.expectedMtimeMs === "number" ? body.expectedMtimeMs : null;
|
|
1852
|
+
try {
|
|
1853
|
+
const workspace = await resolveWorkspace(request);
|
|
1854
|
+
if (expectedMtimeMs !== null) {
|
|
1855
|
+
try {
|
|
1856
|
+
const current = await workspace.stat(path4);
|
|
1857
|
+
if (current.kind === "file" && current.mtimeMs !== expectedMtimeMs) {
|
|
1858
|
+
return reply.code(409).send({
|
|
1859
|
+
error: {
|
|
1860
|
+
code: ERROR_CODE_CONFLICT,
|
|
1861
|
+
message: "file has been modified since last read",
|
|
1862
|
+
currentMtimeMs: current.mtimeMs,
|
|
1863
|
+
expectedMtimeMs
|
|
1864
|
+
}
|
|
1865
|
+
});
|
|
1866
|
+
}
|
|
1867
|
+
} catch (statErr) {
|
|
1868
|
+
const code = statErr?.code;
|
|
1869
|
+
if (code === "ENOENT") {
|
|
1870
|
+
return reply.code(409).send({
|
|
1871
|
+
error: {
|
|
1872
|
+
code: ERROR_CODE_CONFLICT,
|
|
1873
|
+
message: "file no longer exists",
|
|
1874
|
+
expectedMtimeMs
|
|
1875
|
+
}
|
|
1876
|
+
});
|
|
1877
|
+
}
|
|
1878
|
+
throw statErr;
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
if (body.createDirs) {
|
|
1882
|
+
const dir = path4.includes("/") ? path4.slice(0, path4.lastIndexOf("/")) : void 0;
|
|
1883
|
+
if (dir) await workspace.mkdir(dir, { recursive: true });
|
|
1884
|
+
}
|
|
1885
|
+
const content = body.content;
|
|
1886
|
+
const stat7 = workspace.writeFileWithStat ? await workspace.writeFileWithStat(path4, content) : await (async () => {
|
|
1887
|
+
await workspace.writeFile(path4, content);
|
|
1888
|
+
return await workspace.stat(path4);
|
|
1889
|
+
})();
|
|
1890
|
+
return { ok: true, mtimeMs: stat7.kind === "file" ? stat7.mtimeMs : void 0 };
|
|
1891
|
+
} catch (err) {
|
|
1892
|
+
return classifyError(err, reply, "file");
|
|
1893
|
+
}
|
|
1894
|
+
});
|
|
1895
|
+
app.post("/api/v1/files/upload", async (request, reply) => {
|
|
1896
|
+
const body = request.body;
|
|
1897
|
+
const filename = requireStringParam(body?.filename, "filename", reply);
|
|
1898
|
+
if (filename === null) return;
|
|
1899
|
+
const contentBase64 = requireStringParam(body?.contentBase64, "contentBase64", reply);
|
|
1900
|
+
if (contentBase64 === null) return;
|
|
1901
|
+
const contentType = typeof body.contentType === "string" ? body.contentType.trim().toLowerCase() : "";
|
|
1902
|
+
if (!contentType.startsWith("image/")) {
|
|
1903
|
+
return reply.code(400).send({
|
|
1904
|
+
error: { code: ERROR_CODE_VALIDATION_ERROR, message: "contentType must be an image/* MIME type", field: "contentType" }
|
|
1905
|
+
});
|
|
1906
|
+
}
|
|
1907
|
+
try {
|
|
1908
|
+
const workspace = await resolveWorkspace(request);
|
|
1909
|
+
const settings = await readWorkspaceSettings(workspace);
|
|
1910
|
+
const dir = normalizeUploadDir(body.directory) ?? normalizeUploadDir(settings.markdown?.imageUploadDir) ?? DEFAULT_MARKDOWN_IMAGE_UPLOAD_DIR;
|
|
1911
|
+
const ext = extForUpload(filename, contentType);
|
|
1912
|
+
const base = basenameForUpload(filename);
|
|
1913
|
+
const unique = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1914
|
+
const path4 = `${dir}/${base}-${unique}.${ext}`;
|
|
1915
|
+
const estimatedBytes = Math.ceil(contentBase64.length * 0.75);
|
|
1916
|
+
if (estimatedBytes === 0 || estimatedBytes > MAX_UPLOAD_BYTES) {
|
|
1917
|
+
return reply.code(400).send({
|
|
1918
|
+
error: { code: ERROR_CODE_VALIDATION_ERROR, message: `upload must be 1 byte to ${MAX_UPLOAD_BYTES} bytes`, field: "contentBase64" }
|
|
1919
|
+
});
|
|
1920
|
+
}
|
|
1921
|
+
const bytes = Buffer.from(contentBase64, "base64");
|
|
1922
|
+
await workspace.mkdir(dir, { recursive: true });
|
|
1923
|
+
const stat7 = workspace.writeBinaryFileWithStat ? await workspace.writeBinaryFileWithStat(path4, bytes) : await (async () => {
|
|
1924
|
+
if (!workspace.writeBinaryFile) {
|
|
1925
|
+
throw new Error("workspace does not support binary uploads");
|
|
1926
|
+
}
|
|
1927
|
+
await workspace.writeBinaryFile(path4, bytes);
|
|
1928
|
+
return await workspace.stat(path4);
|
|
1929
|
+
})();
|
|
1930
|
+
const MAX_SOURCE_PATH = 1024;
|
|
1931
|
+
const rawSourcePath = body.sourcePath;
|
|
1932
|
+
const sourcePath = typeof rawSourcePath === "string" && rawSourcePath.length > 0 && rawSourcePath.length <= MAX_SOURCE_PATH && !rawSourcePath.includes("\0") ? rawSourcePath : null;
|
|
1933
|
+
return {
|
|
1934
|
+
ok: true,
|
|
1935
|
+
path: path4,
|
|
1936
|
+
markdownUrl: markdownUrlFor(sourcePath, path4),
|
|
1937
|
+
mtimeMs: stat7.kind === "file" ? stat7.mtimeMs : void 0
|
|
1938
|
+
};
|
|
1939
|
+
} catch (err) {
|
|
1940
|
+
return classifyError(err, reply, "upload");
|
|
1941
|
+
}
|
|
1942
|
+
});
|
|
1943
|
+
app.get("/api/v1/workspace-settings", async (request, reply) => {
|
|
1944
|
+
try {
|
|
1945
|
+
const workspace = await resolveWorkspace(request);
|
|
1946
|
+
return { settings: await readWorkspaceSettings(workspace) };
|
|
1947
|
+
} catch (err) {
|
|
1948
|
+
return classifyError(err, reply, "settings");
|
|
1949
|
+
}
|
|
1950
|
+
});
|
|
1951
|
+
app.put("/api/v1/workspace-settings", async (request, reply) => {
|
|
1952
|
+
const body = request.body;
|
|
1953
|
+
const incoming = body?.settings ?? body;
|
|
1954
|
+
const markdown = incoming.markdown ?? {};
|
|
1955
|
+
const imageUploadDir = normalizeUploadDir(markdown.imageUploadDir);
|
|
1956
|
+
if (!imageUploadDir) {
|
|
1957
|
+
return reply.code(400).send({
|
|
1958
|
+
error: { code: ERROR_CODE_INVALID_PATH, message: "markdown.imageUploadDir must be a relative workspace path", field: "markdown.imageUploadDir" }
|
|
1959
|
+
});
|
|
1960
|
+
}
|
|
1961
|
+
try {
|
|
1962
|
+
const workspace = await resolveWorkspace(request);
|
|
1963
|
+
const current = await readWorkspaceSettings(workspace);
|
|
1964
|
+
const next = {
|
|
1965
|
+
...current,
|
|
1966
|
+
markdown: {
|
|
1967
|
+
...current.markdown ?? {},
|
|
1968
|
+
imageUploadDir
|
|
1969
|
+
}
|
|
1970
|
+
};
|
|
1971
|
+
await workspace.mkdir(".boring", { recursive: true });
|
|
1972
|
+
await workspace.writeFile(BORING_SETTINGS_PATH, `${JSON.stringify(next, null, 2)}
|
|
1973
|
+
`);
|
|
1974
|
+
return { settings: next };
|
|
1975
|
+
} catch (err) {
|
|
1976
|
+
return classifyError(err, reply, "settings");
|
|
1977
|
+
}
|
|
1978
|
+
});
|
|
1979
|
+
app.delete("/api/v1/files", async (request, reply) => {
|
|
1980
|
+
const query = request.query;
|
|
1981
|
+
const path4 = requireStringParam(query.path, "path", reply);
|
|
1982
|
+
if (path4 === null) return;
|
|
1983
|
+
try {
|
|
1984
|
+
const workspace = await resolveWorkspace(request);
|
|
1985
|
+
await workspace.unlink(path4);
|
|
1986
|
+
return { ok: true };
|
|
1987
|
+
} catch (err) {
|
|
1988
|
+
return classifyError(err, reply, "file");
|
|
1989
|
+
}
|
|
1990
|
+
});
|
|
1991
|
+
app.post("/api/v1/files/move", async (request, reply) => {
|
|
1992
|
+
const body = request.body;
|
|
1993
|
+
const from = requireStringParam(body?.from, "from", reply);
|
|
1994
|
+
if (from === null) return;
|
|
1995
|
+
const to = requireStringParam(body?.to, "to", reply);
|
|
1996
|
+
if (to === null) return;
|
|
1997
|
+
try {
|
|
1998
|
+
const workspace = await resolveWorkspace(request);
|
|
1999
|
+
await workspace.rename(from, to);
|
|
2000
|
+
return { ok: true };
|
|
2001
|
+
} catch (err) {
|
|
2002
|
+
return classifyError(err, reply, "file");
|
|
2003
|
+
}
|
|
2004
|
+
});
|
|
2005
|
+
app.post("/api/v1/dirs", async (request, reply) => {
|
|
2006
|
+
const body = request.body;
|
|
2007
|
+
const path4 = requireStringParam(body?.path, "path", reply);
|
|
2008
|
+
if (path4 === null) return;
|
|
2009
|
+
const recursive = body.recursive === true;
|
|
2010
|
+
try {
|
|
2011
|
+
const workspace = await resolveWorkspace(request);
|
|
2012
|
+
await workspace.mkdir(path4, { recursive });
|
|
2013
|
+
return { ok: true };
|
|
2014
|
+
} catch (err) {
|
|
2015
|
+
return classifyError(err, reply, "directory");
|
|
2016
|
+
}
|
|
2017
|
+
});
|
|
2018
|
+
app.get("/api/v1/stat", async (request, reply) => {
|
|
2019
|
+
const query = request.query;
|
|
2020
|
+
const path4 = requireStringParam(query.path, "path", reply);
|
|
2021
|
+
if (path4 === null) return;
|
|
2022
|
+
try {
|
|
2023
|
+
const workspace = await resolveWorkspace(request);
|
|
2024
|
+
const stat7 = await workspace.stat(path4);
|
|
2025
|
+
return stat7;
|
|
2026
|
+
} catch (err) {
|
|
2027
|
+
return classifyError(err, reply, "path");
|
|
2028
|
+
}
|
|
2029
|
+
});
|
|
2030
|
+
done();
|
|
2031
|
+
}
|
|
2032
|
+
|
|
2033
|
+
// src/server/workspace/provisionRuntime.ts
|
|
2034
|
+
import { createHash as createHash3 } from "crypto";
|
|
2035
|
+
import { constants as constants2 } from "fs";
|
|
2036
|
+
import { access as access2, chmod as chmod3, cp, mkdir as mkdir4, readdir as readdir2, readFile as readFile4, stat as stat3, writeFile as writeFile4 } from "fs/promises";
|
|
2037
|
+
import { dirname as dirname5, join as join3, resolve as resolve3 } from "path";
|
|
2038
|
+
import { fileURLToPath } from "url";
|
|
2039
|
+
import { execFile } from "child_process";
|
|
2040
|
+
import { promisify } from "util";
|
|
2041
|
+
var execFileAsync = promisify(execFile);
|
|
2042
|
+
var PROVISIONING_VERSION = 1;
|
|
2043
|
+
function toPath(value) {
|
|
2044
|
+
return value instanceof URL ? fileURLToPath(value) : value;
|
|
2045
|
+
}
|
|
2046
|
+
async function exists(path4) {
|
|
2047
|
+
try {
|
|
2048
|
+
await access2(path4, constants2.F_OK);
|
|
2049
|
+
return true;
|
|
2050
|
+
} catch {
|
|
2051
|
+
return false;
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
async function run(cmd, args, cwd) {
|
|
2055
|
+
await execFileAsync(cmd, args, {
|
|
2056
|
+
cwd,
|
|
2057
|
+
env: process.env,
|
|
2058
|
+
maxBuffer: 1024 * 1024 * 20,
|
|
2059
|
+
timeout: 5 * 60 * 1e3
|
|
2060
|
+
});
|
|
2061
|
+
}
|
|
2062
|
+
async function commandExists(cmd) {
|
|
2063
|
+
try {
|
|
2064
|
+
await execFileAsync(cmd, ["--version"], { maxBuffer: 1024 * 1024 });
|
|
2065
|
+
return true;
|
|
2066
|
+
} catch {
|
|
2067
|
+
return false;
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
async function hashPath(path4, hash) {
|
|
2071
|
+
const info = await stat3(path4);
|
|
2072
|
+
if (info.isDirectory()) {
|
|
2073
|
+
const entries = (await readdir2(path4)).filter((entry) => entry !== "__pycache__" && entry !== "build" && !entry.endsWith(".egg-info")).sort();
|
|
2074
|
+
for (const entry of entries) {
|
|
2075
|
+
hash.update(`dir:${entry}
|
|
2076
|
+
`);
|
|
2077
|
+
await hashPath(join3(path4, entry), hash);
|
|
2078
|
+
}
|
|
2079
|
+
return;
|
|
2080
|
+
}
|
|
2081
|
+
if (!info.isFile()) return;
|
|
2082
|
+
hash.update(`file:${path4}
|
|
2083
|
+
`);
|
|
2084
|
+
hash.update(await readFile4(path4));
|
|
2085
|
+
}
|
|
2086
|
+
async function fingerprint(contributions) {
|
|
2087
|
+
const hash = createHash3("sha256");
|
|
2088
|
+
hash.update(JSON.stringify({ v: PROVISIONING_VERSION }));
|
|
2089
|
+
for (const { id, provisioning } of contributions) {
|
|
2090
|
+
hash.update(`plugin:${id}
|
|
2091
|
+
`);
|
|
2092
|
+
for (const template of provisioning.templateDirs ?? []) {
|
|
2093
|
+
const templatePath = toPath(template.path);
|
|
2094
|
+
hash.update(`template:${template.id}:${template.target ?? ""}:${templatePath}
|
|
2095
|
+
`);
|
|
2096
|
+
if (await exists(templatePath)) await hashPath(templatePath, hash);
|
|
2097
|
+
}
|
|
2098
|
+
for (const spec of provisioning.python ?? []) {
|
|
2099
|
+
const projectFile = toPath(spec.projectFile);
|
|
2100
|
+
hash.update(`python:${spec.id}:${projectFile}
|
|
2101
|
+
`);
|
|
2102
|
+
hash.update(JSON.stringify(spec.extraLibs ?? []));
|
|
2103
|
+
hash.update(JSON.stringify(Object.fromEntries(Object.entries(spec.env ?? {}).map(([k, v]) => [k, String(v)]))));
|
|
2104
|
+
if (await exists(projectFile)) await hashPath(dirname5(projectFile), hash);
|
|
2105
|
+
}
|
|
2106
|
+
}
|
|
2107
|
+
return `sha256:${hash.digest("hex")}`;
|
|
2108
|
+
}
|
|
2109
|
+
function collectEnv(contributions) {
|
|
2110
|
+
const env = {};
|
|
2111
|
+
for (const { provisioning } of contributions) {
|
|
2112
|
+
for (const spec of provisioning.python ?? []) {
|
|
2113
|
+
for (const [key, value] of Object.entries(spec.env ?? {})) {
|
|
2114
|
+
env[key] = toPath(value);
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
return env;
|
|
2119
|
+
}
|
|
2120
|
+
async function seedTemplates(workspaceRoot, contributions) {
|
|
2121
|
+
for (const { provisioning } of contributions) {
|
|
2122
|
+
for (const template of provisioning.templateDirs ?? []) {
|
|
2123
|
+
await cp(toPath(template.path), resolve3(workspaceRoot, template.target ?? "."), {
|
|
2124
|
+
recursive: true,
|
|
2125
|
+
force: false,
|
|
2126
|
+
errorOnExist: false
|
|
2127
|
+
});
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
async function ensurePython(workspaceRoot, specs) {
|
|
2132
|
+
if (specs.length === 0) return;
|
|
2133
|
+
const venvPython = join3(workspaceRoot, ".venv", "bin", "python");
|
|
2134
|
+
const uv = await commandExists("uv");
|
|
2135
|
+
if (!await exists(venvPython)) {
|
|
2136
|
+
if (uv) await run("uv", ["venv", ".venv"], workspaceRoot);
|
|
2137
|
+
else await run("/usr/bin/python3", ["-m", "venv", ".venv"], workspaceRoot);
|
|
2138
|
+
}
|
|
2139
|
+
for (const spec of specs) {
|
|
2140
|
+
const projectDir = dirname5(toPath(spec.projectFile));
|
|
2141
|
+
if (uv) {
|
|
2142
|
+
await run("uv", ["pip", "install", "--python", venvPython, projectDir], workspaceRoot);
|
|
2143
|
+
if (spec.extraLibs?.length) {
|
|
2144
|
+
await run("uv", ["pip", "install", "--python", venvPython, ...spec.extraLibs], workspaceRoot);
|
|
2145
|
+
}
|
|
2146
|
+
} else {
|
|
2147
|
+
await run(venvPython, ["-m", "pip", "install", projectDir], workspaceRoot);
|
|
2148
|
+
if (spec.extraLibs?.length) await run(venvPython, ["-m", "pip", "install", ...spec.extraLibs], workspaceRoot);
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
async function writeExecutable(path4, body) {
|
|
2153
|
+
await writeFile4(path4, body, "utf8");
|
|
2154
|
+
await chmod3(path4, 493);
|
|
2155
|
+
}
|
|
2156
|
+
function bashSingleQuote(value) {
|
|
2157
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
2158
|
+
}
|
|
2159
|
+
function assertEnvKey(key) {
|
|
2160
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
|
|
2161
|
+
throw new Error(`Invalid provisioning env key: ${key}`);
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
async function isRuntimeMaterialized(workspaceRoot, contributions) {
|
|
2165
|
+
const hasPython = contributions.some(({ provisioning }) => (provisioning.python ?? []).length > 0);
|
|
2166
|
+
if (hasPython && !await exists(join3(workspaceRoot, ".venv", "bin", "python"))) return false;
|
|
2167
|
+
return true;
|
|
2168
|
+
}
|
|
2169
|
+
async function writeShims(workspaceRoot, env) {
|
|
2170
|
+
const shimDir = join3(workspaceRoot, ".boring-agent", "bin");
|
|
2171
|
+
const venvBin = join3(workspaceRoot, ".venv", "bin");
|
|
2172
|
+
await mkdir4(shimDir, { recursive: true });
|
|
2173
|
+
const exports = Object.entries(env).map(([key, value]) => {
|
|
2174
|
+
assertEnvKey(key);
|
|
2175
|
+
return `export ${key}=${bashSingleQuote(value)}`;
|
|
2176
|
+
}).join("\n");
|
|
2177
|
+
const base = `#!/usr/bin/env bash
|
|
2178
|
+
set -euo pipefail
|
|
2179
|
+
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
|
|
2180
|
+
WORKSPACE_ROOT="$(CDPATH= cd -- "$SCRIPT_DIR/../.." && pwd)"
|
|
2181
|
+
export BORING_AGENT_WORKSPACE_ROOT="$WORKSPACE_ROOT"
|
|
2182
|
+
${exports}
|
|
2183
|
+
VENV_BIN="$WORKSPACE_ROOT/.venv/bin"
|
|
2184
|
+
`;
|
|
2185
|
+
await writeExecutable(join3(shimDir, "python"), `${base}exec "$VENV_BIN/python" "$@"
|
|
2186
|
+
`);
|
|
2187
|
+
await writeExecutable(join3(shimDir, "python3"), `${base}exec "$VENV_BIN/python" "$@"
|
|
2188
|
+
`);
|
|
2189
|
+
await writeExecutable(join3(shimDir, "pip"), `${base}exec "$VENV_BIN/python" -m pip "$@"
|
|
2190
|
+
`);
|
|
2191
|
+
await writeExecutable(join3(shimDir, "pip3"), `${base}exec "$VENV_BIN/python" -m pip "$@"
|
|
2192
|
+
`);
|
|
2193
|
+
if (await exists(venvBin)) {
|
|
2194
|
+
for (const entry of await readdir2(venvBin)) {
|
|
2195
|
+
if (["python", "python3", "pip", "pip3"].includes(entry)) continue;
|
|
2196
|
+
const full = join3(venvBin, entry);
|
|
2197
|
+
const info = await stat3(full).catch(() => null);
|
|
2198
|
+
if (!info?.isFile()) continue;
|
|
2199
|
+
await writeExecutable(join3(shimDir, entry), `${base}TARGET="$VENV_BIN"/${bashSingleQuote(entry)}
|
|
2200
|
+
SHEBANG="$(head -n 1 "$TARGET" 2>/dev/null || true)"
|
|
2201
|
+
case "$SHEBANG" in
|
|
2202
|
+
*python*) exec "$VENV_BIN/python" "$TARGET" "$@" ;;
|
|
2203
|
+
esac
|
|
2204
|
+
exec "$TARGET" "$@"
|
|
2205
|
+
`);
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
return shimDir;
|
|
2209
|
+
}
|
|
2210
|
+
async function provisionRuntimeWorkspace({
|
|
2211
|
+
workspaceRoot,
|
|
2212
|
+
contributions,
|
|
2213
|
+
force = false
|
|
2214
|
+
}) {
|
|
2215
|
+
const active = [];
|
|
2216
|
+
for (const contribution of contributions ?? []) {
|
|
2217
|
+
if (contribution.provisioning) active.push({ id: contribution.id, provisioning: contribution.provisioning });
|
|
2218
|
+
}
|
|
2219
|
+
await mkdir4(workspaceRoot, { recursive: true });
|
|
2220
|
+
const env = collectEnv(active);
|
|
2221
|
+
const hash = await fingerprint(active);
|
|
2222
|
+
const markerPath = join3(workspaceRoot, ".boring-agent", "provisioning.json");
|
|
2223
|
+
const binDir = join3(workspaceRoot, ".boring-agent", "bin");
|
|
2224
|
+
if (!force && await exists(markerPath)) {
|
|
2225
|
+
try {
|
|
2226
|
+
const marker = JSON.parse(await readFile4(markerPath, "utf8"));
|
|
2227
|
+
if (marker.fingerprint === hash && await isRuntimeMaterialized(workspaceRoot, active)) {
|
|
2228
|
+
await writeShims(workspaceRoot, env);
|
|
2229
|
+
return { fingerprint: hash, changed: false, env, binDir };
|
|
2230
|
+
}
|
|
2231
|
+
} catch {
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
await seedTemplates(workspaceRoot, active);
|
|
2235
|
+
await ensurePython(workspaceRoot, active.flatMap(({ provisioning }) => provisioning.python ?? []));
|
|
2236
|
+
const actualBinDir = await writeShims(workspaceRoot, env);
|
|
2237
|
+
await mkdir4(dirname5(markerPath), { recursive: true });
|
|
2238
|
+
await writeFile4(markerPath, JSON.stringify({ v: PROVISIONING_VERSION, fingerprint: hash }, null, 2), "utf8");
|
|
2239
|
+
return { fingerprint: hash, changed: true, env, binDir: actualBinDir };
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
// src/server/workspace/createVercelSandboxWorkspace.ts
|
|
2243
|
+
var VERCEL_SANDBOX_REMOTE_ROOT = "/vercel/sandbox";
|
|
2244
|
+
var VERCEL_SANDBOX_WORKSPACE_ROOT = "/workspace";
|
|
2245
|
+
var CACHE_TTL_MS = 15e3;
|
|
2246
|
+
var CACHE_MAX_ENTRIES = 512;
|
|
2247
|
+
var MAX_INLINE_WRITE_BYTES = 128 * 1024;
|
|
2248
|
+
var metadataInvalidators = /* @__PURE__ */ new WeakMap();
|
|
2249
|
+
function toSandboxPath(relPath) {
|
|
2250
|
+
return validatePath(VERCEL_SANDBOX_REMOTE_ROOT, relPath);
|
|
2251
|
+
}
|
|
2252
|
+
function createTimedLruCache(ttlMs, maxEntries) {
|
|
2253
|
+
const entries = /* @__PURE__ */ new Map();
|
|
2254
|
+
return {
|
|
2255
|
+
get(key) {
|
|
2256
|
+
const now = Date.now();
|
|
2257
|
+
const entry = entries.get(key);
|
|
2258
|
+
if (!entry) return void 0;
|
|
2259
|
+
if (entry.expiresAt <= now) {
|
|
2260
|
+
entries.delete(key);
|
|
2261
|
+
return void 0;
|
|
2262
|
+
}
|
|
2263
|
+
entries.delete(key);
|
|
2264
|
+
entries.set(key, entry);
|
|
2265
|
+
return entry.value;
|
|
2266
|
+
},
|
|
2267
|
+
set(key, value) {
|
|
2268
|
+
entries.delete(key);
|
|
2269
|
+
entries.set(key, { value, expiresAt: Date.now() + ttlMs });
|
|
2270
|
+
while (entries.size > maxEntries) {
|
|
2271
|
+
const oldest = entries.keys().next().value;
|
|
2272
|
+
if (!oldest) return;
|
|
2273
|
+
entries.delete(oldest);
|
|
2274
|
+
}
|
|
2275
|
+
},
|
|
2276
|
+
clear() {
|
|
2277
|
+
entries.clear();
|
|
2278
|
+
}
|
|
2279
|
+
};
|
|
2280
|
+
}
|
|
2281
|
+
function cloneStat(stat7) {
|
|
2282
|
+
return {
|
|
2283
|
+
size: stat7.size,
|
|
2284
|
+
mtimeMs: stat7.mtimeMs,
|
|
2285
|
+
kind: stat7.kind
|
|
2286
|
+
};
|
|
2287
|
+
}
|
|
2288
|
+
function cloneEntries(entries) {
|
|
2289
|
+
return entries.map((entry) => ({ name: entry.name, kind: entry.kind }));
|
|
2290
|
+
}
|
|
2291
|
+
function shellQuote2(value) {
|
|
2292
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
2293
|
+
}
|
|
2294
|
+
async function runJson(sandbox, script) {
|
|
2295
|
+
const result = await sandbox.runCommand({ cmd: "sh", args: ["-c", script] });
|
|
2296
|
+
const [out, err] = await Promise.all([
|
|
2297
|
+
result.stdout?.() ?? Promise.resolve(""),
|
|
2298
|
+
result.stderr?.() ?? Promise.resolve("")
|
|
2299
|
+
]);
|
|
2300
|
+
if ((result.exitCode ?? 1) !== 0) {
|
|
2301
|
+
throw new Error(err || `sandbox command failed with exit code ${result.exitCode}`);
|
|
2302
|
+
}
|
|
2303
|
+
return JSON.parse(out);
|
|
2304
|
+
}
|
|
2305
|
+
async function runShell(sandbox, script) {
|
|
2306
|
+
const result = await sandbox.runCommand({ cmd: "sh", args: ["-c", script] });
|
|
2307
|
+
if ((result.exitCode ?? 1) !== 0) {
|
|
2308
|
+
const err = await (result.stderr?.() ?? Promise.resolve(""));
|
|
2309
|
+
throw new Error(err || `sandbox command failed with exit code ${result.exitCode}`);
|
|
2310
|
+
}
|
|
2311
|
+
}
|
|
2312
|
+
function registerMetadataInvalidator(sandbox, invalidate) {
|
|
1772
2313
|
const existing = metadataInvalidators.get(sandbox);
|
|
1773
2314
|
if (existing) {
|
|
1774
2315
|
existing.add(invalidate);
|
|
@@ -2128,7 +2669,7 @@ function createServerFileSearch(workspace, sandbox) {
|
|
|
2128
2669
|
|
|
2129
2670
|
// src/server/workspace/provision.ts
|
|
2130
2671
|
import { cp as cp2, mkdir as mkdir5, stat as stat4, writeFile as writeFile5 } from "fs/promises";
|
|
2131
|
-
import { dirname as
|
|
2672
|
+
import { dirname as dirname6, join as join4 } from "path";
|
|
2132
2673
|
var PROVISION_MARKER_REL_PATH = ".boring-agent/provisioned";
|
|
2133
2674
|
async function fileExists(targetPath) {
|
|
2134
2675
|
try {
|
|
@@ -2157,13 +2698,14 @@ async function copyTemplate(templatePath, workspaceRoot) {
|
|
|
2157
2698
|
{ cause: error }
|
|
2158
2699
|
);
|
|
2159
2700
|
}
|
|
2160
|
-
await mkdir5(
|
|
2701
|
+
await mkdir5(dirname6(markerPath), { recursive: true });
|
|
2161
2702
|
await writeFile5(markerPath, (/* @__PURE__ */ new Date()).toISOString(), "utf-8");
|
|
2162
2703
|
}
|
|
2163
2704
|
|
|
2164
2705
|
// src/server/runtime/modes/direct.ts
|
|
2165
2706
|
var directModeAdapter = {
|
|
2166
2707
|
id: "direct",
|
|
2708
|
+
workspaceFsCapability: "strong",
|
|
2167
2709
|
async create(ctx) {
|
|
2168
2710
|
await mkdir6(ctx.workspaceRoot, { recursive: true });
|
|
2169
2711
|
await copyTemplate(ctx.templatePath, ctx.workspaceRoot);
|
|
@@ -2182,6 +2724,7 @@ var directModeAdapter = {
|
|
|
2182
2724
|
import { mkdir as mkdir7 } from "fs/promises";
|
|
2183
2725
|
var localModeAdapter = {
|
|
2184
2726
|
id: "local",
|
|
2727
|
+
workspaceFsCapability: "strong",
|
|
2185
2728
|
async create(ctx) {
|
|
2186
2729
|
if (process.platform !== "linux") {
|
|
2187
2730
|
throw new Error("local mode requires Linux with bubblewrap");
|
|
@@ -2249,17 +2792,32 @@ function toRemotePath(value) {
|
|
|
2249
2792
|
}
|
|
2250
2793
|
return value;
|
|
2251
2794
|
}
|
|
2795
|
+
function escapeRegExp(value) {
|
|
2796
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2797
|
+
}
|
|
2798
|
+
var WORKSPACE_ALIAS_PREFIX_BOUNDARY = String.raw`(^|[\s"'=:;,\[({<>&|])`;
|
|
2799
|
+
var WORKSPACE_ALIAS_SUFFIX_BOUNDARY = String.raw`(?=/|$|[\s"'=:;,)\]}> &|])`;
|
|
2800
|
+
var WORKSPACE_ALIAS_PATTERN = new RegExp(
|
|
2801
|
+
`${WORKSPACE_ALIAS_PREFIX_BOUNDARY}${escapeRegExp(VERCEL_SANDBOX_WORKSPACE_ROOT)}${WORKSPACE_ALIAS_SUFFIX_BOUNDARY}`,
|
|
2802
|
+
"g"
|
|
2803
|
+
);
|
|
2804
|
+
function replaceWorkspaceAliases(value) {
|
|
2805
|
+
return value.replace(
|
|
2806
|
+
WORKSPACE_ALIAS_PATTERN,
|
|
2807
|
+
(_match, prefix) => `${prefix}${VERCEL_SANDBOX_REMOTE_ROOT}`
|
|
2808
|
+
);
|
|
2809
|
+
}
|
|
2252
2810
|
function toRemoteCwd(cwd) {
|
|
2253
2811
|
if (!cwd) return cwd;
|
|
2254
2812
|
return toRemotePath(cwd);
|
|
2255
2813
|
}
|
|
2256
2814
|
function toRemoteCommand(command) {
|
|
2257
|
-
return command
|
|
2815
|
+
return replaceWorkspaceAliases(command);
|
|
2258
2816
|
}
|
|
2259
2817
|
function toRemoteEnv(env) {
|
|
2260
2818
|
if (!env) return void 0;
|
|
2261
2819
|
return Object.fromEntries(
|
|
2262
|
-
Object.entries(env).map(([key, value]) => [key, toRemotePath(value)])
|
|
2820
|
+
Object.entries(env).map(([key, value]) => [key, replaceWorkspaceAliases(toRemotePath(value))])
|
|
2263
2821
|
);
|
|
2264
2822
|
}
|
|
2265
2823
|
function createVercelSandboxExec(sandbox, execOpts = {}) {
|
|
@@ -2347,7 +2905,7 @@ import { createGzip } from "zlib";
|
|
|
2347
2905
|
import { put } from "@vercel/blob";
|
|
2348
2906
|
var urlCache = /* @__PURE__ */ new Map();
|
|
2349
2907
|
var LOG_PREFIX = "[template-tarball]";
|
|
2350
|
-
function
|
|
2908
|
+
function log2(msg, meta = {}) {
|
|
2351
2909
|
const metaStr = Object.keys(meta).length > 0 ? ` ${JSON.stringify(meta)}` : "";
|
|
2352
2910
|
process.stderr.write(`${LOG_PREFIX} ${msg}${metaStr}
|
|
2353
2911
|
`);
|
|
@@ -2430,11 +2988,11 @@ async function packageTemplate(templatePath, opts = {}) {
|
|
|
2430
2988
|
const hash = computeTemplateHash(files);
|
|
2431
2989
|
const cached = urlCache.get(hash);
|
|
2432
2990
|
if (cached) {
|
|
2433
|
-
|
|
2991
|
+
log2("cache hit", { hash, fileCount: files.length });
|
|
2434
2992
|
return { url: cached, hash };
|
|
2435
2993
|
}
|
|
2436
2994
|
const tarball = await buildTarGz(files);
|
|
2437
|
-
|
|
2995
|
+
log2("tarball built", {
|
|
2438
2996
|
hash,
|
|
2439
2997
|
fileCount: files.length,
|
|
2440
2998
|
sizeBytes: tarball.length,
|
|
@@ -2443,7 +3001,7 @@ async function packageTemplate(templatePath, opts = {}) {
|
|
|
2443
3001
|
const upload = opts.uploadFn ?? defaultUpload;
|
|
2444
3002
|
const url = await upload(hash, tarball);
|
|
2445
3003
|
urlCache.set(hash, url);
|
|
2446
|
-
|
|
3004
|
+
log2("uploaded", { hash, url, totalMs: Date.now() - startMs });
|
|
2447
3005
|
return { url, hash };
|
|
2448
3006
|
}
|
|
2449
3007
|
|
|
@@ -2590,6 +3148,7 @@ function createVercelSandboxModeAdapter(opts = {}) {
|
|
|
2590
3148
|
const snapshotScheduler = opts.snapshotScheduler ?? null;
|
|
2591
3149
|
return {
|
|
2592
3150
|
id: "vercel-sandbox",
|
|
3151
|
+
workspaceFsCapability: "best-effort",
|
|
2593
3152
|
async dispose() {
|
|
2594
3153
|
await snapshotScheduler?.shutdown();
|
|
2595
3154
|
},
|
|
@@ -2669,149 +3228,70 @@ function createVercelSandboxModeAdapter(opts = {}) {
|
|
|
2669
3228
|
const sandbox = createVercelSandboxExec(sandboxHandle, {
|
|
2670
3229
|
onMutation: markDirty
|
|
2671
3230
|
});
|
|
2672
|
-
await sandbox.init?.({ workspace, sessionId: ctx.sessionId });
|
|
2673
|
-
return {
|
|
2674
|
-
workspace,
|
|
2675
|
-
sandbox,
|
|
2676
|
-
fileSearch: createServerFileSearch(workspace, sandbox)
|
|
2677
|
-
};
|
|
2678
|
-
}
|
|
2679
|
-
};
|
|
2680
|
-
}
|
|
2681
|
-
var vercelSandboxModeAdapter = createVercelSandboxModeAdapter();
|
|
2682
|
-
|
|
2683
|
-
// src/server/runtime/resolveMode.ts
|
|
2684
|
-
var MODE_ADAPTERS = {
|
|
2685
|
-
direct: directModeAdapter,
|
|
2686
|
-
local: localModeAdapter,
|
|
2687
|
-
"vercel-sandbox": vercelSandboxModeAdapter
|
|
2688
|
-
};
|
|
2689
|
-
function isRuntimeModeId(value) {
|
|
2690
|
-
return value === "direct" || value === "local" || value === "vercel-sandbox";
|
|
2691
|
-
}
|
|
2692
|
-
function hasBwrap() {
|
|
2693
|
-
const result = spawnSync("bwrap", ["--version"], { stdio: "ignore" });
|
|
2694
|
-
return !result.error && result.status === 0;
|
|
2695
|
-
}
|
|
2696
|
-
function autoDetectMode() {
|
|
2697
|
-
const explicitMode = getEnv("BORING_AGENT_MODE");
|
|
2698
|
-
if (explicitMode) {
|
|
2699
|
-
if (!isRuntimeModeId(explicitMode)) {
|
|
2700
|
-
throw new Error(
|
|
2701
|
-
`Invalid BORING_AGENT_MODE "${explicitMode}". Expected direct, local, or vercel-sandbox.`
|
|
2702
|
-
);
|
|
2703
|
-
}
|
|
2704
|
-
return explicitMode;
|
|
2705
|
-
}
|
|
2706
|
-
if (process.platform === "linux" && hasBwrap()) {
|
|
2707
|
-
return "local";
|
|
2708
|
-
}
|
|
2709
|
-
return "direct";
|
|
2710
|
-
}
|
|
2711
|
-
function resolveMode(mode = autoDetectMode()) {
|
|
2712
|
-
return MODE_ADAPTERS[mode];
|
|
2713
|
-
}
|
|
2714
|
-
|
|
2715
|
-
// src/server/createAgentApp.ts
|
|
2716
|
-
import Fastify from "fastify";
|
|
2717
|
-
import { basename as basename2 } from "path";
|
|
2718
|
-
|
|
2719
|
-
// src/server/harness/pi-coding-agent/createHarness.ts
|
|
2720
|
-
import { mkdir as mkdir9, writeFile as writeFile7 } from "fs/promises";
|
|
2721
|
-
import { extname, join as join6 } from "path";
|
|
2722
|
-
import {
|
|
2723
|
-
createAgentSession,
|
|
2724
|
-
SessionManager,
|
|
2725
|
-
AuthStorage,
|
|
2726
|
-
ModelRegistry,
|
|
2727
|
-
DefaultResourceLoader,
|
|
2728
|
-
SettingsManager,
|
|
2729
|
-
getAgentDir,
|
|
2730
|
-
loadSkills
|
|
2731
|
-
} from "@mariozechner/pi-coding-agent";
|
|
2732
|
-
|
|
2733
|
-
// src/server/logging.ts
|
|
2734
|
-
var SENSITIVE_KEYS = new Set([
|
|
2735
|
-
"apiKey",
|
|
2736
|
-
"api_key",
|
|
2737
|
-
"token",
|
|
2738
|
-
"secret",
|
|
2739
|
-
"password",
|
|
2740
|
-
"authorization",
|
|
2741
|
-
"cookie",
|
|
2742
|
-
"oidcToken",
|
|
2743
|
-
"accessToken",
|
|
2744
|
-
"refreshToken",
|
|
2745
|
-
"ANTHROPIC_API_KEY",
|
|
2746
|
-
"OPENAI_API_KEY",
|
|
2747
|
-
"VERCEL_OIDC_TOKEN",
|
|
2748
|
-
"VERCEL_TEAM_ID"
|
|
2749
|
-
].map((key) => key.toLowerCase()));
|
|
2750
|
-
function isSensitiveKey(key) {
|
|
2751
|
-
return SENSITIVE_KEYS.has(key.toLowerCase());
|
|
2752
|
-
}
|
|
2753
|
-
function redactValue(key, value, seen) {
|
|
2754
|
-
if (key && isSensitiveKey(key) && value != null) return "***";
|
|
2755
|
-
if (value == null || typeof value !== "object") return value;
|
|
2756
|
-
if (value instanceof Date) return value;
|
|
2757
|
-
if (seen.has(value)) return "[Circular]";
|
|
2758
|
-
seen.add(value);
|
|
2759
|
-
if (Array.isArray(value)) {
|
|
2760
|
-
const out2 = value.map((item) => redactValue(void 0, item, seen));
|
|
2761
|
-
seen.delete(value);
|
|
2762
|
-
return out2;
|
|
2763
|
-
}
|
|
2764
|
-
const out = {};
|
|
2765
|
-
for (const [childKey, childValue] of Object.entries(value)) {
|
|
2766
|
-
out[childKey] = redactValue(childKey, childValue, seen);
|
|
2767
|
-
}
|
|
2768
|
-
seen.delete(value);
|
|
2769
|
-
return out;
|
|
2770
|
-
}
|
|
2771
|
-
function redact(fields) {
|
|
2772
|
-
const out = {};
|
|
2773
|
-
const seen = /* @__PURE__ */ new WeakSet();
|
|
2774
|
-
for (const [k, v] of Object.entries(fields)) {
|
|
2775
|
-
out[k] = redactValue(k, v, seen);
|
|
2776
|
-
}
|
|
2777
|
-
return out;
|
|
2778
|
-
}
|
|
2779
|
-
var verbose = typeof process !== "undefined" && process.env?.BORING_AGENT_VERBOSE === "1";
|
|
2780
|
-
function createLogger(prefix) {
|
|
2781
|
-
function emit(level, msg, fields) {
|
|
2782
|
-
const entry = {
|
|
2783
|
-
level,
|
|
2784
|
-
prefix,
|
|
2785
|
-
msg,
|
|
2786
|
-
...fields ? redact(fields) : {},
|
|
2787
|
-
t: (/* @__PURE__ */ new Date()).toISOString()
|
|
2788
|
-
};
|
|
2789
|
-
if (level === "error") {
|
|
2790
|
-
console.error(JSON.stringify(entry));
|
|
2791
|
-
} else if (level === "warn") {
|
|
2792
|
-
console.warn(JSON.stringify(entry));
|
|
2793
|
-
} else {
|
|
2794
|
-
console.log(JSON.stringify(entry));
|
|
2795
|
-
}
|
|
2796
|
-
}
|
|
2797
|
-
return {
|
|
2798
|
-
debug(msg, fields) {
|
|
2799
|
-
if (verbose) emit("debug", msg, fields);
|
|
2800
|
-
},
|
|
2801
|
-
info(msg, fields) {
|
|
2802
|
-
emit("info", msg, fields);
|
|
2803
|
-
},
|
|
2804
|
-
warn(msg, fields) {
|
|
2805
|
-
emit("warn", msg, fields);
|
|
2806
|
-
},
|
|
2807
|
-
error(msg, fields) {
|
|
2808
|
-
emit("error", msg, fields);
|
|
3231
|
+
await sandbox.init?.({ workspace, sessionId: ctx.sessionId });
|
|
3232
|
+
return {
|
|
3233
|
+
workspace,
|
|
3234
|
+
sandbox,
|
|
3235
|
+
fileSearch: createServerFileSearch(workspace, sandbox)
|
|
3236
|
+
};
|
|
2809
3237
|
}
|
|
2810
3238
|
};
|
|
2811
3239
|
}
|
|
3240
|
+
var vercelSandboxModeAdapter = createVercelSandboxModeAdapter();
|
|
3241
|
+
|
|
3242
|
+
// src/server/runtime/resolveMode.ts
|
|
3243
|
+
var MODE_ADAPTERS = {
|
|
3244
|
+
direct: directModeAdapter,
|
|
3245
|
+
local: localModeAdapter,
|
|
3246
|
+
"vercel-sandbox": vercelSandboxModeAdapter
|
|
3247
|
+
};
|
|
3248
|
+
function isBuiltinRuntimeModeId(value) {
|
|
3249
|
+
return value === "direct" || value === "local" || value === "vercel-sandbox";
|
|
3250
|
+
}
|
|
3251
|
+
function hasBwrap() {
|
|
3252
|
+
const result = spawnSync("bwrap", ["--version"], { stdio: "ignore" });
|
|
3253
|
+
return !result.error && result.status === 0;
|
|
3254
|
+
}
|
|
3255
|
+
function autoDetectMode() {
|
|
3256
|
+
const explicitMode = getEnv("BORING_AGENT_MODE");
|
|
3257
|
+
if (explicitMode) {
|
|
3258
|
+
if (!isBuiltinRuntimeModeId(explicitMode)) {
|
|
3259
|
+
throw new Error(
|
|
3260
|
+
`Invalid BORING_AGENT_MODE "${explicitMode}". Expected direct, local, or vercel-sandbox.`
|
|
3261
|
+
);
|
|
3262
|
+
}
|
|
3263
|
+
return explicitMode;
|
|
3264
|
+
}
|
|
3265
|
+
if (process.platform === "linux" && hasBwrap()) {
|
|
3266
|
+
return "local";
|
|
3267
|
+
}
|
|
3268
|
+
return "direct";
|
|
3269
|
+
}
|
|
3270
|
+
function resolveMode(mode = autoDetectMode()) {
|
|
3271
|
+
if (isBuiltinRuntimeModeId(mode)) return MODE_ADAPTERS[mode];
|
|
3272
|
+
throw new Error(`Runtime mode "${mode}" has no built-in adapter. Pass runtimeModeAdapter to use a custom sandbox mode.`);
|
|
3273
|
+
}
|
|
3274
|
+
|
|
3275
|
+
// src/server/createAgentApp.ts
|
|
3276
|
+
import Fastify from "fastify";
|
|
3277
|
+
import { basename as basename2 } from "path";
|
|
3278
|
+
|
|
3279
|
+
// src/server/harness/pi-coding-agent/createHarness.ts
|
|
3280
|
+
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
3281
|
+
import { mkdir as mkdir9, writeFile as writeFile7 } from "fs/promises";
|
|
3282
|
+
import { extname as extname2, join as join6 } from "path";
|
|
3283
|
+
import {
|
|
3284
|
+
createAgentSession,
|
|
3285
|
+
SessionManager,
|
|
3286
|
+
AuthStorage,
|
|
3287
|
+
ModelRegistry,
|
|
3288
|
+
DefaultResourceLoader,
|
|
3289
|
+
SettingsManager as SettingsManager2,
|
|
3290
|
+
getAgentDir as getAgentDir2
|
|
3291
|
+
} from "@mariozechner/pi-coding-agent";
|
|
2812
3292
|
|
|
2813
3293
|
// src/server/harness/pi-coding-agent/tool-adapter.ts
|
|
2814
|
-
function adaptToolForPi(tool) {
|
|
3294
|
+
function adaptToolForPi(tool, sessionId) {
|
|
2815
3295
|
return {
|
|
2816
3296
|
name: tool.name,
|
|
2817
3297
|
label: tool.name,
|
|
@@ -2822,7 +3302,8 @@ function adaptToolForPi(tool) {
|
|
|
2822
3302
|
const result = await tool.execute(params, {
|
|
2823
3303
|
toolCallId,
|
|
2824
3304
|
abortSignal: signal ?? new AbortController().signal,
|
|
2825
|
-
onUpdate: onUpdate ? (partial) => onUpdate({ content: [{ type: "text", text: partial }], details: void 0 }) : void 0
|
|
3305
|
+
onUpdate: onUpdate ? (partial) => onUpdate({ content: [{ type: "text", text: partial }], details: void 0 }) : void 0,
|
|
3306
|
+
sessionId
|
|
2826
3307
|
});
|
|
2827
3308
|
if (result.isError) {
|
|
2828
3309
|
throw new Error(result.content.map((c) => c.text).join("\n"));
|
|
@@ -2834,8 +3315,8 @@ function adaptToolForPi(tool) {
|
|
|
2834
3315
|
}
|
|
2835
3316
|
};
|
|
2836
3317
|
}
|
|
2837
|
-
function adaptToolsForPi(tools) {
|
|
2838
|
-
return tools.map(adaptToolForPi);
|
|
3318
|
+
function adaptToolsForPi(tools, sessionId) {
|
|
3319
|
+
return tools.map((tool) => adaptToolForPi(tool, sessionId));
|
|
2839
3320
|
}
|
|
2840
3321
|
|
|
2841
3322
|
// src/server/harness/pi-coding-agent/stream-adapter.ts
|
|
@@ -3045,12 +3526,24 @@ function defaultSessionDir(cwd) {
|
|
|
3045
3526
|
return join5(homedir3(), ".pi", "agent", "sessions", safePath);
|
|
3046
3527
|
}
|
|
3047
3528
|
var SAFE_ID = /^[a-zA-Z0-9_-]+$/;
|
|
3529
|
+
var SAFE_SESSION_NAMESPACE = /^[a-zA-Z0-9_-]+$/;
|
|
3530
|
+
function sessionDirForNamespace(namespace) {
|
|
3531
|
+
const safeNamespace = namespace.trim();
|
|
3532
|
+
if (!SAFE_SESSION_NAMESPACE.test(safeNamespace)) {
|
|
3533
|
+
throw new Error("session namespace must contain only letters, numbers, underscores, and dashes");
|
|
3534
|
+
}
|
|
3535
|
+
return join5(homedir3(), ".pi", "agent", "sessions", safeNamespace);
|
|
3536
|
+
}
|
|
3048
3537
|
var PiSessionStore = class {
|
|
3049
3538
|
cwd;
|
|
3050
3539
|
sessionDir;
|
|
3051
|
-
constructor(cwd,
|
|
3540
|
+
constructor(cwd, options) {
|
|
3052
3541
|
this.cwd = cwd;
|
|
3053
|
-
|
|
3542
|
+
if (typeof options === "string") {
|
|
3543
|
+
this.sessionDir = options;
|
|
3544
|
+
return;
|
|
3545
|
+
}
|
|
3546
|
+
this.sessionDir = options?.sessionDir ?? (options?.sessionNamespace ? sessionDirForNamespace(options.sessionNamespace) : defaultSessionDir(cwd));
|
|
3054
3547
|
}
|
|
3055
3548
|
async list(ctx) {
|
|
3056
3549
|
const files = await readdir4(this.sessionDir).catch(() => []);
|
|
@@ -3486,7 +3979,7 @@ function createSessionTitleScheduler(opts) {
|
|
|
3486
3979
|
if (!detail || detail.turnCount !== 1) return;
|
|
3487
3980
|
if (hasCustomTitle(detail.title)) return;
|
|
3488
3981
|
const fallbackTitle = formatFallbackTitle(getNow());
|
|
3489
|
-
const apiKey = opts.getApiKey?.()?.trim() ||
|
|
3982
|
+
const apiKey = opts.getApiKey?.()?.trim() || "";
|
|
3490
3983
|
if (!apiKey || !fetchImpl) {
|
|
3491
3984
|
opts.writeTitle(input.sessionId, fallbackTitle);
|
|
3492
3985
|
return;
|
|
@@ -3514,6 +4007,7 @@ function createSessionTitleScheduler(opts) {
|
|
|
3514
4007
|
}
|
|
3515
4008
|
|
|
3516
4009
|
// src/server/models/modelConfig.ts
|
|
4010
|
+
import { getAgentDir, SettingsManager } from "@mariozechner/pi-coding-agent";
|
|
3517
4011
|
var INFOMANIAK_PROVIDER = "infomaniak";
|
|
3518
4012
|
var INFOMANIAK_API_BASE = "https://api.infomaniak.com";
|
|
3519
4013
|
var DEFAULT_CUSTOM_MODEL_MAX_TOKENS = 16384;
|
|
@@ -3642,6 +4136,16 @@ function registerConfiguredModelProviders(registry) {
|
|
|
3642
4136
|
}
|
|
3643
4137
|
return registered;
|
|
3644
4138
|
}
|
|
4139
|
+
function readPiSettingsDefaultModel() {
|
|
4140
|
+
try {
|
|
4141
|
+
const settings = SettingsManager.create(process.cwd(), getAgentDir());
|
|
4142
|
+
const provider = clean(settings.getDefaultProvider());
|
|
4143
|
+
const id = clean(settings.getDefaultModel());
|
|
4144
|
+
return provider && id ? { provider, id } : void 0;
|
|
4145
|
+
} catch {
|
|
4146
|
+
return void 0;
|
|
4147
|
+
}
|
|
4148
|
+
}
|
|
3645
4149
|
function readConfiguredDefaultModel() {
|
|
3646
4150
|
const encoded = clean(getEnv("BORING_AGENT_DEFAULT_MODEL"));
|
|
3647
4151
|
if (encoded) {
|
|
@@ -3655,7 +4159,7 @@ function readConfiguredDefaultModel() {
|
|
|
3655
4159
|
if (explicitProvider && explicitId) {
|
|
3656
4160
|
return { provider: explicitProvider, id: explicitId };
|
|
3657
4161
|
}
|
|
3658
|
-
return readInfomaniakProvider()?.model ?? readCustomProvider()?.model;
|
|
4162
|
+
return readPiSettingsDefaultModel() ?? readInfomaniakProvider()?.model ?? readCustomProvider()?.model;
|
|
3659
4163
|
}
|
|
3660
4164
|
|
|
3661
4165
|
// src/server/piPackages.ts
|
|
@@ -3701,6 +4205,17 @@ function mergePiPackageSources(base = [], additional = []) {
|
|
|
3701
4205
|
}
|
|
3702
4206
|
|
|
3703
4207
|
// src/server/harness/pi-coding-agent/createHarness.ts
|
|
4208
|
+
var WORKSPACE_PATHS_GUIDELINE = [
|
|
4209
|
+
"## Workspace paths",
|
|
4210
|
+
"",
|
|
4211
|
+
'- The "Current working directory" above is the workspace root. Tool path arguments must be relative to it (e.g. `README.md`, `src/foo.ts`).',
|
|
4212
|
+
"- Never pass an absolute path or a path that walks outside the workspace (no leading `/`, no `..` that escapes the root). The sandbox will reject it and the call is wasted.",
|
|
4213
|
+
"- For `find`/`grep`/`ls`: omit the `path` argument to search from the workspace root. Pass `path` only when you need to restrict to a subdirectory, and only as a workspace-relative path.",
|
|
4214
|
+
"- For `read`/`edit`/`write`: pass workspace-relative paths only."
|
|
4215
|
+
].join("\n");
|
|
4216
|
+
function composeSystemPromptAppend(hostAppend) {
|
|
4217
|
+
return [WORKSPACE_PATHS_GUIDELINE, hostAppend?.trim()].filter(Boolean).join("\n\n");
|
|
4218
|
+
}
|
|
3704
4219
|
function extractUserMessageText(message) {
|
|
3705
4220
|
const record = message;
|
|
3706
4221
|
if (record?.role !== "user") return "";
|
|
@@ -3729,6 +4244,16 @@ function findLastAssistantMessage(messages) {
|
|
|
3729
4244
|
}
|
|
3730
4245
|
return void 0;
|
|
3731
4246
|
}
|
|
4247
|
+
function extractAssistantReasoningTexts(message) {
|
|
4248
|
+
const content = message?.content;
|
|
4249
|
+
if (!Array.isArray(content)) return [];
|
|
4250
|
+
return content.flatMap((part) => {
|
|
4251
|
+
const item = part;
|
|
4252
|
+
if (item.type !== "thinking" && item.type !== "reasoning") return [];
|
|
4253
|
+
const text = item.thinking ?? item.text ?? item.content;
|
|
4254
|
+
return typeof text === "string" && text.length > 0 ? [text] : [];
|
|
4255
|
+
});
|
|
4256
|
+
}
|
|
3732
4257
|
function resolveRequestedModel(modelRegistry, input) {
|
|
3733
4258
|
const requestedId = input.model?.id;
|
|
3734
4259
|
if (!input.model || !requestedId) return void 0;
|
|
@@ -3748,27 +4273,37 @@ function resolveDefaultModel(modelRegistry) {
|
|
|
3748
4273
|
}
|
|
3749
4274
|
return void 0;
|
|
3750
4275
|
}
|
|
3751
|
-
function
|
|
3752
|
-
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
|
|
3760
|
-
packages: mergePiPackageSources(projectSettings.packages ?? [], piPackages)
|
|
4276
|
+
function readSettingsFileIfPresent(path4) {
|
|
4277
|
+
return existsSync(path4) ? readFileSync2(path4, "utf-8") : void 0;
|
|
4278
|
+
}
|
|
4279
|
+
function mergeInjectedProjectPackages(settingsJson, piPackages) {
|
|
4280
|
+
const settings = settingsJson ? JSON.parse(settingsJson) : {};
|
|
4281
|
+
const configuredPackages = Array.isArray(settings.packages) ? settings.packages : [];
|
|
4282
|
+
return JSON.stringify({
|
|
4283
|
+
...settings,
|
|
4284
|
+
packages: mergePiPackageSources(configuredPackages, piPackages)
|
|
3761
4285
|
});
|
|
4286
|
+
}
|
|
4287
|
+
function createResourceSettingsManager(cwd, agentDir, piPackages) {
|
|
4288
|
+
if (piPackages.length === 0) return SettingsManager2.create(cwd, agentDir);
|
|
4289
|
+
const globalSettingsPath = join6(agentDir, "settings.json");
|
|
4290
|
+
const projectSettingsPath = join6(cwd, ".pi", "settings.json");
|
|
4291
|
+
let globalSettingsOverrideJson;
|
|
4292
|
+
let projectSettingsOverrideJson;
|
|
3762
4293
|
const storage = {
|
|
3763
4294
|
withLock(scope, fn) {
|
|
3764
4295
|
if (scope === "global") {
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
4296
|
+
const current2 = globalSettingsOverrideJson ?? readSettingsFileIfPresent(globalSettingsPath);
|
|
4297
|
+
const next2 = fn(current2);
|
|
4298
|
+
if (next2 !== void 0) globalSettingsOverrideJson = next2;
|
|
4299
|
+
return;
|
|
3768
4300
|
}
|
|
4301
|
+
const current = projectSettingsOverrideJson ?? mergeInjectedProjectPackages(readSettingsFileIfPresent(projectSettingsPath), piPackages);
|
|
4302
|
+
const next = fn(current);
|
|
4303
|
+
if (next !== void 0) projectSettingsOverrideJson = next;
|
|
3769
4304
|
}
|
|
3770
4305
|
};
|
|
3771
|
-
return
|
|
4306
|
+
return SettingsManager2.fromStorage(storage);
|
|
3772
4307
|
}
|
|
3773
4308
|
async function applyRequestedSessionOptions(handle, input) {
|
|
3774
4309
|
const requestedModel = resolveRequestedModel(handle.modelRegistry, input);
|
|
@@ -3782,10 +4317,10 @@ async function applyRequestedSessionOptions(handle, input) {
|
|
|
3782
4317
|
handle.piSession.setThinkingLevel(input.thinkingLevel);
|
|
3783
4318
|
}
|
|
3784
4319
|
}
|
|
3785
|
-
var
|
|
4320
|
+
var log3 = createLogger("pi-harness");
|
|
3786
4321
|
var DEFAULT_ATTACHMENT_DIR = "assets/images";
|
|
3787
4322
|
function extForAttachment(filename, contentType) {
|
|
3788
|
-
const fromName =
|
|
4323
|
+
const fromName = extname2(filename).toLowerCase().replace(/^\./, "");
|
|
3789
4324
|
if (/^[a-z0-9]{1,12}$/.test(fromName)) return fromName;
|
|
3790
4325
|
if (contentType === "image/jpeg") return "jpg";
|
|
3791
4326
|
if (contentType === "image/png") return "png";
|
|
@@ -3801,7 +4336,10 @@ function basenameForAttachment(filename) {
|
|
|
3801
4336
|
return safe || "image";
|
|
3802
4337
|
}
|
|
3803
4338
|
function createPiCodingAgentHarness(opts) {
|
|
3804
|
-
const sessionStore = new PiSessionStore(opts.cwd
|
|
4339
|
+
const sessionStore = new PiSessionStore(opts.cwd, {
|
|
4340
|
+
sessionNamespace: opts.sessionNamespace,
|
|
4341
|
+
sessionDir: opts.sessionDir
|
|
4342
|
+
});
|
|
3805
4343
|
const piSessions = /* @__PURE__ */ new Map();
|
|
3806
4344
|
const scheduleSessionTitle = createSessionTitleScheduler({
|
|
3807
4345
|
loadSession: (sessionId) => sessionStore.load({ workspaceId: "default" }, sessionId),
|
|
@@ -3835,38 +4373,29 @@ function createPiCodingAgentHarness(opts) {
|
|
|
3835
4373
|
}
|
|
3836
4374
|
const resolvedModel = resolveRequestedModel(modelRegistry, input);
|
|
3837
4375
|
const model = resolvedModel ?? resolveDefaultModel(modelRegistry);
|
|
3838
|
-
const
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
skillsOverride: () => loadSkills({
|
|
3857
|
-
cwd: ctx.workdir,
|
|
3858
|
-
agentDir,
|
|
3859
|
-
skillPaths: additionalSkillPaths,
|
|
3860
|
-
includeDefaults: false
|
|
3861
|
-
})
|
|
3862
|
-
} : {}
|
|
3863
|
-
});
|
|
3864
|
-
})() : void 0;
|
|
4376
|
+
const composedSystemPromptAppend = composeSystemPromptAppend(opts.systemPromptAppend);
|
|
4377
|
+
const agentDir = getAgentDir2();
|
|
4378
|
+
const additionalSkillPaths = opts.resourceLoaderOptions?.additionalSkillPaths ?? [];
|
|
4379
|
+
const piPackages = opts.resourceLoaderOptions?.piPackages ?? [];
|
|
4380
|
+
const settingsManager = createResourceSettingsManager(
|
|
4381
|
+
ctx.workdir,
|
|
4382
|
+
agentDir,
|
|
4383
|
+
piPackages
|
|
4384
|
+
);
|
|
4385
|
+
const resourceLoader = new DefaultResourceLoader({
|
|
4386
|
+
cwd: ctx.workdir,
|
|
4387
|
+
agentDir,
|
|
4388
|
+
settingsManager,
|
|
4389
|
+
appendSystemPromptOverride: (base) => [...base, composedSystemPromptAppend],
|
|
4390
|
+
...opts.resourceLoaderOptions?.noContextFiles ? { noContextFiles: true } : {},
|
|
4391
|
+
...opts.resourceLoaderOptions?.noSkills ? { noSkills: true } : {},
|
|
4392
|
+
...additionalSkillPaths.length ? { additionalSkillPaths } : {}
|
|
4393
|
+
});
|
|
3865
4394
|
await resourceLoader?.reload();
|
|
3866
4395
|
const { session: piSession } = await createAgentSession({
|
|
3867
4396
|
cwd: ctx.workdir,
|
|
3868
4397
|
tools: [],
|
|
3869
|
-
customTools: adaptToolsForPi(opts.tools),
|
|
4398
|
+
customTools: adaptToolsForPi(opts.tools, input.sessionId),
|
|
3870
4399
|
model,
|
|
3871
4400
|
thinkingLevel: input.thinkingLevel ?? "off",
|
|
3872
4401
|
sessionManager,
|
|
@@ -3890,6 +4419,7 @@ function createPiCodingAgentHarness(opts) {
|
|
|
3890
4419
|
if (!handle) return;
|
|
3891
4420
|
handle.piSession.dispose();
|
|
3892
4421
|
piSessions.delete(sessionId);
|
|
4422
|
+
clearNativeFollowUpWork(sessionId);
|
|
3893
4423
|
}
|
|
3894
4424
|
const originalDelete = sessionStore.delete.bind(sessionStore);
|
|
3895
4425
|
sessionStore.delete = async (ctx, sessionId) => {
|
|
@@ -3897,17 +4427,95 @@ function createPiCodingAgentHarness(opts) {
|
|
|
3897
4427
|
disposePiSession(sessionId);
|
|
3898
4428
|
};
|
|
3899
4429
|
const nativeFollowUpPending = /* @__PURE__ */ new Set();
|
|
4430
|
+
const nativeFollowUpQueues = /* @__PURE__ */ new Map();
|
|
4431
|
+
function clearNativeFollowUpWork(sessionId) {
|
|
4432
|
+
nativeFollowUpPending.delete(sessionId);
|
|
4433
|
+
nativeFollowUpQueues.delete(sessionId);
|
|
4434
|
+
}
|
|
4435
|
+
function userMessageText(message) {
|
|
4436
|
+
const content = message.content;
|
|
4437
|
+
if (!Array.isArray(content)) return "";
|
|
4438
|
+
return content.map((part) => part.type === "text" && typeof part.text === "string" ? part.text : "").join("");
|
|
4439
|
+
}
|
|
4440
|
+
function removeFirstMatchingOrdinal(items, matches, ordinal) {
|
|
4441
|
+
let seen = 0;
|
|
4442
|
+
const index = items.findIndex((item) => {
|
|
4443
|
+
if (!matches(item)) return false;
|
|
4444
|
+
if (seen++ !== ordinal) return false;
|
|
4445
|
+
return true;
|
|
4446
|
+
});
|
|
4447
|
+
if (index >= 0) items.splice(index, 1);
|
|
4448
|
+
}
|
|
4449
|
+
function removePiQueuedFollowUp(piSession, text, textOrdinal = 0) {
|
|
4450
|
+
const session = piSession;
|
|
4451
|
+
if (!text) {
|
|
4452
|
+
session.agent?.clearFollowUpQueue?.();
|
|
4453
|
+
if (Array.isArray(session._followUpMessages)) session._followUpMessages = [];
|
|
4454
|
+
session._emitQueueUpdate?.();
|
|
4455
|
+
return;
|
|
4456
|
+
}
|
|
4457
|
+
const queuedMessages = session.agent?.followUpQueue?.messages;
|
|
4458
|
+
if (Array.isArray(queuedMessages)) {
|
|
4459
|
+
removeFirstMatchingOrdinal(queuedMessages, (message) => userMessageText(message) === text, textOrdinal);
|
|
4460
|
+
}
|
|
4461
|
+
if (Array.isArray(session._followUpMessages)) {
|
|
4462
|
+
removeFirstMatchingOrdinal(session._followUpMessages, (message) => message === text, textOrdinal);
|
|
4463
|
+
}
|
|
4464
|
+
session._emitQueueUpdate?.();
|
|
4465
|
+
}
|
|
4466
|
+
function hasFollowUpSelector(options) {
|
|
4467
|
+
return Boolean(options?.clientNonce) || options?.clientSeq !== void 0;
|
|
4468
|
+
}
|
|
4469
|
+
function matchesFollowUpSelector(item, options) {
|
|
4470
|
+
if (!hasFollowUpSelector(options)) return true;
|
|
4471
|
+
return Boolean(options?.clientNonce && item.clientNonce === options.clientNonce) || options?.clientSeq !== void 0 && item.clientSeq === options.clientSeq;
|
|
4472
|
+
}
|
|
4473
|
+
function removeNativeFollowUp(sessionId, options) {
|
|
4474
|
+
const queue = nativeFollowUpQueues.get(sessionId);
|
|
4475
|
+
if (!queue?.length) {
|
|
4476
|
+
if (!hasFollowUpSelector(options)) clearNativeFollowUpWork(sessionId);
|
|
4477
|
+
return [];
|
|
4478
|
+
}
|
|
4479
|
+
const removed = [];
|
|
4480
|
+
const next = [];
|
|
4481
|
+
const textCounts = /* @__PURE__ */ new Map();
|
|
4482
|
+
for (const request of queue) {
|
|
4483
|
+
const textOrdinal = textCounts.get(request.text) ?? 0;
|
|
4484
|
+
textCounts.set(request.text, textOrdinal + 1);
|
|
4485
|
+
if (matchesFollowUpSelector(request, options)) removed.push({ request, textOrdinal });
|
|
4486
|
+
else next.push(request);
|
|
4487
|
+
}
|
|
4488
|
+
if (next.length > 0) nativeFollowUpQueues.set(sessionId, next);
|
|
4489
|
+
else clearNativeFollowUpWork(sessionId);
|
|
4490
|
+
return removed;
|
|
4491
|
+
}
|
|
3900
4492
|
return {
|
|
3901
4493
|
id: "pi-coding-agent",
|
|
3902
4494
|
placement: "server",
|
|
3903
4495
|
sessions: sessionStore,
|
|
3904
|
-
async followUp(sessionId, text, _attachments,
|
|
4496
|
+
async followUp(sessionId, text, _attachments, displayText = text, options) {
|
|
3905
4497
|
const handle = piSessions.get(sessionId);
|
|
3906
|
-
if (!handle)
|
|
4498
|
+
if (!handle) throw new Error("followup_session_not_ready");
|
|
4499
|
+
const queue = nativeFollowUpQueues.get(sessionId) ?? [];
|
|
4500
|
+
queue.push({
|
|
4501
|
+
text,
|
|
4502
|
+
displayText,
|
|
4503
|
+
clientNonce: options?.clientNonce,
|
|
4504
|
+
clientSeq: options?.clientSeq
|
|
4505
|
+
});
|
|
4506
|
+
nativeFollowUpQueues.set(sessionId, queue);
|
|
3907
4507
|
nativeFollowUpPending.add(sessionId);
|
|
3908
4508
|
await handle.piSession.followUp(text);
|
|
3909
4509
|
},
|
|
3910
|
-
clearFollowUp(
|
|
4510
|
+
clearFollowUp(sessionId, options) {
|
|
4511
|
+
const handle = piSessions.get(sessionId);
|
|
4512
|
+
const removed = removeNativeFollowUp(sessionId, options);
|
|
4513
|
+
if (!handle) return;
|
|
4514
|
+
if (!options?.clientNonce && options?.clientSeq === void 0) {
|
|
4515
|
+
removePiQueuedFollowUp(handle.piSession);
|
|
4516
|
+
return;
|
|
4517
|
+
}
|
|
4518
|
+
for (const item of removed) removePiQueuedFollowUp(handle.piSession, item.request.text, item.textOrdinal);
|
|
3911
4519
|
},
|
|
3912
4520
|
/**
|
|
3913
4521
|
* Pi exposes the resolved system prompt as a getter on AgentSession.
|
|
@@ -3973,6 +4581,27 @@ function createPiCodingAgentHarness(opts) {
|
|
|
3973
4581
|
}
|
|
3974
4582
|
let sawTextChunk = false;
|
|
3975
4583
|
let inlineTurnIndex = 0;
|
|
4584
|
+
let currentPiAssistantMessageId = null;
|
|
4585
|
+
const messageIdsWithStreamedReasoning = /* @__PURE__ */ new Set();
|
|
4586
|
+
let piSeq = 0;
|
|
4587
|
+
const nextPiSeq = () => ++piSeq;
|
|
4588
|
+
const STANDARD_VISIBLE_CHUNK_TYPES = /* @__PURE__ */ new Set([
|
|
4589
|
+
"text-start",
|
|
4590
|
+
"text-delta",
|
|
4591
|
+
"text-end",
|
|
4592
|
+
"reasoning-start",
|
|
4593
|
+
"reasoning-delta",
|
|
4594
|
+
"reasoning-end",
|
|
4595
|
+
"tool-input-available",
|
|
4596
|
+
"tool-input-error",
|
|
4597
|
+
"tool-output-available",
|
|
4598
|
+
"tool-output-error"
|
|
4599
|
+
]);
|
|
4600
|
+
const standardToolInputsSeen = /* @__PURE__ */ new Set();
|
|
4601
|
+
function isStandardVisibleChunk(chunk2) {
|
|
4602
|
+
const type = chunk2.type;
|
|
4603
|
+
return typeof type === "string" && STANDARD_VISIBLE_CHUNK_TYPES.has(type);
|
|
4604
|
+
}
|
|
3976
4605
|
function namespaceInlinePartIds(input2) {
|
|
3977
4606
|
if (inlineTurnIndex === 0) return input2;
|
|
3978
4607
|
return input2.map((chunk2) => {
|
|
@@ -3983,6 +4612,23 @@ function createPiCodingAgentHarness(opts) {
|
|
|
3983
4612
|
return chunk2;
|
|
3984
4613
|
});
|
|
3985
4614
|
}
|
|
4615
|
+
function filterSdkChunksForCurrentSegment(input2) {
|
|
4616
|
+
const out = [];
|
|
4617
|
+
for (const chunk2 of input2) {
|
|
4618
|
+
const rec = chunk2;
|
|
4619
|
+
if (inlineTurnIndex > 0 && isStandardVisibleChunk(chunk2)) continue;
|
|
4620
|
+
if (rec.type === "tool-input-available" && rec.toolCallId) {
|
|
4621
|
+
standardToolInputsSeen.add(rec.toolCallId);
|
|
4622
|
+
out.push(chunk2);
|
|
4623
|
+
continue;
|
|
4624
|
+
}
|
|
4625
|
+
if ((rec.type === "tool-output-available" || rec.type === "tool-output-error" || rec.type === "tool-output-denied") && rec.toolCallId && !standardToolInputsSeen.has(rec.toolCallId)) {
|
|
4626
|
+
continue;
|
|
4627
|
+
}
|
|
4628
|
+
out.push(chunk2);
|
|
4629
|
+
}
|
|
4630
|
+
return out;
|
|
4631
|
+
}
|
|
3986
4632
|
let promptSettled = false;
|
|
3987
4633
|
let promptPromise = Promise.resolve();
|
|
3988
4634
|
const unsubscribe = piSession.subscribe((event) => {
|
|
@@ -4005,56 +4651,138 @@ function createPiCodingAgentHarness(opts) {
|
|
|
4005
4651
|
if (activeTools.size === 0) stopHeartbeat();
|
|
4006
4652
|
}
|
|
4007
4653
|
let converted;
|
|
4654
|
+
const piHistoryChunks = [];
|
|
4655
|
+
const eventMessage = event.message;
|
|
4656
|
+
if (event.type === "message_start" && (eventMessage?.role === "user" || eventMessage?.role === "assistant")) {
|
|
4657
|
+
const messageId = typeof eventMessage.id === "string" ? eventMessage.id : `${eventMessage.role}-${Date.now()}`;
|
|
4658
|
+
const role = eventMessage.role;
|
|
4659
|
+
if (role === "assistant") currentPiAssistantMessageId = messageId;
|
|
4660
|
+
const text = role === "user" ? extractUserMessageText(eventMessage) : void 0;
|
|
4661
|
+
if (role === "user" && text || role === "assistant") {
|
|
4662
|
+
piHistoryChunks.push({
|
|
4663
|
+
type: "data-pi-message-start",
|
|
4664
|
+
data: { seq: nextPiSeq(), messageId, role, ...text ? { text } : {} }
|
|
4665
|
+
});
|
|
4666
|
+
}
|
|
4667
|
+
}
|
|
4668
|
+
if (event.type === "message_update") {
|
|
4669
|
+
const ame = event.assistantMessageEvent;
|
|
4670
|
+
const messageId = typeof event.messageId === "string" ? event.messageId : typeof event.message?.id === "string" ? event.message.id : currentPiAssistantMessageId ?? "assistant-streaming";
|
|
4671
|
+
if (ame.type === "text_start") {
|
|
4672
|
+
piHistoryChunks.push({
|
|
4673
|
+
type: "data-pi-text-start",
|
|
4674
|
+
data: { seq: nextPiSeq(), messageId, partId: String(ame.contentIndex) }
|
|
4675
|
+
});
|
|
4676
|
+
} else if (ame.type === "text_delta" && ame.delta) {
|
|
4677
|
+
const seq = nextPiSeq();
|
|
4678
|
+
piHistoryChunks.push(
|
|
4679
|
+
{
|
|
4680
|
+
type: "data-pi-text-delta",
|
|
4681
|
+
data: { seq, messageId, partId: String(ame.contentIndex), delta: ame.delta }
|
|
4682
|
+
},
|
|
4683
|
+
// Back-compat for the current client projection. Remove once
|
|
4684
|
+
// ChatPanel consumes the stable data-pi-text-* DTOs directly.
|
|
4685
|
+
{
|
|
4686
|
+
type: "data-pi-message-delta",
|
|
4687
|
+
data: { seq, messageId, role: "assistant", delta: ame.delta }
|
|
4688
|
+
}
|
|
4689
|
+
);
|
|
4690
|
+
} else if (ame.type === "text_end") {
|
|
4691
|
+
piHistoryChunks.push({
|
|
4692
|
+
type: "data-pi-text-end",
|
|
4693
|
+
data: { seq: nextPiSeq(), messageId, partId: String(ame.contentIndex), ...typeof ame.content === "string" ? { text: ame.content } : {} }
|
|
4694
|
+
});
|
|
4695
|
+
} else if (ame.type === "thinking_start") {
|
|
4696
|
+
messageIdsWithStreamedReasoning.add(messageId);
|
|
4697
|
+
piHistoryChunks.push({
|
|
4698
|
+
type: "data-pi-reasoning-start",
|
|
4699
|
+
data: { seq: nextPiSeq(), messageId, partId: String(ame.contentIndex) }
|
|
4700
|
+
});
|
|
4701
|
+
} else if (ame.type === "thinking_delta") {
|
|
4702
|
+
messageIdsWithStreamedReasoning.add(messageId);
|
|
4703
|
+
piHistoryChunks.push({
|
|
4704
|
+
type: "data-pi-reasoning-delta",
|
|
4705
|
+
data: { seq: nextPiSeq(), messageId, partId: String(ame.contentIndex), delta: ame.delta }
|
|
4706
|
+
});
|
|
4707
|
+
} else if (ame.type === "thinking_end") {
|
|
4708
|
+
piHistoryChunks.push({
|
|
4709
|
+
type: "data-pi-reasoning-end",
|
|
4710
|
+
data: { seq: nextPiSeq(), messageId, partId: String(ame.contentIndex) }
|
|
4711
|
+
});
|
|
4712
|
+
} else if (ame.type === "toolcall_end") {
|
|
4713
|
+
piHistoryChunks.push({
|
|
4714
|
+
type: "data-pi-tool-call-end",
|
|
4715
|
+
data: { seq: nextPiSeq(), messageId, toolCallId: ame.toolCall.id, toolName: ame.toolCall.name, input: ame.toolCall.arguments }
|
|
4716
|
+
});
|
|
4717
|
+
}
|
|
4718
|
+
}
|
|
4719
|
+
if (event.type === "message_end" && (eventMessage?.role === "user" || eventMessage?.role === "assistant")) {
|
|
4720
|
+
const role = eventMessage.role;
|
|
4721
|
+
const messageId = typeof eventMessage.id === "string" ? eventMessage.id : role === "assistant" && currentPiAssistantMessageId ? currentPiAssistantMessageId : `${role}-${Date.now()}`;
|
|
4722
|
+
const text = role === "user" ? extractUserMessageText(eventMessage) : extractAssistantMessageText(eventMessage).text;
|
|
4723
|
+
if (role === "assistant" && !messageIdsWithStreamedReasoning.has(messageId)) {
|
|
4724
|
+
for (const reasoningText of extractAssistantReasoningTexts(eventMessage)) {
|
|
4725
|
+
const partId = `reasoning-${nextPiSeq()}`;
|
|
4726
|
+
piHistoryChunks.push(
|
|
4727
|
+
{ type: "data-pi-reasoning-start", data: { seq: nextPiSeq(), messageId, partId } },
|
|
4728
|
+
{ type: "data-pi-reasoning-delta", data: { seq: nextPiSeq(), messageId, partId, delta: reasoningText } },
|
|
4729
|
+
{ type: "data-pi-reasoning-end", data: { seq: nextPiSeq(), messageId, partId } }
|
|
4730
|
+
);
|
|
4731
|
+
}
|
|
4732
|
+
}
|
|
4733
|
+
piHistoryChunks.push({
|
|
4734
|
+
type: "data-pi-message-end",
|
|
4735
|
+
data: { seq: nextPiSeq(), messageId, role, ...text ? { text } : {} }
|
|
4736
|
+
});
|
|
4737
|
+
}
|
|
4738
|
+
if (event.type === "tool_execution_start" && currentPiAssistantMessageId) {
|
|
4739
|
+
piHistoryChunks.push({
|
|
4740
|
+
type: "data-pi-tool-call-end",
|
|
4741
|
+
data: { seq: nextPiSeq(), messageId: currentPiAssistantMessageId, toolCallId: event.toolCallId, toolName: event.toolName, input: event.args }
|
|
4742
|
+
});
|
|
4743
|
+
}
|
|
4744
|
+
if (event.type === "tool_execution_end" && currentPiAssistantMessageId) {
|
|
4745
|
+
piHistoryChunks.push({
|
|
4746
|
+
type: "data-pi-tool-result",
|
|
4747
|
+
data: { seq: nextPiSeq(), messageId: currentPiAssistantMessageId, toolCallId: event.toolCallId, output: event.result, isError: event.isError }
|
|
4748
|
+
});
|
|
4749
|
+
}
|
|
4008
4750
|
if (event.type === "message_start" && event.message?.role === "user") {
|
|
4009
4751
|
const text = extractUserMessageText(event.message);
|
|
4010
|
-
converted = text && text !== input.message ? [{ type: "data-followup-consumed", data: { text } }] :
|
|
4752
|
+
converted = text && text !== input.message ? [{ type: "data-followup-consumed", data: { text } }, ...piHistoryChunks] : piHistoryChunks;
|
|
4011
4753
|
if (text && text !== input.message) {
|
|
4012
4754
|
inlineTurnIndex += 1;
|
|
4013
|
-
|
|
4755
|
+
sawTextChunk = false;
|
|
4756
|
+
currentPiAssistantMessageId = null;
|
|
4757
|
+
const queue = nativeFollowUpQueues.get(input.sessionId);
|
|
4758
|
+
if (queue?.length) {
|
|
4759
|
+
const index = queue.findIndex((item) => item.text === text || item.displayText === text);
|
|
4760
|
+
if (index >= 0) queue.splice(index, 1);
|
|
4761
|
+
else queue.shift();
|
|
4762
|
+
if (queue.length > 0) nativeFollowUpQueues.set(input.sessionId, queue);
|
|
4763
|
+
else clearNativeFollowUpWork(input.sessionId);
|
|
4764
|
+
} else {
|
|
4765
|
+
nativeFollowUpPending.delete(input.sessionId);
|
|
4766
|
+
}
|
|
4014
4767
|
}
|
|
4015
4768
|
} else if (event.type === "message_end" && event.message?.role === "user") {
|
|
4016
|
-
converted =
|
|
4769
|
+
converted = piHistoryChunks;
|
|
4017
4770
|
} else {
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
const ame = event.assistantMessageEvent;
|
|
4022
|
-
if (ame.type === "text_end" && typeof ame.content === "string" && ame.content.length > 0 && !textDeltaSeen.has(ame.contentIndex)) {
|
|
4023
|
-
const id = inlineTurnIndex === 0 ? String(ame.contentIndex) : `turn-${inlineTurnIndex}:${ame.contentIndex}`;
|
|
4024
|
-
converted = [
|
|
4025
|
-
...textStartSeen.has(ame.contentIndex) ? [] : [{ type: "text-start", id }],
|
|
4026
|
-
{ type: "text-delta", id, delta: ame.content },
|
|
4027
|
-
...converted
|
|
4028
|
-
];
|
|
4029
|
-
textDeltaSeen.add(ame.contentIndex);
|
|
4030
|
-
assistantText += ame.content;
|
|
4031
|
-
}
|
|
4771
|
+
const sdkChunks = namespaceInlinePartIds(dedupStartChunks(piEventToChunks(event)));
|
|
4772
|
+
const sdkChunksForTurn = filterSdkChunksForCurrentSegment(sdkChunks);
|
|
4773
|
+
converted = [...piHistoryChunks, ...sdkChunksForTurn];
|
|
4032
4774
|
}
|
|
4033
4775
|
for (const chunk2 of converted) {
|
|
4034
4776
|
const t = chunk2.type;
|
|
4035
|
-
if (t === "text-delta") {
|
|
4777
|
+
if (t === "text-delta" || t === "data-pi-text-delta" || t === "data-pi-text-end") {
|
|
4036
4778
|
sawTextChunk = true;
|
|
4037
4779
|
}
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
if (event.type === "message_end" && !sawTextChunk) {
|
|
4041
|
-
const { role, text, errorText } = extractAssistantMessageText(
|
|
4042
|
-
event.message
|
|
4043
|
-
);
|
|
4044
|
-
if (role === "assistant" && errorText.length > 0) {
|
|
4045
|
-
chunks.push({ type: "error", errorText });
|
|
4046
|
-
sawTextChunk = true;
|
|
4047
|
-
} else if (role === "assistant" && text.length > 0) {
|
|
4048
|
-
const id = inlineTurnIndex === 0 ? "0" : `turn-${inlineTurnIndex}:0`;
|
|
4049
|
-
chunks.push(
|
|
4050
|
-
{ type: "text-start", id },
|
|
4051
|
-
{ type: "text-delta", id, delta: text },
|
|
4052
|
-
{ type: "text-end", id }
|
|
4053
|
-
);
|
|
4780
|
+
const piEndData = chunk2.data;
|
|
4781
|
+
if (t === "data-pi-message-end" && piEndData?.role === "assistant" && typeof piEndData.text === "string" && piEndData.text.length > 0) {
|
|
4054
4782
|
sawTextChunk = true;
|
|
4055
|
-
assistantText += text;
|
|
4056
4783
|
}
|
|
4057
4784
|
}
|
|
4785
|
+
chunks.push(...converted);
|
|
4058
4786
|
if (event.type === "agent_end") {
|
|
4059
4787
|
if (!sawTextChunk) {
|
|
4060
4788
|
const { role, text, errorText } = extractAssistantMessageText(
|
|
@@ -4066,11 +4794,10 @@ function createPiCodingAgentHarness(opts) {
|
|
|
4066
4794
|
chunks.push({ type: "error", errorText });
|
|
4067
4795
|
sawTextChunk = true;
|
|
4068
4796
|
} else if (role === "assistant" && text.length > 0) {
|
|
4069
|
-
const
|
|
4797
|
+
const messageId = currentPiAssistantMessageId ?? "assistant-streaming";
|
|
4070
4798
|
chunks.push(
|
|
4071
|
-
{ type: "
|
|
4072
|
-
{ type: "
|
|
4073
|
-
{ type: "text-end", id }
|
|
4799
|
+
{ type: "data-pi-message-start", data: { seq: nextPiSeq(), messageId, role } },
|
|
4800
|
+
{ type: "data-pi-message-end", data: { seq: nextPiSeq(), messageId, role, text } }
|
|
4074
4801
|
);
|
|
4075
4802
|
sawTextChunk = true;
|
|
4076
4803
|
assistantText += text;
|
|
@@ -4098,6 +4825,11 @@ function createPiCodingAgentHarness(opts) {
|
|
|
4098
4825
|
continue;
|
|
4099
4826
|
}
|
|
4100
4827
|
try {
|
|
4828
|
+
const estimatedSize = Math.ceil(b64.length * 0.75);
|
|
4829
|
+
if (estimatedSize > 10 * 1024 * 1024) {
|
|
4830
|
+
writeErrors.push(`${a.filename ?? "image"}: attachment exceeds 10 MB limit`);
|
|
4831
|
+
continue;
|
|
4832
|
+
}
|
|
4101
4833
|
const bytes = Buffer.from(b64, "base64");
|
|
4102
4834
|
const ext = extForAttachment(a.filename ?? "image", contentType);
|
|
4103
4835
|
const base = basenameForAttachment(a.filename ?? "image");
|
|
@@ -4108,7 +4840,7 @@ function createPiCodingAgentHarness(opts) {
|
|
|
4108
4840
|
savedPaths.push(relPath);
|
|
4109
4841
|
} catch (err) {
|
|
4110
4842
|
const msg = err instanceof Error ? err.message : String(err);
|
|
4111
|
-
|
|
4843
|
+
log3.error("attachment write failed", { workdir: ctx.workdir, error: msg });
|
|
4112
4844
|
writeErrors.push(`${a.filename ?? "image"}: ${msg}`);
|
|
4113
4845
|
}
|
|
4114
4846
|
}
|
|
@@ -4132,6 +4864,12 @@ ${attachmentNotes.join("\n")}` : message,
|
|
|
4132
4864
|
const prepared = await prepareTurn(message, attachments);
|
|
4133
4865
|
return piSession.prompt(prepared.message, prepared.promptOpts).then(() => {
|
|
4134
4866
|
promptSettled = true;
|
|
4867
|
+
if (!done && nativeFollowUpPending.has(input.sessionId)) {
|
|
4868
|
+
clearNativeFollowUpWork(input.sessionId);
|
|
4869
|
+
done = true;
|
|
4870
|
+
stopHeartbeat();
|
|
4871
|
+
if (wake) wake();
|
|
4872
|
+
}
|
|
4135
4873
|
}).catch((err) => {
|
|
4136
4874
|
promptSettled = true;
|
|
4137
4875
|
streamError = err;
|
|
@@ -4188,7 +4926,7 @@ ${attachmentNotes.join("\n")}` : message,
|
|
|
4188
4926
|
|
|
4189
4927
|
// src/server/harness/pi-coding-agent/pluginLoader.ts
|
|
4190
4928
|
import { readdir as readdir5, stat as stat5, readFile as readFile7 } from "fs/promises";
|
|
4191
|
-
import { join as join7, extname as
|
|
4929
|
+
import { join as join7, extname as extname3, resolve as resolve4, sep as sep3, relative as relative5 } from "path";
|
|
4192
4930
|
import { homedir as homedir4 } from "os";
|
|
4193
4931
|
import { pathToFileURL } from "url";
|
|
4194
4932
|
var VALID_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".mjs"]);
|
|
@@ -4214,7 +4952,7 @@ async function fileExists2(path4) {
|
|
|
4214
4952
|
async function discoverFromDir(dir, source) {
|
|
4215
4953
|
if (!await dirExists(dir)) return [];
|
|
4216
4954
|
const entries = await readdir5(dir);
|
|
4217
|
-
return entries.filter((e) => VALID_EXTENSIONS.has(
|
|
4955
|
+
return entries.filter((e) => VALID_EXTENSIONS.has(extname3(e))).map((e) => ({ path: join7(dir, e), source }));
|
|
4218
4956
|
}
|
|
4219
4957
|
function extractTools(mod) {
|
|
4220
4958
|
const tools = [];
|
|
@@ -4263,6 +5001,10 @@ async function loadNpmPlugin(pkgDir, importFn) {
|
|
|
4263
5001
|
if (resolvedMain !== resolvedPkgDir && !resolvedMain.startsWith(resolvedPkgDir + sep3)) {
|
|
4264
5002
|
return [];
|
|
4265
5003
|
}
|
|
5004
|
+
const relPath = relative5(resolvedPkgDir, resolvedMain);
|
|
5005
|
+
if (relPath.startsWith("..")) {
|
|
5006
|
+
return [];
|
|
5007
|
+
}
|
|
4266
5008
|
return loadModule(resolvedMain, importFn);
|
|
4267
5009
|
}
|
|
4268
5010
|
async function loadExtensionsJson(cwd) {
|
|
@@ -4344,7 +5086,7 @@ import {
|
|
|
4344
5086
|
// src/server/tools/operations/bound.ts
|
|
4345
5087
|
import { constants as constants3 } from "fs";
|
|
4346
5088
|
import { access as access3, lstat as lstat3, mkdir as mkdir10, readFile as readFile8, readdir as readdir6, readlink, realpath as realpath2, stat as stat6, writeFile as writeFile8 } from "fs/promises";
|
|
4347
|
-
import { dirname as
|
|
5089
|
+
import { dirname as dirname7, isAbsolute as isAbsolute4, relative as relative6, resolve as resolve5 } from "path";
|
|
4348
5090
|
function toPosixPath(value) {
|
|
4349
5091
|
return value.split("\\").join("/");
|
|
4350
5092
|
}
|
|
@@ -4401,7 +5143,7 @@ async function walkMatches(root, current, pattern, ignore, limit, out) {
|
|
|
4401
5143
|
for (const entry of entries) {
|
|
4402
5144
|
if (out.length >= limit) return;
|
|
4403
5145
|
const absolutePath = resolve5(current, entry.name);
|
|
4404
|
-
const relativePath = toPosixPath(
|
|
5146
|
+
const relativePath = toPosixPath(relative6(root, absolutePath));
|
|
4405
5147
|
if (entry.isDirectory() && shouldSkipDir(relativePath, ignore)) continue;
|
|
4406
5148
|
if (matchesGlob(relativePath, pattern)) {
|
|
4407
5149
|
out.push(absolutePath);
|
|
@@ -4418,7 +5160,7 @@ async function findNearestExistingAncestor(absPath) {
|
|
|
4418
5160
|
await stat6(current);
|
|
4419
5161
|
return current;
|
|
4420
5162
|
} catch {
|
|
4421
|
-
const parent =
|
|
5163
|
+
const parent = dirname7(current);
|
|
4422
5164
|
if (parent === current) return current;
|
|
4423
5165
|
current = parent;
|
|
4424
5166
|
}
|
|
@@ -4430,10 +5172,10 @@ async function assertWithinWorkspace(workspaceRoot, absPath) {
|
|
|
4430
5172
|
const s = await lstat3(absPath);
|
|
4431
5173
|
if (s.isSymbolicLink()) {
|
|
4432
5174
|
const target = await readlink(absPath);
|
|
4433
|
-
const resolvedTarget = resolve5(
|
|
5175
|
+
const resolvedTarget = resolve5(dirname7(absPath), target);
|
|
4434
5176
|
const nearestAncestor = await findNearestExistingAncestor(resolvedTarget);
|
|
4435
5177
|
const realAncestor = await realpath2(nearestAncestor);
|
|
4436
|
-
const rel2 =
|
|
5178
|
+
const rel2 = relative6(realRoot, realAncestor);
|
|
4437
5179
|
if (rel2.startsWith("..") || isAbsolute4(rel2)) {
|
|
4438
5180
|
throw new Error(`path "${absPath}" is outside workspace`);
|
|
4439
5181
|
}
|
|
@@ -4452,1140 +5194,695 @@ async function assertWithinWorkspace(workspaceRoot, absPath) {
|
|
|
4452
5194
|
} catch (err) {
|
|
4453
5195
|
const code = err.code;
|
|
4454
5196
|
if (code === "ENOENT") {
|
|
4455
|
-
const nearestAncestor = await findNearestExistingAncestor(
|
|
5197
|
+
const nearestAncestor = await findNearestExistingAncestor(dirname7(absPath));
|
|
4456
5198
|
const realAncestor = await realpath2(nearestAncestor);
|
|
4457
|
-
const rel2 =
|
|
5199
|
+
const rel2 = relative6(realRoot, realAncestor);
|
|
4458
5200
|
if (rel2.startsWith("..") || isAbsolute4(rel2)) {
|
|
4459
5201
|
throw new Error(`path "${absPath}" is outside workspace`);
|
|
4460
5202
|
}
|
|
4461
|
-
return;
|
|
4462
|
-
}
|
|
4463
|
-
throw err;
|
|
4464
|
-
}
|
|
4465
|
-
const rel = relative4(realRoot, realCandidate);
|
|
4466
|
-
if (rel.startsWith("..") || isAbsolute4(rel)) {
|
|
4467
|
-
throw new Error(`path "${absPath}" is outside workspace`);
|
|
4468
|
-
}
|
|
4469
|
-
}
|
|
4470
|
-
function boundFs(workspaceRoot) {
|
|
4471
|
-
const read = {
|
|
4472
|
-
async readFile(absolutePath) {
|
|
4473
|
-
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
4474
|
-
return await readFile8(absolutePath);
|
|
4475
|
-
},
|
|
4476
|
-
async access(absolutePath) {
|
|
4477
|
-
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
4478
|
-
await access3(absolutePath, constants3.R_OK);
|
|
4479
|
-
}
|
|
4480
|
-
};
|
|
4481
|
-
const write = {
|
|
4482
|
-
async writeFile(absolutePath, content) {
|
|
4483
|
-
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
4484
|
-
await mkdir10(dirname6(absolutePath), { recursive: true });
|
|
4485
|
-
await writeFile8(absolutePath, content);
|
|
4486
|
-
},
|
|
4487
|
-
async mkdir(dir) {
|
|
4488
|
-
await assertWithinWorkspace(workspaceRoot, dir);
|
|
4489
|
-
await mkdir10(dir, { recursive: true });
|
|
4490
|
-
}
|
|
4491
|
-
};
|
|
4492
|
-
const edit = {
|
|
4493
|
-
async readFile(absolutePath) {
|
|
4494
|
-
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
4495
|
-
return await readFile8(absolutePath);
|
|
4496
|
-
},
|
|
4497
|
-
async writeFile(absolutePath, content) {
|
|
4498
|
-
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
4499
|
-
await writeFile8(absolutePath, content);
|
|
4500
|
-
},
|
|
4501
|
-
async access(absolutePath) {
|
|
4502
|
-
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
4503
|
-
await access3(absolutePath, constants3.R_OK | constants3.W_OK);
|
|
4504
|
-
}
|
|
4505
|
-
};
|
|
4506
|
-
const find = {
|
|
4507
|
-
async exists(absolutePath) {
|
|
4508
|
-
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
4509
|
-
try {
|
|
4510
|
-
await stat6(absolutePath);
|
|
4511
|
-
return true;
|
|
4512
|
-
} catch (err) {
|
|
4513
|
-
if (err.code === "ENOENT") return false;
|
|
4514
|
-
throw err;
|
|
4515
|
-
}
|
|
4516
|
-
},
|
|
4517
|
-
async glob(pattern, cwd, options) {
|
|
4518
|
-
await assertWithinWorkspace(workspaceRoot, cwd);
|
|
4519
|
-
const matches = [];
|
|
4520
|
-
await walkMatches(cwd, cwd, pattern, options.ignore, options.limit, matches);
|
|
4521
|
-
return matches;
|
|
4522
|
-
}
|
|
4523
|
-
};
|
|
4524
|
-
const grep = {
|
|
4525
|
-
async isDirectory(absolutePath) {
|
|
4526
|
-
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
4527
|
-
return (await stat6(absolutePath)).isDirectory();
|
|
4528
|
-
},
|
|
4529
|
-
async readFile(absolutePath) {
|
|
4530
|
-
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
4531
|
-
return await readFile8(absolutePath, "utf8");
|
|
4532
|
-
}
|
|
4533
|
-
};
|
|
4534
|
-
const ls = {
|
|
4535
|
-
async exists(absolutePath) {
|
|
4536
|
-
try {
|
|
4537
|
-
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
4538
|
-
await stat6(absolutePath);
|
|
4539
|
-
return true;
|
|
4540
|
-
} catch {
|
|
4541
|
-
return false;
|
|
4542
|
-
}
|
|
4543
|
-
},
|
|
4544
|
-
async stat(absolutePath) {
|
|
4545
|
-
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
4546
|
-
return await stat6(absolutePath);
|
|
4547
|
-
},
|
|
4548
|
-
async readdir(absolutePath) {
|
|
4549
|
-
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
4550
|
-
return await readdir6(absolutePath);
|
|
4551
|
-
}
|
|
4552
|
-
};
|
|
4553
|
-
return { read, write, edit, find, grep, ls };
|
|
4554
|
-
}
|
|
4555
|
-
|
|
4556
|
-
// src/server/tools/operations/vercel.ts
|
|
4557
|
-
import { relative as relative5 } from "path";
|
|
4558
|
-
function rootAliases(workspace) {
|
|
4559
|
-
const aliases = [workspace.root];
|
|
4560
|
-
if (workspace.root === VERCEL_SANDBOX_WORKSPACE_ROOT) aliases.push(VERCEL_SANDBOX_REMOTE_ROOT);
|
|
4561
|
-
if (workspace.root === VERCEL_SANDBOX_REMOTE_ROOT) aliases.push(VERCEL_SANDBOX_WORKSPACE_ROOT);
|
|
4562
|
-
return aliases;
|
|
4563
|
-
}
|
|
4564
|
-
function toRelPath(workspace, absolutePath) {
|
|
4565
|
-
for (const root of rootAliases(workspace)) {
|
|
4566
|
-
const rel = relative5(root, absolutePath);
|
|
4567
|
-
if (!rel.startsWith("..") && !rel.startsWith("/")) return rel;
|
|
5203
|
+
return;
|
|
5204
|
+
}
|
|
5205
|
+
throw err;
|
|
4568
5206
|
}
|
|
4569
|
-
const
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
return `.agents/skills/${absolutePath.slice(skillIndex + skillMarker.length)}`;
|
|
5207
|
+
const rel = relative6(realRoot, realCandidate);
|
|
5208
|
+
if (rel.startsWith("..") || isAbsolute4(rel)) {
|
|
5209
|
+
throw new Error(`path "${absPath}" is outside workspace`);
|
|
4573
5210
|
}
|
|
4574
|
-
throw new Error(
|
|
4575
|
-
`path "${absolutePath}" is outside workspace; use a path relative to the workspace root or under ${rootAliases(workspace).join(" / ")}`
|
|
4576
|
-
);
|
|
4577
|
-
}
|
|
4578
|
-
function vercelBashOps(sandbox) {
|
|
4579
|
-
return {
|
|
4580
|
-
exec(command, cwd, { onData, signal, timeout, env }) {
|
|
4581
|
-
const filteredEnv = env ? Object.fromEntries(Object.entries(env).filter((e) => e[1] != null)) : void 0;
|
|
4582
|
-
return sandbox.exec(command, {
|
|
4583
|
-
cwd,
|
|
4584
|
-
env: filteredEnv,
|
|
4585
|
-
signal,
|
|
4586
|
-
timeoutMs: timeout ? timeout * 1e3 : void 0,
|
|
4587
|
-
onStdout: (chunk2) => onData(Buffer.from(chunk2)),
|
|
4588
|
-
onStderr: (chunk2) => onData(Buffer.from(chunk2))
|
|
4589
|
-
}).then((result) => ({ exitCode: result.exitCode }));
|
|
4590
|
-
}
|
|
4591
|
-
};
|
|
4592
5211
|
}
|
|
4593
|
-
function
|
|
4594
|
-
|
|
5212
|
+
function boundFs(workspaceRoot) {
|
|
5213
|
+
const read = {
|
|
4595
5214
|
async readFile(absolutePath) {
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
return Buffer.from(content, "utf-8");
|
|
5215
|
+
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
5216
|
+
return await readFile8(absolutePath);
|
|
4599
5217
|
},
|
|
4600
5218
|
async access(absolutePath) {
|
|
4601
|
-
|
|
4602
|
-
await
|
|
5219
|
+
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
5220
|
+
await access3(absolutePath, constants3.R_OK);
|
|
4603
5221
|
}
|
|
4604
5222
|
};
|
|
4605
|
-
|
|
4606
|
-
function vercelWriteOps(workspace) {
|
|
4607
|
-
return {
|
|
5223
|
+
const write = {
|
|
4608
5224
|
async writeFile(absolutePath, content) {
|
|
4609
|
-
|
|
4610
|
-
await
|
|
5225
|
+
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
5226
|
+
await mkdir10(dirname7(absolutePath), { recursive: true });
|
|
5227
|
+
await writeFile8(absolutePath, content);
|
|
4611
5228
|
},
|
|
4612
5229
|
async mkdir(dir) {
|
|
4613
|
-
|
|
4614
|
-
await
|
|
5230
|
+
await assertWithinWorkspace(workspaceRoot, dir);
|
|
5231
|
+
await mkdir10(dir, { recursive: true });
|
|
4615
5232
|
}
|
|
4616
5233
|
};
|
|
4617
|
-
|
|
4618
|
-
function vercelEditOps(workspace) {
|
|
4619
|
-
return {
|
|
5234
|
+
const edit = {
|
|
4620
5235
|
async readFile(absolutePath) {
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
return Buffer.from(content, "utf-8");
|
|
5236
|
+
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
5237
|
+
return await readFile8(absolutePath);
|
|
4624
5238
|
},
|
|
4625
5239
|
async writeFile(absolutePath, content) {
|
|
4626
|
-
|
|
4627
|
-
await
|
|
5240
|
+
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
5241
|
+
await writeFile8(absolutePath, content);
|
|
4628
5242
|
},
|
|
4629
5243
|
async access(absolutePath) {
|
|
4630
|
-
|
|
4631
|
-
await
|
|
4632
|
-
}
|
|
4633
|
-
};
|
|
4634
|
-
}
|
|
4635
|
-
function toRemotePath2(value) {
|
|
4636
|
-
if (value === VERCEL_SANDBOX_WORKSPACE_ROOT) return VERCEL_SANDBOX_REMOTE_ROOT;
|
|
4637
|
-
if (value.startsWith(`${VERCEL_SANDBOX_WORKSPACE_ROOT}/`)) {
|
|
4638
|
-
return `${VERCEL_SANDBOX_REMOTE_ROOT}${value.slice(VERCEL_SANDBOX_WORKSPACE_ROOT.length)}`;
|
|
4639
|
-
}
|
|
4640
|
-
return value;
|
|
4641
|
-
}
|
|
4642
|
-
function findPredicate(pattern) {
|
|
4643
|
-
const isPathShaped = pattern.includes("/") || pattern.includes("**");
|
|
4644
|
-
if (!isPathShaped) return `-name ${shellEscape(pattern)}`;
|
|
4645
|
-
let translated = pattern.replaceAll("**", "*").replace(/^\/+/, "");
|
|
4646
|
-
if (!translated.startsWith("*")) translated = `*${translated}`;
|
|
4647
|
-
return `-path ${shellEscape(translated)}`;
|
|
4648
|
-
}
|
|
4649
|
-
function findIgnoreArgs(cwd, ignore) {
|
|
4650
|
-
return ignore.map((pattern) => `! -path ${shellEscape(`${cwd}/${pattern.replace(/^\/+/, "")}/*`)}`).join(" ");
|
|
4651
|
-
}
|
|
4652
|
-
function fallbackFindCommand(pattern, cwd, options) {
|
|
4653
|
-
return [
|
|
4654
|
-
"find",
|
|
4655
|
-
shellEscape(cwd),
|
|
4656
|
-
"-maxdepth 20",
|
|
4657
|
-
"-type f",
|
|
4658
|
-
findIgnoreArgs(cwd, options.ignore),
|
|
4659
|
-
findPredicate(pattern),
|
|
4660
|
-
`| head -n ${Math.max(1, Math.trunc(options.limit))}`
|
|
4661
|
-
].filter(Boolean).join(" ");
|
|
4662
|
-
}
|
|
4663
|
-
function isFdMissing(result) {
|
|
4664
|
-
if (result.exitCode !== 127) return false;
|
|
4665
|
-
const stderr = Buffer.from(result.stderr).toString("utf-8");
|
|
4666
|
-
return /fd: not found|fd: command not found|not found/i.test(stderr);
|
|
4667
|
-
}
|
|
4668
|
-
function vercelFindOps(sandbox, workspace) {
|
|
4669
|
-
return {
|
|
4670
|
-
async exists(absolutePath) {
|
|
4671
|
-
if (workspace) {
|
|
4672
|
-
try {
|
|
4673
|
-
const rel = toRelPath(workspace, absolutePath);
|
|
4674
|
-
await workspace.stat(rel);
|
|
4675
|
-
return true;
|
|
4676
|
-
} catch {
|
|
4677
|
-
return false;
|
|
4678
|
-
}
|
|
4679
|
-
}
|
|
4680
|
-
const result = await sandbox.exec(`test -e ${shellEscape(absolutePath)}`, {
|
|
4681
|
-
timeoutMs: 5e3
|
|
4682
|
-
});
|
|
4683
|
-
return result.exitCode === 0;
|
|
4684
|
-
},
|
|
4685
|
-
async glob(pattern, cwd, options) {
|
|
4686
|
-
const remoteCwd = toRemotePath2(cwd);
|
|
4687
|
-
const args = ["fd", "--glob", "--no-require-git", "--max-results", String(options.limit)];
|
|
4688
|
-
for (const ig of options.ignore) {
|
|
4689
|
-
args.push("--exclude", ig);
|
|
4690
|
-
}
|
|
4691
|
-
args.push(pattern, remoteCwd);
|
|
4692
|
-
let result = await sandbox.exec(args.map(shellEscape).join(" "), {
|
|
4693
|
-
timeoutMs: 3e4,
|
|
4694
|
-
maxOutputBytes: 1048576
|
|
4695
|
-
});
|
|
4696
|
-
if (isFdMissing(result)) {
|
|
4697
|
-
result = await sandbox.exec(fallbackFindCommand(pattern, remoteCwd, options), {
|
|
4698
|
-
timeoutMs: 3e4,
|
|
4699
|
-
maxOutputBytes: 1048576
|
|
4700
|
-
});
|
|
4701
|
-
}
|
|
4702
|
-
if (result.exitCode !== 0 && result.exitCode !== 1) {
|
|
4703
|
-
const stderr = Buffer.from(result.stderr).toString("utf-8").trim();
|
|
4704
|
-
throw new Error(`file search failed (exit ${result.exitCode}): ${stderr}`);
|
|
4705
|
-
}
|
|
4706
|
-
const stdout = Buffer.from(result.stdout).toString("utf-8");
|
|
4707
|
-
return stdout.split("\n").filter(Boolean);
|
|
5244
|
+
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
5245
|
+
await access3(absolutePath, constants3.R_OK | constants3.W_OK);
|
|
4708
5246
|
}
|
|
4709
5247
|
};
|
|
4710
|
-
|
|
4711
|
-
function vercelLsOps(workspace) {
|
|
4712
|
-
return {
|
|
5248
|
+
const find = {
|
|
4713
5249
|
async exists(absolutePath) {
|
|
5250
|
+
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
4714
5251
|
try {
|
|
4715
|
-
|
|
4716
|
-
await workspace.stat(rel);
|
|
5252
|
+
await stat6(absolutePath);
|
|
4717
5253
|
return true;
|
|
4718
|
-
} catch {
|
|
4719
|
-
return false;
|
|
5254
|
+
} catch (err) {
|
|
5255
|
+
if (err.code === "ENOENT") return false;
|
|
5256
|
+
throw err;
|
|
4720
5257
|
}
|
|
4721
5258
|
},
|
|
4722
|
-
async
|
|
4723
|
-
|
|
4724
|
-
const
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
async readdir(absolutePath) {
|
|
4728
|
-
const rel = toRelPath(workspace, absolutePath);
|
|
4729
|
-
const entries = await workspace.readdir(rel);
|
|
4730
|
-
return entries.map((e) => e.name);
|
|
5259
|
+
async glob(pattern, cwd, options) {
|
|
5260
|
+
await assertWithinWorkspace(workspaceRoot, cwd);
|
|
5261
|
+
const matches = [];
|
|
5262
|
+
await walkMatches(cwd, cwd, pattern, options.ignore, options.limit, matches);
|
|
5263
|
+
return matches;
|
|
4731
5264
|
}
|
|
4732
5265
|
};
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
isError: true
|
|
4752
|
-
};
|
|
4753
|
-
}
|
|
4754
|
-
function bytesWritten(content) {
|
|
4755
|
-
return new TextEncoder().encode(content).byteLength;
|
|
4756
|
-
}
|
|
4757
|
-
var decoder = new TextDecoder("utf-8", { fatal: false });
|
|
4758
|
-
function decode(bytes) {
|
|
4759
|
-
return decoder.decode(bytes);
|
|
4760
|
-
}
|
|
4761
|
-
|
|
4762
|
-
// src/server/tools/vercelGrepTool.ts
|
|
4763
|
-
var PI_GREP_TOOL = createGrepToolDefinition("/");
|
|
4764
|
-
var DEFAULT_LIMIT2 = 100;
|
|
4765
|
-
var GREP_MAX_LINE_LENGTH = 500;
|
|
4766
|
-
var GREP_TIMEOUT_MS = 3e4;
|
|
4767
|
-
var GREP_MAX_OUTPUT_BYTES = 2097152;
|
|
4768
|
-
function quoteShell(value) {
|
|
4769
|
-
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
4770
|
-
}
|
|
4771
|
-
function numberParam(value, fallback, min = 1) {
|
|
4772
|
-
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
|
4773
|
-
return Math.max(min, Math.floor(value));
|
|
4774
|
-
}
|
|
4775
|
-
function optionalStringParam(value) {
|
|
4776
|
-
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
4777
|
-
}
|
|
4778
|
-
function buildRgCommand(params) {
|
|
4779
|
-
const args = ["--json", "--line-number", "--color=never", "--hidden"];
|
|
4780
|
-
if (params.ignoreCase === true) args.push("--ignore-case");
|
|
4781
|
-
if (params.literal === true) args.push("--fixed-strings");
|
|
4782
|
-
const glob = optionalStringParam(params.glob);
|
|
4783
|
-
if (glob) args.push("--glob", quoteShell(glob));
|
|
4784
|
-
const context = numberParam(params.context, 0, 0);
|
|
4785
|
-
if (context > 0) args.push("--context", String(context));
|
|
4786
|
-
const pattern = params.pattern;
|
|
4787
|
-
const searchPath = optionalStringParam(params.path) ?? ".";
|
|
4788
|
-
args.push("--", quoteShell(pattern), quoteShell(searchPath));
|
|
4789
|
-
return `rg ${args.join(" ")}`;
|
|
4790
|
-
}
|
|
4791
|
-
function parseRgJson(stdout, limit) {
|
|
4792
|
-
const outputLines = [];
|
|
4793
|
-
let matchCount = 0;
|
|
4794
|
-
let matchLimitReached = false;
|
|
4795
|
-
let linesTruncated = false;
|
|
4796
|
-
for (const rawLine of stdout.split("\n")) {
|
|
4797
|
-
if (rawLine.trim().length === 0) continue;
|
|
4798
|
-
let event;
|
|
4799
|
-
try {
|
|
4800
|
-
event = JSON.parse(rawLine);
|
|
4801
|
-
} catch {
|
|
4802
|
-
continue;
|
|
4803
|
-
}
|
|
4804
|
-
const filePath = event.data?.path?.text;
|
|
4805
|
-
const lineNumber = event.data?.line_number;
|
|
4806
|
-
const lineText = event.data?.lines?.text;
|
|
4807
|
-
if (!filePath || typeof lineNumber !== "number" || typeof lineText !== "string") {
|
|
4808
|
-
continue;
|
|
4809
|
-
}
|
|
4810
|
-
const isMatch = event.type === "match";
|
|
4811
|
-
if (isMatch) {
|
|
4812
|
-
matchCount += 1;
|
|
4813
|
-
if (matchCount > limit) {
|
|
4814
|
-
matchLimitReached = true;
|
|
4815
|
-
break;
|
|
5266
|
+
const grep = {
|
|
5267
|
+
async isDirectory(absolutePath) {
|
|
5268
|
+
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
5269
|
+
return (await stat6(absolutePath)).isDirectory();
|
|
5270
|
+
},
|
|
5271
|
+
async readFile(absolutePath) {
|
|
5272
|
+
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
5273
|
+
return await readFile8(absolutePath, "utf8");
|
|
5274
|
+
}
|
|
5275
|
+
};
|
|
5276
|
+
const ls = {
|
|
5277
|
+
async exists(absolutePath) {
|
|
5278
|
+
try {
|
|
5279
|
+
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
5280
|
+
await stat6(absolutePath);
|
|
5281
|
+
return true;
|
|
5282
|
+
} catch {
|
|
5283
|
+
return false;
|
|
4816
5284
|
}
|
|
4817
|
-
}
|
|
4818
|
-
|
|
5285
|
+
},
|
|
5286
|
+
async stat(absolutePath) {
|
|
5287
|
+
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
5288
|
+
return await stat6(absolutePath);
|
|
5289
|
+
},
|
|
5290
|
+
async readdir(absolutePath) {
|
|
5291
|
+
await assertWithinWorkspace(workspaceRoot, absolutePath);
|
|
5292
|
+
return await readdir6(absolutePath);
|
|
4819
5293
|
}
|
|
4820
|
-
const sanitized = lineText.replace(/\r\n/g, "\n").replace(/\r/g, "").replace(/\n$/, "");
|
|
4821
|
-
const { text, wasTruncated } = truncateLine(sanitized);
|
|
4822
|
-
if (wasTruncated) linesTruncated = true;
|
|
4823
|
-
const separator = isMatch ? ":" : "-";
|
|
4824
|
-
outputLines.push(`${filePath}${separator}${lineNumber}${separator} ${text}`);
|
|
4825
|
-
}
|
|
4826
|
-
return { outputLines, matchCount, matchLimitReached, linesTruncated };
|
|
4827
|
-
}
|
|
4828
|
-
function syntheticExecTruncation(output) {
|
|
4829
|
-
const outputLines = output.length === 0 ? 0 : output.split("\n").length;
|
|
4830
|
-
const outputBytes = bytesWritten(output);
|
|
4831
|
-
return {
|
|
4832
|
-
content: output,
|
|
4833
|
-
truncated: true,
|
|
4834
|
-
truncatedBy: "bytes",
|
|
4835
|
-
totalLines: outputLines,
|
|
4836
|
-
totalBytes: outputBytes,
|
|
4837
|
-
outputLines,
|
|
4838
|
-
outputBytes,
|
|
4839
|
-
lastLinePartial: false,
|
|
4840
|
-
firstLineExceedsLimit: false,
|
|
4841
|
-
maxLines: Number.MAX_SAFE_INTEGER,
|
|
4842
|
-
maxBytes: GREP_MAX_OUTPUT_BYTES
|
|
4843
5294
|
};
|
|
5295
|
+
return { read, write, edit, find, grep, ls };
|
|
4844
5296
|
}
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
);
|
|
4861
|
-
details.matchLimitReached = effectiveLimit;
|
|
4862
|
-
}
|
|
4863
|
-
if (truncation.truncated || sandboxTruncated) {
|
|
4864
|
-
const effectiveTruncation = truncation.truncated ? truncation : syntheticExecTruncation(output);
|
|
4865
|
-
notices.push(`${formatSize(effectiveTruncation.maxBytes)} limit reached`);
|
|
4866
|
-
details.truncation = effectiveTruncation;
|
|
5297
|
+
|
|
5298
|
+
// src/server/tools/operations/vercel.ts
|
|
5299
|
+
import { isAbsolute as isAbsolute5, relative as relative7 } from "path";
|
|
5300
|
+
function rootAliases(workspace) {
|
|
5301
|
+
const aliases = [workspace.root];
|
|
5302
|
+
if (workspace.root === VERCEL_SANDBOX_WORKSPACE_ROOT) aliases.push(VERCEL_SANDBOX_REMOTE_ROOT);
|
|
5303
|
+
if (workspace.root === VERCEL_SANDBOX_REMOTE_ROOT) aliases.push(VERCEL_SANDBOX_WORKSPACE_ROOT);
|
|
5304
|
+
return aliases;
|
|
5305
|
+
}
|
|
5306
|
+
function isOutsideWorkspaceRel(rel) {
|
|
5307
|
+
return rel === ".." || rel.startsWith("../") || rel.startsWith("..\\") || isAbsolute5(rel);
|
|
5308
|
+
}
|
|
5309
|
+
function toRelPath(workspace, absolutePath) {
|
|
5310
|
+
for (const root of rootAliases(workspace)) {
|
|
5311
|
+
const rel = relative7(root, absolutePath);
|
|
5312
|
+
if (!isOutsideWorkspaceRel(rel)) return rel;
|
|
4867
5313
|
}
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
);
|
|
4872
|
-
|
|
5314
|
+
const skillMarker = "/.agents/skills/";
|
|
5315
|
+
const skillIndex = absolutePath.indexOf(skillMarker);
|
|
5316
|
+
if (skillIndex >= 0) {
|
|
5317
|
+
const skillPath = absolutePath.slice(skillIndex + skillMarker.length);
|
|
5318
|
+
if (skillPath.includes("\0") || skillPath.split(/[\\/]+/).includes("..")) {
|
|
5319
|
+
throw new Error(`path "${absolutePath}" escapes the workspace skills directory`);
|
|
5320
|
+
}
|
|
5321
|
+
return `.agents/skills/${skillPath}`;
|
|
4873
5322
|
}
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
5323
|
+
throw new Error(
|
|
5324
|
+
`path "${absolutePath}" is outside workspace; use a path relative to the workspace root or under ${rootAliases(workspace).join(" / ")}`
|
|
5325
|
+
);
|
|
5326
|
+
}
|
|
5327
|
+
function vercelBashOps(sandbox) {
|
|
4877
5328
|
return {
|
|
4878
|
-
|
|
4879
|
-
|
|
5329
|
+
exec(command, cwd, { onData, signal, timeout, env }) {
|
|
5330
|
+
const filteredEnv = env ? Object.fromEntries(Object.entries(env).filter((e) => e[1] != null)) : void 0;
|
|
5331
|
+
return sandbox.exec(command, {
|
|
5332
|
+
cwd,
|
|
5333
|
+
env: filteredEnv,
|
|
5334
|
+
signal,
|
|
5335
|
+
timeoutMs: timeout ? timeout * 1e3 : void 0,
|
|
5336
|
+
onStdout: (chunk2) => onData(Buffer.from(chunk2)),
|
|
5337
|
+
onStderr: (chunk2) => onData(Buffer.from(chunk2))
|
|
5338
|
+
}).then((result) => ({ exitCode: result.exitCode }));
|
|
5339
|
+
}
|
|
4880
5340
|
};
|
|
4881
5341
|
}
|
|
4882
|
-
function
|
|
5342
|
+
function vercelReadOps(workspace) {
|
|
4883
5343
|
return {
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
}
|
|
4893
|
-
if (typeof params.pattern !== "string" || params.pattern.length === 0) {
|
|
4894
|
-
return makeError("pattern is required");
|
|
4895
|
-
}
|
|
4896
|
-
if (workspaceRoot && typeof params.path === "string" && params.path.length > 0) {
|
|
4897
|
-
const resolved = resolve6(workspaceRoot, params.path);
|
|
4898
|
-
const rel = relative6(workspaceRoot, resolved);
|
|
4899
|
-
if (rel.startsWith("..") || rel.startsWith("/")) {
|
|
4900
|
-
return makeError(`path "${params.path}" is outside workspace`);
|
|
4901
|
-
}
|
|
4902
|
-
}
|
|
4903
|
-
try {
|
|
4904
|
-
const result = await sandbox.exec(buildRgCommand(params), {
|
|
4905
|
-
signal: ctx.abortSignal,
|
|
4906
|
-
timeoutMs: GREP_TIMEOUT_MS,
|
|
4907
|
-
maxOutputBytes: GREP_MAX_OUTPUT_BYTES
|
|
4908
|
-
});
|
|
4909
|
-
if (result.exitCode !== 0 && result.exitCode !== 1) {
|
|
4910
|
-
const stderr = decode(result.stderr).trim();
|
|
4911
|
-
const message = stderr || `ripgrep exited with code ${result.exitCode}`;
|
|
4912
|
-
return makeError(`grep failed: ${message}`);
|
|
4913
|
-
}
|
|
4914
|
-
const limit = numberParam(params.limit, DEFAULT_LIMIT2);
|
|
4915
|
-
const parsed = parseRgJson(decode(result.stdout), limit);
|
|
4916
|
-
return buildSuccessResult(parsed, limit, result.truncated);
|
|
4917
|
-
} catch (error) {
|
|
4918
|
-
const message = error instanceof Error ? error.message : "unknown error";
|
|
4919
|
-
return makeError(`grep failed: ${message}`);
|
|
4920
|
-
}
|
|
5344
|
+
async readFile(absolutePath) {
|
|
5345
|
+
const rel = toRelPath(workspace, absolutePath);
|
|
5346
|
+
const content = await workspace.readFile(rel);
|
|
5347
|
+
return Buffer.from(content, "utf-8");
|
|
5348
|
+
},
|
|
5349
|
+
async access(absolutePath) {
|
|
5350
|
+
const rel = toRelPath(workspace, absolutePath);
|
|
5351
|
+
await workspace.stat(rel);
|
|
4921
5352
|
}
|
|
4922
5353
|
};
|
|
4923
5354
|
}
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
5355
|
+
function vercelWriteOps(workspace) {
|
|
5356
|
+
return {
|
|
5357
|
+
async writeFile(absolutePath, content) {
|
|
5358
|
+
const rel = toRelPath(workspace, absolutePath);
|
|
5359
|
+
await workspace.writeFile(rel, content);
|
|
5360
|
+
},
|
|
5361
|
+
async mkdir(dir) {
|
|
5362
|
+
const rel = toRelPath(workspace, dir);
|
|
5363
|
+
await workspace.mkdir(rel, { recursive: true });
|
|
5364
|
+
}
|
|
5365
|
+
};
|
|
4928
5366
|
}
|
|
4929
|
-
function
|
|
5367
|
+
function vercelEditOps(workspace) {
|
|
4930
5368
|
return {
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
} : void 0,
|
|
4944
|
-
{}
|
|
4945
|
-
);
|
|
4946
|
-
const textContent = (result.content ?? []).filter(isTextContent).map((c) => ({ type: "text", text: c.text }));
|
|
4947
|
-
return {
|
|
4948
|
-
content: textContent.length > 0 ? textContent : [{ type: "text", text: "" }],
|
|
4949
|
-
isError: false,
|
|
4950
|
-
details: result.details
|
|
4951
|
-
};
|
|
5369
|
+
async readFile(absolutePath) {
|
|
5370
|
+
const rel = toRelPath(workspace, absolutePath);
|
|
5371
|
+
const content = await workspace.readFile(rel);
|
|
5372
|
+
return Buffer.from(content, "utf-8");
|
|
5373
|
+
},
|
|
5374
|
+
async writeFile(absolutePath, content) {
|
|
5375
|
+
const rel = toRelPath(workspace, absolutePath);
|
|
5376
|
+
await workspace.writeFile(rel, content);
|
|
5377
|
+
},
|
|
5378
|
+
async access(absolutePath) {
|
|
5379
|
+
const rel = toRelPath(workspace, absolutePath);
|
|
5380
|
+
await workspace.stat(rel);
|
|
4952
5381
|
}
|
|
4953
5382
|
};
|
|
4954
5383
|
}
|
|
4955
|
-
function
|
|
4956
|
-
|
|
4957
|
-
if (
|
|
4958
|
-
return
|
|
4959
|
-
adaptPiTool(createReadToolDefinition(cwd, { operations: vercelReadOps(bundle.workspace) })),
|
|
4960
|
-
adaptPiTool(createWriteToolDefinition(cwd, { operations: vercelWriteOps(bundle.workspace) })),
|
|
4961
|
-
adaptPiTool(createEditToolDefinition(cwd, { operations: vercelEditOps(bundle.workspace) })),
|
|
4962
|
-
adaptPiTool(createFindToolDefinition(cwd, { operations: vercelFindOps(bundle.sandbox, bundle.workspace) })),
|
|
4963
|
-
vercelGrepTool(bundle.sandbox, cwd),
|
|
4964
|
-
adaptPiTool(createLsToolDefinition(cwd, { operations: vercelLsOps(bundle.workspace) }))
|
|
4965
|
-
];
|
|
5384
|
+
function toRemotePath2(value) {
|
|
5385
|
+
if (value === VERCEL_SANDBOX_WORKSPACE_ROOT) return VERCEL_SANDBOX_REMOTE_ROOT;
|
|
5386
|
+
if (value.startsWith(`${VERCEL_SANDBOX_WORKSPACE_ROOT}/`)) {
|
|
5387
|
+
return `${VERCEL_SANDBOX_REMOTE_ROOT}${value.slice(VERCEL_SANDBOX_WORKSPACE_ROOT.length)}`;
|
|
4966
5388
|
}
|
|
4967
|
-
|
|
4968
|
-
return [
|
|
4969
|
-
adaptPiTool(createReadToolDefinition(cwd, { operations: ops.read })),
|
|
4970
|
-
adaptPiTool(createWriteToolDefinition(cwd, { operations: ops.write })),
|
|
4971
|
-
adaptPiTool(createEditToolDefinition(cwd, { operations: ops.edit })),
|
|
4972
|
-
adaptPiTool(createFindToolDefinition(cwd, { operations: ops.find })),
|
|
4973
|
-
adaptPiTool(createGrepToolDefinition2(cwd, { operations: ops.grep })),
|
|
4974
|
-
adaptPiTool(createLsToolDefinition(cwd, { operations: ops.ls }))
|
|
4975
|
-
];
|
|
4976
|
-
}
|
|
4977
|
-
|
|
4978
|
-
// src/server/tools/harness/index.ts
|
|
4979
|
-
import {
|
|
4980
|
-
createBashToolDefinition,
|
|
4981
|
-
createLocalBashOperations
|
|
4982
|
-
} from "@mariozechner/pi-coding-agent";
|
|
4983
|
-
function shellEscape2(s) {
|
|
4984
|
-
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
5389
|
+
return value;
|
|
4985
5390
|
}
|
|
4986
|
-
function
|
|
4987
|
-
const
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
env: withWorkspacePythonEnv({
|
|
4993
|
-
workspaceRoot,
|
|
4994
|
-
env: context.env,
|
|
4995
|
-
sandboxRoot: "/workspace"
|
|
4996
|
-
})
|
|
4997
|
-
});
|
|
5391
|
+
function findPredicate(pattern) {
|
|
5392
|
+
const isPathShaped = pattern.includes("/") || pattern.includes("**");
|
|
5393
|
+
if (!isPathShaped) return `-name ${shellEscape(pattern)}`;
|
|
5394
|
+
let translated = pattern.replaceAll("**", "*").replace(/^\/+/, "");
|
|
5395
|
+
if (!translated.startsWith("*")) translated = `*${translated}`;
|
|
5396
|
+
return `-path ${shellEscape(translated)}`;
|
|
4998
5397
|
}
|
|
4999
|
-
function
|
|
5000
|
-
return (
|
|
5001
|
-
...context,
|
|
5002
|
-
env: withWorkspacePythonEnv({ workspaceRoot, env: context.env })
|
|
5003
|
-
});
|
|
5398
|
+
function findIgnoreArgs(cwd, ignore) {
|
|
5399
|
+
return ignore.map((pattern) => `! -path ${shellEscape(`${cwd}/${pattern.replace(/^\/+/, "")}/*`)}`).join(" ");
|
|
5004
5400
|
}
|
|
5005
|
-
function
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
return {
|
|
5016
|
-
operations: createLocalBashOperations(),
|
|
5017
|
-
spawnHook: directSpawnHook(bundle.workspace.root)
|
|
5018
|
-
};
|
|
5019
|
-
}
|
|
5401
|
+
function fallbackFindCommand(pattern, cwd, options) {
|
|
5402
|
+
return [
|
|
5403
|
+
"find",
|
|
5404
|
+
shellEscape(cwd),
|
|
5405
|
+
"-maxdepth 20",
|
|
5406
|
+
"-type f",
|
|
5407
|
+
findIgnoreArgs(cwd, options.ignore),
|
|
5408
|
+
findPredicate(pattern),
|
|
5409
|
+
`| head -n ${Math.max(1, Math.trunc(options.limit))}`
|
|
5410
|
+
].filter(Boolean).join(" ");
|
|
5020
5411
|
}
|
|
5021
|
-
function
|
|
5022
|
-
return
|
|
5023
|
-
|
|
5024
|
-
|
|
5025
|
-
promptSnippet: piTool.promptSnippet,
|
|
5026
|
-
parameters: piTool.parameters,
|
|
5027
|
-
async execute(params, ctx) {
|
|
5028
|
-
const result = await piTool.execute(
|
|
5029
|
-
ctx.toolCallId,
|
|
5030
|
-
params,
|
|
5031
|
-
ctx.abortSignal,
|
|
5032
|
-
ctx.onUpdate ? (update) => {
|
|
5033
|
-
const text = update.content.filter((c) => c.type === "text").map((c) => c.text).join("");
|
|
5034
|
-
ctx.onUpdate(text);
|
|
5035
|
-
} : void 0,
|
|
5036
|
-
{}
|
|
5037
|
-
);
|
|
5038
|
-
const textContent = (result.content ?? []).filter((c) => c.type === "text").map((c) => ({ type: "text", text: c.text }));
|
|
5039
|
-
return {
|
|
5040
|
-
content: textContent.length > 0 ? textContent : [{ type: "text", text: "" }],
|
|
5041
|
-
isError: false,
|
|
5042
|
-
details: result.details
|
|
5043
|
-
};
|
|
5044
|
-
}
|
|
5045
|
-
};
|
|
5412
|
+
function isFdMissing(result) {
|
|
5413
|
+
if (result.exitCode !== 127) return false;
|
|
5414
|
+
const stderr = Buffer.from(result.stderr).toString("utf-8");
|
|
5415
|
+
return /\bfd: (?:not found|command not found)\b/i.test(stderr);
|
|
5046
5416
|
}
|
|
5047
|
-
function
|
|
5417
|
+
function vercelFindOps(sandbox, workspace) {
|
|
5048
5418
|
return {
|
|
5049
|
-
|
|
5050
|
-
|
|
5051
|
-
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
5055
|
-
|
|
5056
|
-
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5062
|
-
|
|
5419
|
+
async exists(absolutePath) {
|
|
5420
|
+
if (workspace) {
|
|
5421
|
+
try {
|
|
5422
|
+
const rel = toRelPath(workspace, absolutePath);
|
|
5423
|
+
await workspace.stat(rel);
|
|
5424
|
+
return true;
|
|
5425
|
+
} catch {
|
|
5426
|
+
return false;
|
|
5427
|
+
}
|
|
5428
|
+
}
|
|
5429
|
+
const result = await sandbox.exec(`test -e ${shellEscape(absolutePath)}`, {
|
|
5430
|
+
timeoutMs: 5e3
|
|
5431
|
+
});
|
|
5432
|
+
return result.exitCode === 0;
|
|
5063
5433
|
},
|
|
5064
|
-
async
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
};
|
|
5434
|
+
async glob(pattern, cwd, options) {
|
|
5435
|
+
const remoteCwd = toRemotePath2(cwd);
|
|
5436
|
+
const args = ["fd", "--glob", "--no-require-git", "--max-results", String(options.limit)];
|
|
5437
|
+
for (const ig of options.ignore) {
|
|
5438
|
+
args.push("--exclude", ig);
|
|
5070
5439
|
}
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5440
|
+
args.push(pattern, remoteCwd);
|
|
5441
|
+
let result = await sandbox.exec(args.map(shellEscape).join(" "), {
|
|
5442
|
+
timeoutMs: 3e4,
|
|
5443
|
+
maxOutputBytes: 1048576
|
|
5444
|
+
});
|
|
5445
|
+
if (isFdMissing(result)) {
|
|
5446
|
+
result = await sandbox.exec(fallbackFindCommand(pattern, remoteCwd, options), {
|
|
5447
|
+
timeoutMs: 3e4,
|
|
5448
|
+
maxOutputBytes: 1048576
|
|
5449
|
+
});
|
|
5075
5450
|
}
|
|
5076
|
-
if (
|
|
5077
|
-
|
|
5451
|
+
if (result.exitCode !== 0 && result.exitCode !== 1) {
|
|
5452
|
+
const stderr = Buffer.from(result.stderr).toString("utf-8").trim();
|
|
5453
|
+
throw new Error(`file search failed (exit ${result.exitCode}): ${stderr}`);
|
|
5078
5454
|
}
|
|
5455
|
+
const stdout = Buffer.from(result.stdout).toString("utf-8");
|
|
5456
|
+
return stdout.split("\n").filter(Boolean);
|
|
5457
|
+
}
|
|
5458
|
+
};
|
|
5459
|
+
}
|
|
5460
|
+
function vercelLsOps(workspace) {
|
|
5461
|
+
return {
|
|
5462
|
+
async exists(absolutePath) {
|
|
5079
5463
|
try {
|
|
5080
|
-
const
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
sandboxId: typeof input.sandboxId === "string" ? input.sandboxId : void 0,
|
|
5086
|
-
vmSize: input.vmSize === "xxs" || input.vmSize === "xs" || input.vmSize === "s" || input.vmSize === "m" || input.vmSize === "l" ? input.vmSize : void 0
|
|
5087
|
-
});
|
|
5088
|
-
return {
|
|
5089
|
-
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
5090
|
-
isError: result.exitCode !== 0,
|
|
5091
|
-
details: result
|
|
5092
|
-
};
|
|
5093
|
-
} catch (error) {
|
|
5094
|
-
return {
|
|
5095
|
-
content: [{ type: "text", text: error instanceof Error ? error.message : "execute_isolated_code failed" }],
|
|
5096
|
-
isError: true
|
|
5097
|
-
};
|
|
5464
|
+
const rel = toRelPath(workspace, absolutePath);
|
|
5465
|
+
await workspace.stat(rel);
|
|
5466
|
+
return true;
|
|
5467
|
+
} catch {
|
|
5468
|
+
return false;
|
|
5098
5469
|
}
|
|
5470
|
+
},
|
|
5471
|
+
async stat(absolutePath) {
|
|
5472
|
+
const rel = toRelPath(workspace, absolutePath);
|
|
5473
|
+
const s = await workspace.stat(rel);
|
|
5474
|
+
return { isDirectory: () => s.kind === "dir" };
|
|
5475
|
+
},
|
|
5476
|
+
async readdir(absolutePath) {
|
|
5477
|
+
const rel = toRelPath(workspace, absolutePath);
|
|
5478
|
+
const entries = await workspace.readdir(rel);
|
|
5479
|
+
return entries.map((e) => e.name);
|
|
5099
5480
|
}
|
|
5100
5481
|
};
|
|
5101
5482
|
}
|
|
5102
|
-
function
|
|
5103
|
-
|
|
5104
|
-
adaptPiTool2(createBashToolDefinition(bundle.workspace.root, bashOptionsForMode(bundle)))
|
|
5105
|
-
];
|
|
5106
|
-
if (bundle.sandbox.capabilities.includes("isolated-code")) {
|
|
5107
|
-
tools.push(createExecuteIsolatedCodeTool(bundle.sandbox));
|
|
5108
|
-
}
|
|
5109
|
-
return tools;
|
|
5483
|
+
function shellEscape(arg) {
|
|
5484
|
+
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
5110
5485
|
}
|
|
5111
5486
|
|
|
5112
|
-
// src/server/
|
|
5113
|
-
|
|
5114
|
-
|
|
5115
|
-
|
|
5116
|
-
|
|
5117
|
-
|
|
5118
|
-
|
|
5119
|
-
|
|
5120
|
-
|
|
5121
|
-
|
|
5122
|
-
|
|
5123
|
-
var ERROR_CODE_NOT_IMPLEMENTED = "not_implemented";
|
|
5124
|
-
var DEV_MODE_WARNING = "No auth token set \u2014 running in dev mode";
|
|
5125
|
-
var DEFAULT_WORKSPACE_ID = "default";
|
|
5126
|
-
function errorPayload(code, message, field) {
|
|
5487
|
+
// src/server/tools/vercelGrepTool.ts
|
|
5488
|
+
import {
|
|
5489
|
+
createGrepToolDefinition,
|
|
5490
|
+
formatSize,
|
|
5491
|
+
truncateHead,
|
|
5492
|
+
truncateLine
|
|
5493
|
+
} from "@mariozechner/pi-coding-agent";
|
|
5494
|
+
import { resolve as resolve6, relative as relative8 } from "path";
|
|
5495
|
+
|
|
5496
|
+
// src/server/catalog/tools/_shared.ts
|
|
5497
|
+
function makeError(message) {
|
|
5127
5498
|
return {
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5499
|
+
content: [{ type: "text", text: message }],
|
|
5500
|
+
isError: true
|
|
5501
|
+
};
|
|
5502
|
+
}
|
|
5503
|
+
function bytesWritten(content) {
|
|
5504
|
+
return new TextEncoder().encode(content).byteLength;
|
|
5505
|
+
}
|
|
5506
|
+
var decoder = new TextDecoder("utf-8", { fatal: false });
|
|
5507
|
+
function decode(bytes) {
|
|
5508
|
+
return decoder.decode(bytes);
|
|
5509
|
+
}
|
|
5510
|
+
|
|
5511
|
+
// src/server/tools/vercelGrepTool.ts
|
|
5512
|
+
var PI_GREP_TOOL = createGrepToolDefinition("/");
|
|
5513
|
+
var DEFAULT_LIMIT2 = 100;
|
|
5514
|
+
var GREP_MAX_LINE_LENGTH = 500;
|
|
5515
|
+
var GREP_TIMEOUT_MS = 3e4;
|
|
5516
|
+
var GREP_MAX_OUTPUT_BYTES = 2097152;
|
|
5517
|
+
function quoteShell(value) {
|
|
5518
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
5519
|
+
}
|
|
5520
|
+
function numberParam(value, fallback, min = 1) {
|
|
5521
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
|
5522
|
+
return Math.max(min, Math.floor(value));
|
|
5523
|
+
}
|
|
5524
|
+
function optionalStringParam(value) {
|
|
5525
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
5526
|
+
}
|
|
5527
|
+
function buildRgCommand(params) {
|
|
5528
|
+
const args = ["--json", "--line-number", "--color=never", "--hidden"];
|
|
5529
|
+
if (params.ignoreCase === true) args.push("--ignore-case");
|
|
5530
|
+
if (params.literal === true) args.push("--fixed-strings");
|
|
5531
|
+
const glob = optionalStringParam(params.glob);
|
|
5532
|
+
if (glob) args.push("--glob", quoteShell(glob));
|
|
5533
|
+
const context = numberParam(params.context, 0, 0);
|
|
5534
|
+
if (context > 0) args.push("--context", String(context));
|
|
5535
|
+
const pattern = params.pattern;
|
|
5536
|
+
const searchPath = optionalStringParam(params.path) ?? ".";
|
|
5537
|
+
args.push("--", quoteShell(pattern), quoteShell(searchPath));
|
|
5538
|
+
return `rg ${args.join(" ")}`;
|
|
5539
|
+
}
|
|
5540
|
+
function parseRgJson(stdout, limit) {
|
|
5541
|
+
const outputLines = [];
|
|
5542
|
+
let matchCount = 0;
|
|
5543
|
+
let matchLimitReached = false;
|
|
5544
|
+
let linesTruncated = false;
|
|
5545
|
+
for (const rawLine of stdout.split("\n")) {
|
|
5546
|
+
if (rawLine.trim().length === 0) continue;
|
|
5547
|
+
let event;
|
|
5548
|
+
try {
|
|
5549
|
+
event = JSON.parse(rawLine);
|
|
5550
|
+
} catch {
|
|
5551
|
+
continue;
|
|
5132
5552
|
}
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
authenticated
|
|
5139
|
-
};
|
|
5140
|
-
}
|
|
5141
|
-
function createAuthMiddleware(opts = {}) {
|
|
5142
|
-
let warnedDevMode = false;
|
|
5143
|
-
return async function authMiddleware(request, reply) {
|
|
5144
|
-
const workspaceId = opts.workspaceId ?? DEFAULT_WORKSPACE_ID;
|
|
5145
|
-
const authToken = opts.authToken?.trim() || void 0;
|
|
5146
|
-
ensureWorkspaceContext(request, workspaceId, false);
|
|
5147
|
-
if (opts.publicPaths?.includes(request.url.split("?")[0])) {
|
|
5148
|
-
return;
|
|
5553
|
+
const filePath = event.data?.path?.text;
|
|
5554
|
+
const lineNumber = event.data?.line_number;
|
|
5555
|
+
const lineText = event.data?.lines?.text;
|
|
5556
|
+
if (!filePath || typeof lineNumber !== "number" || typeof lineText !== "string") {
|
|
5557
|
+
continue;
|
|
5149
5558
|
}
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
5559
|
+
const isMatch = event.type === "match";
|
|
5560
|
+
if (isMatch) {
|
|
5561
|
+
matchCount += 1;
|
|
5562
|
+
if (matchCount > limit) {
|
|
5563
|
+
matchLimitReached = true;
|
|
5564
|
+
break;
|
|
5155
5565
|
}
|
|
5156
|
-
|
|
5157
|
-
|
|
5158
|
-
const header = request.headers.authorization;
|
|
5159
|
-
if (typeof header !== "string" || !header.startsWith("Bearer ")) {
|
|
5160
|
-
reply.code(401).send(
|
|
5161
|
-
errorPayload(
|
|
5162
|
-
ERROR_CODE_AUTH_REQUIRED,
|
|
5163
|
-
"Missing Bearer token"
|
|
5164
|
-
)
|
|
5165
|
-
);
|
|
5166
|
-
return;
|
|
5167
|
-
}
|
|
5168
|
-
const candidateToken = header.slice("Bearer ".length);
|
|
5169
|
-
if (candidateToken !== authToken) {
|
|
5170
|
-
reply.code(403).send(errorPayload(ERROR_CODE_AUTH_INVALID, "Invalid token"));
|
|
5171
|
-
return;
|
|
5566
|
+
} else if (event.type !== "context") {
|
|
5567
|
+
continue;
|
|
5172
5568
|
}
|
|
5173
|
-
|
|
5174
|
-
|
|
5569
|
+
const sanitized = lineText.replace(/\r\n/g, "\n").replace(/\r/g, "").replace(/\n$/, "");
|
|
5570
|
+
const { text, wasTruncated } = truncateLine(sanitized);
|
|
5571
|
+
if (wasTruncated) linesTruncated = true;
|
|
5572
|
+
const separator = isMatch ? ":" : "-";
|
|
5573
|
+
outputLines.push(`${filePath}${separator}${lineNumber}${separator} ${text}`);
|
|
5574
|
+
}
|
|
5575
|
+
return { outputLines, matchCount, matchLimitReached, linesTruncated };
|
|
5175
5576
|
}
|
|
5176
|
-
function
|
|
5177
|
-
|
|
5178
|
-
|
|
5179
|
-
|
|
5180
|
-
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
request.body = parsed.data;
|
|
5577
|
+
function syntheticExecTruncation(output) {
|
|
5578
|
+
const outputLines = output.length === 0 ? 0 : output.split("\n").length;
|
|
5579
|
+
const outputBytes = bytesWritten(output);
|
|
5580
|
+
return {
|
|
5581
|
+
content: output,
|
|
5582
|
+
truncated: true,
|
|
5583
|
+
truncatedBy: "bytes",
|
|
5584
|
+
totalLines: outputLines,
|
|
5585
|
+
totalBytes: outputBytes,
|
|
5586
|
+
outputLines,
|
|
5587
|
+
outputBytes,
|
|
5588
|
+
lastLinePartial: false,
|
|
5589
|
+
firstLineExceedsLimit: false,
|
|
5590
|
+
maxLines: Number.MAX_SAFE_INTEGER,
|
|
5591
|
+
maxBytes: GREP_MAX_OUTPUT_BYTES
|
|
5192
5592
|
};
|
|
5193
5593
|
}
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
function healthRoutes(app, opts, done) {
|
|
5197
|
-
const startTime = Date.now();
|
|
5198
|
-
app.get("/health", async () => {
|
|
5594
|
+
function buildSuccessResult(parsed, effectiveLimit, sandboxTruncated) {
|
|
5595
|
+
if (parsed.matchCount === 0) {
|
|
5199
5596
|
return {
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
uptime: Math.floor((Date.now() - startTime) / 1e3)
|
|
5597
|
+
content: [{ type: "text", text: "No matches found" }],
|
|
5598
|
+
details: void 0
|
|
5203
5599
|
};
|
|
5204
|
-
});
|
|
5205
|
-
app.get("/ready", async (_request, reply) => {
|
|
5206
|
-
const state = opts.getReadiness();
|
|
5207
|
-
if (state.degradedReason) {
|
|
5208
|
-
return reply.code(503).send({
|
|
5209
|
-
status: "degraded",
|
|
5210
|
-
reason: state.degradedReason
|
|
5211
|
-
});
|
|
5212
|
-
}
|
|
5213
|
-
if (!state.sandboxReady || !state.harnessReady) {
|
|
5214
|
-
return reply.code(503).send({
|
|
5215
|
-
status: "provisioning",
|
|
5216
|
-
retryAfter: 2
|
|
5217
|
-
});
|
|
5218
|
-
}
|
|
5219
|
-
return { status: "ready" };
|
|
5220
|
-
});
|
|
5221
|
-
done();
|
|
5222
|
-
}
|
|
5223
|
-
|
|
5224
|
-
// src/server/http/routes/file.ts
|
|
5225
|
-
import { dirname as dirname7, extname as extname3, relative as relative7 } from "path/posix";
|
|
5226
|
-
var BORING_SETTINGS_PATH = ".boring/settings";
|
|
5227
|
-
var DEFAULT_MARKDOWN_IMAGE_UPLOAD_DIR = "assets/images";
|
|
5228
|
-
var MAX_UPLOAD_BYTES = 10 * 1024 * 1024;
|
|
5229
|
-
function defaultWorkspaceSettings() {
|
|
5230
|
-
return { markdown: { imageUploadDir: DEFAULT_MARKDOWN_IMAGE_UPLOAD_DIR } };
|
|
5231
|
-
}
|
|
5232
|
-
function isPathValidationError(err) {
|
|
5233
|
-
return err instanceof Error && typeof err.reason === "string";
|
|
5234
|
-
}
|
|
5235
|
-
function classifyError(err, reply, subject) {
|
|
5236
|
-
if (isPathValidationError(err)) {
|
|
5237
|
-
return reply.code(403).send({
|
|
5238
|
-
error: { code: ERROR_CODE_PATH_REJECTED, message: "path traversal rejected" }
|
|
5239
|
-
});
|
|
5240
|
-
}
|
|
5241
|
-
const message = err instanceof Error ? err.message : "unknown error";
|
|
5242
|
-
const code = err?.code;
|
|
5243
|
-
if (code === "EPERM" || message.includes("traversal") || message.includes("EPERM")) {
|
|
5244
|
-
return reply.code(403).send({
|
|
5245
|
-
error: { code: ERROR_CODE_PATH_REJECTED, message: "path traversal rejected" }
|
|
5246
|
-
});
|
|
5247
|
-
}
|
|
5248
|
-
if (code === "ENOENT" || message.includes("ENOENT")) {
|
|
5249
|
-
return reply.code(404).send({
|
|
5250
|
-
error: { code: ERROR_CODE_NOT_FOUND, message: `${subject} not found` }
|
|
5251
|
-
});
|
|
5252
5600
|
}
|
|
5253
|
-
|
|
5254
|
-
|
|
5255
|
-
|
|
5256
|
-
|
|
5601
|
+
const rawOutput = parsed.outputLines.join("\n");
|
|
5602
|
+
const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
|
|
5603
|
+
let output = truncation.content;
|
|
5604
|
+
const details = {};
|
|
5605
|
+
const notices = [];
|
|
5606
|
+
if (parsed.matchLimitReached) {
|
|
5607
|
+
notices.push(
|
|
5608
|
+
`${effectiveLimit} matches limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`
|
|
5609
|
+
);
|
|
5610
|
+
details.matchLimitReached = effectiveLimit;
|
|
5257
5611
|
}
|
|
5258
|
-
|
|
5259
|
-
|
|
5260
|
-
|
|
5261
|
-
|
|
5262
|
-
function requireStringParam(value, field, reply) {
|
|
5263
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
5264
|
-
reply.code(400).send({
|
|
5265
|
-
error: { code: ERROR_CODE_VALIDATION_ERROR, message: `${field} is required`, field }
|
|
5266
|
-
});
|
|
5267
|
-
return null;
|
|
5612
|
+
if (truncation.truncated || sandboxTruncated) {
|
|
5613
|
+
const effectiveTruncation = truncation.truncated ? truncation : syntheticExecTruncation(output);
|
|
5614
|
+
notices.push(`${formatSize(effectiveTruncation.maxBytes)} limit reached`);
|
|
5615
|
+
details.truncation = effectiveTruncation;
|
|
5268
5616
|
}
|
|
5269
|
-
if (
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5617
|
+
if (parsed.linesTruncated) {
|
|
5618
|
+
notices.push(
|
|
5619
|
+
`Some lines truncated to ${GREP_MAX_LINE_LENGTH} chars. Use read tool to see full lines`
|
|
5620
|
+
);
|
|
5621
|
+
details.linesTruncated = true;
|
|
5274
5622
|
}
|
|
5275
|
-
|
|
5623
|
+
if (notices.length > 0) output += `
|
|
5624
|
+
|
|
5625
|
+
[${notices.join(". ")}]`;
|
|
5626
|
+
return {
|
|
5627
|
+
content: [{ type: "text", text: output }],
|
|
5628
|
+
details: Object.keys(details).length > 0 ? details : void 0
|
|
5629
|
+
};
|
|
5276
5630
|
}
|
|
5277
|
-
function
|
|
5278
|
-
|
|
5279
|
-
|
|
5280
|
-
|
|
5281
|
-
|
|
5282
|
-
|
|
5283
|
-
|
|
5284
|
-
|
|
5285
|
-
|
|
5631
|
+
function vercelGrepTool(sandbox, workspaceRoot) {
|
|
5632
|
+
return {
|
|
5633
|
+
name: PI_GREP_TOOL.name,
|
|
5634
|
+
description: PI_GREP_TOOL.description,
|
|
5635
|
+
promptSnippet: PI_GREP_TOOL.promptSnippet,
|
|
5636
|
+
parameters: PI_GREP_TOOL.parameters,
|
|
5637
|
+
async execute(input, ctx) {
|
|
5638
|
+
const params = input;
|
|
5639
|
+
if (ctx.abortSignal.aborted) {
|
|
5640
|
+
return makeError("grep aborted");
|
|
5286
5641
|
}
|
|
5287
|
-
|
|
5288
|
-
|
|
5289
|
-
|
|
5290
|
-
|
|
5642
|
+
if (typeof params.pattern !== "string" || params.pattern.length === 0) {
|
|
5643
|
+
return makeError("pattern is required");
|
|
5644
|
+
}
|
|
5645
|
+
if (workspaceRoot && typeof params.path === "string" && params.path.length > 0) {
|
|
5646
|
+
const resolved = resolve6(workspaceRoot, params.path);
|
|
5647
|
+
const rel = relative8(workspaceRoot, resolved);
|
|
5648
|
+
if (rel.startsWith("..") || rel.startsWith("/")) {
|
|
5649
|
+
return makeError(`path "${params.path}" is outside workspace`);
|
|
5650
|
+
}
|
|
5651
|
+
}
|
|
5652
|
+
try {
|
|
5653
|
+
const result = await sandbox.exec(buildRgCommand(params), {
|
|
5654
|
+
signal: ctx.abortSignal,
|
|
5655
|
+
timeoutMs: GREP_TIMEOUT_MS,
|
|
5656
|
+
maxOutputBytes: GREP_MAX_OUTPUT_BYTES
|
|
5657
|
+
});
|
|
5658
|
+
if (result.exitCode !== 0 && result.exitCode !== 1) {
|
|
5659
|
+
const stderr = decode(result.stderr).trim();
|
|
5660
|
+
const message = stderr || `ripgrep exited with code ${result.exitCode}`;
|
|
5661
|
+
return makeError(`grep failed: ${message}`);
|
|
5662
|
+
}
|
|
5663
|
+
const limit = numberParam(params.limit, DEFAULT_LIMIT2);
|
|
5664
|
+
const parsed = parseRgJson(decode(result.stdout), limit);
|
|
5665
|
+
return buildSuccessResult(parsed, limit, result.truncated);
|
|
5666
|
+
} catch (error) {
|
|
5667
|
+
const message = error instanceof Error ? error.message : "unknown error";
|
|
5668
|
+
return makeError(`grep failed: ${message}`);
|
|
5669
|
+
}
|
|
5670
|
+
}
|
|
5671
|
+
};
|
|
5672
|
+
}
|
|
5673
|
+
|
|
5674
|
+
// src/server/tools/filesystem/index.ts
|
|
5675
|
+
function isTextContent(content) {
|
|
5676
|
+
return content.type === "text" && typeof content.text === "string";
|
|
5677
|
+
}
|
|
5678
|
+
function adaptPiTool(piTool) {
|
|
5679
|
+
return {
|
|
5680
|
+
name: piTool.name,
|
|
5681
|
+
description: piTool.description,
|
|
5682
|
+
promptSnippet: piTool.promptSnippet,
|
|
5683
|
+
parameters: piTool.parameters,
|
|
5684
|
+
async execute(params, ctx) {
|
|
5685
|
+
const result = await piTool.execute(
|
|
5686
|
+
ctx.toolCallId,
|
|
5687
|
+
params,
|
|
5688
|
+
ctx.abortSignal,
|
|
5689
|
+
ctx.onUpdate ? (update) => {
|
|
5690
|
+
const text = update.content?.filter(isTextContent).map((c) => c.text).join("");
|
|
5691
|
+
if (text) ctx.onUpdate?.(text);
|
|
5692
|
+
} : void 0,
|
|
5693
|
+
{}
|
|
5694
|
+
);
|
|
5695
|
+
const textContent = (result.content ?? []).filter(isTextContent).map((c) => ({ type: "text", text: c.text }));
|
|
5696
|
+
return {
|
|
5697
|
+
content: textContent.length > 0 ? textContent : [{ type: "text", text: "" }],
|
|
5698
|
+
isError: false,
|
|
5699
|
+
details: result.details
|
|
5700
|
+
};
|
|
5701
|
+
}
|
|
5702
|
+
};
|
|
5291
5703
|
}
|
|
5292
|
-
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
|
|
5297
|
-
|
|
5298
|
-
|
|
5704
|
+
function buildFilesystemAgentTools(bundle) {
|
|
5705
|
+
const cwd = bundle.workspace.root;
|
|
5706
|
+
if (bundle.sandbox.provider === "vercel-sandbox") {
|
|
5707
|
+
return [
|
|
5708
|
+
adaptPiTool(createReadToolDefinition(cwd, { operations: vercelReadOps(bundle.workspace) })),
|
|
5709
|
+
adaptPiTool(createWriteToolDefinition(cwd, { operations: vercelWriteOps(bundle.workspace) })),
|
|
5710
|
+
adaptPiTool(createEditToolDefinition(cwd, { operations: vercelEditOps(bundle.workspace) })),
|
|
5711
|
+
adaptPiTool(createFindToolDefinition(cwd, { operations: vercelFindOps(bundle.sandbox, bundle.workspace) })),
|
|
5712
|
+
vercelGrepTool(bundle.sandbox, cwd),
|
|
5713
|
+
adaptPiTool(createLsToolDefinition(cwd, { operations: vercelLsOps(bundle.workspace) }))
|
|
5714
|
+
];
|
|
5299
5715
|
}
|
|
5716
|
+
const ops = boundFs(cwd);
|
|
5717
|
+
return [
|
|
5718
|
+
adaptPiTool(createReadToolDefinition(cwd, { operations: ops.read })),
|
|
5719
|
+
adaptPiTool(createWriteToolDefinition(cwd, { operations: ops.write })),
|
|
5720
|
+
adaptPiTool(createEditToolDefinition(cwd, { operations: ops.edit })),
|
|
5721
|
+
adaptPiTool(createFindToolDefinition(cwd, { operations: ops.find })),
|
|
5722
|
+
adaptPiTool(createGrepToolDefinition2(cwd, { operations: ops.grep })),
|
|
5723
|
+
adaptPiTool(createLsToolDefinition(cwd, { operations: ops.ls }))
|
|
5724
|
+
];
|
|
5300
5725
|
}
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
}
|
|
5307
|
-
function
|
|
5308
|
-
|
|
5309
|
-
if (/^[a-z0-9]{1,12}$/.test(fromName)) return fromName;
|
|
5310
|
-
if (contentType === "image/jpeg") return "jpg";
|
|
5311
|
-
if (contentType === "image/png") return "png";
|
|
5312
|
-
if (contentType === "image/gif") return "gif";
|
|
5313
|
-
if (contentType === "image/webp") return "webp";
|
|
5314
|
-
if (contentType === "image/svg+xml") return "svg";
|
|
5315
|
-
return "bin";
|
|
5726
|
+
|
|
5727
|
+
// src/server/tools/harness/index.ts
|
|
5728
|
+
import {
|
|
5729
|
+
createBashToolDefinition,
|
|
5730
|
+
createLocalBashOperations
|
|
5731
|
+
} from "@mariozechner/pi-coding-agent";
|
|
5732
|
+
function shellEscape2(s) {
|
|
5733
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
5316
5734
|
}
|
|
5317
|
-
function
|
|
5318
|
-
const
|
|
5319
|
-
const
|
|
5320
|
-
|
|
5321
|
-
|
|
5735
|
+
function bwrapSpawnHook(workspaceRoot) {
|
|
5736
|
+
const args = buildBwrapArgs(workspaceRoot);
|
|
5737
|
+
const bwrapPrefix = ["bwrap", ...args].map(shellEscape2).join(" ");
|
|
5738
|
+
return (context) => ({
|
|
5739
|
+
...context,
|
|
5740
|
+
command: `${bwrapPrefix} bash -lc ${shellEscape2(context.command)}`,
|
|
5741
|
+
env: withWorkspacePythonEnv({
|
|
5742
|
+
workspaceRoot,
|
|
5743
|
+
env: context.env,
|
|
5744
|
+
sandboxRoot: "/workspace"
|
|
5745
|
+
})
|
|
5746
|
+
});
|
|
5322
5747
|
}
|
|
5323
|
-
function
|
|
5324
|
-
|
|
5325
|
-
|
|
5326
|
-
|
|
5327
|
-
|
|
5748
|
+
function directSpawnHook(workspaceRoot) {
|
|
5749
|
+
return (context) => ({
|
|
5750
|
+
...context,
|
|
5751
|
+
env: withWorkspacePythonEnv({ workspaceRoot, env: context.env })
|
|
5752
|
+
});
|
|
5328
5753
|
}
|
|
5329
|
-
function
|
|
5330
|
-
switch (
|
|
5331
|
-
case "
|
|
5332
|
-
return
|
|
5333
|
-
case "
|
|
5334
|
-
return
|
|
5335
|
-
|
|
5336
|
-
|
|
5337
|
-
|
|
5338
|
-
case ".png":
|
|
5339
|
-
return "image/png";
|
|
5340
|
-
case ".svg":
|
|
5341
|
-
return "image/svg+xml";
|
|
5342
|
-
case ".webp":
|
|
5343
|
-
return "image/webp";
|
|
5344
|
-
case ".css":
|
|
5345
|
-
return "text/css; charset=utf-8";
|
|
5346
|
-
case ".html":
|
|
5347
|
-
case ".htm":
|
|
5348
|
-
return "text/html; charset=utf-8";
|
|
5349
|
-
case ".js":
|
|
5350
|
-
return "text/javascript; charset=utf-8";
|
|
5351
|
-
case ".mjs":
|
|
5352
|
-
return "text/javascript; charset=utf-8";
|
|
5353
|
-
case ".pdf":
|
|
5354
|
-
return "application/pdf";
|
|
5754
|
+
function bashOptionsForMode(bundle) {
|
|
5755
|
+
switch (bundle.sandbox.provider) {
|
|
5756
|
+
case "vercel-sandbox":
|
|
5757
|
+
return { operations: vercelBashOps(bundle.sandbox) };
|
|
5758
|
+
case "bwrap":
|
|
5759
|
+
return {
|
|
5760
|
+
operations: createLocalBashOperations(),
|
|
5761
|
+
spawnHook: bwrapSpawnHook(bundle.workspace.root)
|
|
5762
|
+
};
|
|
5355
5763
|
default:
|
|
5356
|
-
return
|
|
5764
|
+
return {
|
|
5765
|
+
operations: createLocalBashOperations(),
|
|
5766
|
+
spawnHook: directSpawnHook(bundle.workspace.root)
|
|
5767
|
+
};
|
|
5357
5768
|
}
|
|
5358
5769
|
}
|
|
5359
|
-
function
|
|
5360
|
-
|
|
5361
|
-
|
|
5362
|
-
|
|
5363
|
-
|
|
5364
|
-
|
|
5365
|
-
|
|
5366
|
-
|
|
5367
|
-
|
|
5368
|
-
|
|
5369
|
-
|
|
5370
|
-
|
|
5371
|
-
|
|
5372
|
-
|
|
5373
|
-
|
|
5374
|
-
}
|
|
5375
|
-
|
|
5376
|
-
const
|
|
5377
|
-
|
|
5378
|
-
|
|
5379
|
-
|
|
5380
|
-
|
|
5381
|
-
}
|
|
5382
|
-
const bytes = await workspace.readBinaryFile(path4);
|
|
5383
|
-
return reply.header("content-type", contentTypeForPath(path4)).header("content-length", String(bytes.byteLength)).header("cache-control", "no-store").send(Buffer.from(bytes));
|
|
5384
|
-
} catch (err) {
|
|
5385
|
-
return classifyError(err, reply, "file");
|
|
5386
|
-
}
|
|
5387
|
-
});
|
|
5388
|
-
app.get("/api/v1/files", async (request, reply) => {
|
|
5389
|
-
const query = request.query;
|
|
5390
|
-
const path4 = requireStringParam(query.path, "path", reply);
|
|
5391
|
-
if (path4 === null) return;
|
|
5392
|
-
try {
|
|
5393
|
-
const workspace = await resolveWorkspace(request);
|
|
5394
|
-
if (workspace.readFileWithStat) {
|
|
5395
|
-
const { content: content2, stat: stat8 } = await workspace.readFileWithStat(path4);
|
|
5396
|
-
return { content: content2, mtimeMs: stat8.kind === "file" ? stat8.mtimeMs : void 0 };
|
|
5397
|
-
}
|
|
5398
|
-
const content = await workspace.readFile(path4);
|
|
5399
|
-
const stat7 = await workspace.stat(path4);
|
|
5400
|
-
return { content, mtimeMs: stat7.kind === "file" ? stat7.mtimeMs : void 0 };
|
|
5401
|
-
} catch (err) {
|
|
5402
|
-
return classifyError(err, reply, "file");
|
|
5403
|
-
}
|
|
5404
|
-
});
|
|
5405
|
-
app.post("/api/v1/files", async (request, reply) => {
|
|
5406
|
-
const body = request.body;
|
|
5407
|
-
const path4 = requireStringParam(body?.path, "path", reply);
|
|
5408
|
-
if (path4 === null) return;
|
|
5409
|
-
if (typeof body.content !== "string") {
|
|
5410
|
-
return reply.code(400).send({
|
|
5411
|
-
error: { code: ERROR_CODE_VALIDATION_ERROR, message: "content is required", field: "content" }
|
|
5412
|
-
});
|
|
5770
|
+
function adaptPiTool2(piTool) {
|
|
5771
|
+
return {
|
|
5772
|
+
name: piTool.name,
|
|
5773
|
+
description: piTool.description,
|
|
5774
|
+
promptSnippet: piTool.promptSnippet,
|
|
5775
|
+
parameters: piTool.parameters,
|
|
5776
|
+
async execute(params, ctx) {
|
|
5777
|
+
const result = await piTool.execute(
|
|
5778
|
+
ctx.toolCallId,
|
|
5779
|
+
params,
|
|
5780
|
+
ctx.abortSignal,
|
|
5781
|
+
ctx.onUpdate ? (update) => {
|
|
5782
|
+
const text = update.content.filter((c) => c.type === "text").map((c) => c.text).join("");
|
|
5783
|
+
ctx.onUpdate(text);
|
|
5784
|
+
} : void 0,
|
|
5785
|
+
{}
|
|
5786
|
+
);
|
|
5787
|
+
const textContent = (result.content ?? []).filter((c) => c.type === "text").map((c) => ({ type: "text", text: c.text }));
|
|
5788
|
+
return {
|
|
5789
|
+
content: textContent.length > 0 ? textContent : [{ type: "text", text: "" }],
|
|
5790
|
+
isError: false,
|
|
5791
|
+
details: result.details
|
|
5792
|
+
};
|
|
5413
5793
|
}
|
|
5414
|
-
|
|
5415
|
-
|
|
5416
|
-
|
|
5417
|
-
|
|
5418
|
-
|
|
5419
|
-
|
|
5420
|
-
|
|
5421
|
-
|
|
5422
|
-
|
|
5423
|
-
|
|
5424
|
-
|
|
5425
|
-
|
|
5426
|
-
|
|
5427
|
-
|
|
5428
|
-
|
|
5429
|
-
|
|
5430
|
-
|
|
5431
|
-
|
|
5432
|
-
|
|
5433
|
-
|
|
5434
|
-
|
|
5435
|
-
|
|
5436
|
-
|
|
5437
|
-
|
|
5438
|
-
|
|
5439
|
-
});
|
|
5440
|
-
}
|
|
5441
|
-
throw statErr;
|
|
5442
|
-
}
|
|
5794
|
+
};
|
|
5795
|
+
}
|
|
5796
|
+
function createExecuteIsolatedCodeTool(sandbox) {
|
|
5797
|
+
return {
|
|
5798
|
+
name: "execute_isolated_code",
|
|
5799
|
+
description: "Execute code in an isolated sandbox environment.",
|
|
5800
|
+
parameters: {
|
|
5801
|
+
type: "object",
|
|
5802
|
+
properties: {
|
|
5803
|
+
code: { type: "string" },
|
|
5804
|
+
language: { type: "string", enum: ["python", "shell"] },
|
|
5805
|
+
image: { type: "string" },
|
|
5806
|
+
packages: { type: "array", items: { type: "string" } },
|
|
5807
|
+
sandboxId: { type: "string" },
|
|
5808
|
+
vmSize: { type: "string", enum: ["xxs", "xs", "s", "m", "l"] }
|
|
5809
|
+
},
|
|
5810
|
+
required: ["code", "language"],
|
|
5811
|
+
additionalProperties: false
|
|
5812
|
+
},
|
|
5813
|
+
async execute(input) {
|
|
5814
|
+
if (!sandbox.executeIsolatedCode) {
|
|
5815
|
+
return {
|
|
5816
|
+
content: [{ type: "text", text: "isolated-code capability is not available" }],
|
|
5817
|
+
isError: true
|
|
5818
|
+
};
|
|
5443
5819
|
}
|
|
5444
|
-
|
|
5445
|
-
|
|
5446
|
-
|
|
5820
|
+
const code = input.code;
|
|
5821
|
+
const language = input.language;
|
|
5822
|
+
if (typeof code !== "string" || code.length === 0) {
|
|
5823
|
+
return { content: [{ type: "text", text: "code is required" }], isError: true };
|
|
5447
5824
|
}
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
|
|
5451
|
-
|
|
5452
|
-
|
|
5453
|
-
|
|
5454
|
-
|
|
5455
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
const body = request.body;
|
|
5460
|
-
const filename = requireStringParam(body?.filename, "filename", reply);
|
|
5461
|
-
if (filename === null) return;
|
|
5462
|
-
const contentBase64 = requireStringParam(body?.contentBase64, "contentBase64", reply);
|
|
5463
|
-
if (contentBase64 === null) return;
|
|
5464
|
-
const contentType = typeof body.contentType === "string" ? body.contentType.trim().toLowerCase() : "";
|
|
5465
|
-
if (!contentType.startsWith("image/")) {
|
|
5466
|
-
return reply.code(400).send({
|
|
5467
|
-
error: { code: ERROR_CODE_VALIDATION_ERROR, message: "contentType must be an image/* MIME type", field: "contentType" }
|
|
5468
|
-
});
|
|
5469
|
-
}
|
|
5470
|
-
try {
|
|
5471
|
-
const workspace = await resolveWorkspace(request);
|
|
5472
|
-
const settings = await readWorkspaceSettings(workspace);
|
|
5473
|
-
const dir = normalizeUploadDir(body.directory) ?? normalizeUploadDir(settings.markdown?.imageUploadDir) ?? DEFAULT_MARKDOWN_IMAGE_UPLOAD_DIR;
|
|
5474
|
-
const ext = extForUpload(filename, contentType);
|
|
5475
|
-
const base = basenameForUpload(filename);
|
|
5476
|
-
const unique = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
5477
|
-
const path4 = `${dir}/${base}-${unique}.${ext}`;
|
|
5478
|
-
const bytes = Buffer.from(contentBase64, "base64");
|
|
5479
|
-
if (bytes.byteLength === 0 || bytes.byteLength > MAX_UPLOAD_BYTES) {
|
|
5480
|
-
return reply.code(400).send({
|
|
5481
|
-
error: { code: ERROR_CODE_VALIDATION_ERROR, message: `upload must be 1 byte to ${MAX_UPLOAD_BYTES} bytes`, field: "contentBase64" }
|
|
5825
|
+
if (language !== "python" && language !== "shell") {
|
|
5826
|
+
return { content: [{ type: "text", text: "language must be python or shell" }], isError: true };
|
|
5827
|
+
}
|
|
5828
|
+
try {
|
|
5829
|
+
const result = await sandbox.executeIsolatedCode({
|
|
5830
|
+
code,
|
|
5831
|
+
language,
|
|
5832
|
+
image: typeof input.image === "string" ? input.image : void 0,
|
|
5833
|
+
packages: Array.isArray(input.packages) ? input.packages.filter((v) => typeof v === "string") : void 0,
|
|
5834
|
+
sandboxId: typeof input.sandboxId === "string" ? input.sandboxId : void 0,
|
|
5835
|
+
vmSize: input.vmSize === "xxs" || input.vmSize === "xs" || input.vmSize === "s" || input.vmSize === "m" || input.vmSize === "l" ? input.vmSize : void 0
|
|
5482
5836
|
});
|
|
5837
|
+
return {
|
|
5838
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
5839
|
+
isError: result.exitCode !== 0,
|
|
5840
|
+
details: result
|
|
5841
|
+
};
|
|
5842
|
+
} catch (error) {
|
|
5843
|
+
return {
|
|
5844
|
+
content: [{ type: "text", text: error instanceof Error ? error.message : "execute_isolated_code failed" }],
|
|
5845
|
+
isError: true
|
|
5846
|
+
};
|
|
5483
5847
|
}
|
|
5484
|
-
await workspace.mkdir(dir, { recursive: true });
|
|
5485
|
-
const stat7 = workspace.writeBinaryFileWithStat ? await workspace.writeBinaryFileWithStat(path4, bytes) : await (async () => {
|
|
5486
|
-
if (!workspace.writeBinaryFile) {
|
|
5487
|
-
throw new Error("workspace does not support binary uploads");
|
|
5488
|
-
}
|
|
5489
|
-
await workspace.writeBinaryFile(path4, bytes);
|
|
5490
|
-
return await workspace.stat(path4);
|
|
5491
|
-
})();
|
|
5492
|
-
const sourcePath = typeof body.sourcePath === "string" && !body.sourcePath.includes("\0") ? body.sourcePath : null;
|
|
5493
|
-
return {
|
|
5494
|
-
ok: true,
|
|
5495
|
-
path: path4,
|
|
5496
|
-
markdownUrl: markdownUrlFor(sourcePath, path4),
|
|
5497
|
-
mtimeMs: stat7.kind === "file" ? stat7.mtimeMs : void 0
|
|
5498
|
-
};
|
|
5499
|
-
} catch (err) {
|
|
5500
|
-
return classifyError(err, reply, "upload");
|
|
5501
|
-
}
|
|
5502
|
-
});
|
|
5503
|
-
app.get("/api/v1/workspace-settings", async (request, reply) => {
|
|
5504
|
-
try {
|
|
5505
|
-
const workspace = await resolveWorkspace(request);
|
|
5506
|
-
return { settings: await readWorkspaceSettings(workspace) };
|
|
5507
|
-
} catch (err) {
|
|
5508
|
-
return classifyError(err, reply, "settings");
|
|
5509
5848
|
}
|
|
5849
|
+
};
|
|
5850
|
+
}
|
|
5851
|
+
function buildHarnessAgentTools(bundle) {
|
|
5852
|
+
const tools = [
|
|
5853
|
+
adaptPiTool2(createBashToolDefinition(bundle.workspace.root, bashOptionsForMode(bundle)))
|
|
5854
|
+
];
|
|
5855
|
+
if (bundle.sandbox.capabilities.includes("isolated-code")) {
|
|
5856
|
+
tools.push(createExecuteIsolatedCodeTool(bundle.sandbox));
|
|
5857
|
+
}
|
|
5858
|
+
return tools;
|
|
5859
|
+
}
|
|
5860
|
+
|
|
5861
|
+
// src/server/http/routes/health.ts
|
|
5862
|
+
function healthRoutes(app, opts, done) {
|
|
5863
|
+
const startTime = Date.now();
|
|
5864
|
+
app.get("/health", async () => {
|
|
5865
|
+
return {
|
|
5866
|
+
status: "ok",
|
|
5867
|
+
version: opts.version,
|
|
5868
|
+
uptime: Math.floor((Date.now() - startTime) / 1e3)
|
|
5869
|
+
};
|
|
5510
5870
|
});
|
|
5511
|
-
app.
|
|
5512
|
-
const
|
|
5513
|
-
|
|
5514
|
-
|
|
5515
|
-
|
|
5516
|
-
|
|
5517
|
-
return reply.code(400).send({
|
|
5518
|
-
error: { code: ERROR_CODE_INVALID_PATH, message: "markdown.imageUploadDir must be a relative workspace path", field: "markdown.imageUploadDir" }
|
|
5871
|
+
app.get("/ready", async (_request, reply) => {
|
|
5872
|
+
const state = opts.getReadiness();
|
|
5873
|
+
if (state.degradedReason) {
|
|
5874
|
+
return reply.code(503).send({
|
|
5875
|
+
status: "degraded",
|
|
5876
|
+
reason: state.degradedReason
|
|
5519
5877
|
});
|
|
5520
5878
|
}
|
|
5521
|
-
|
|
5522
|
-
|
|
5523
|
-
|
|
5524
|
-
|
|
5525
|
-
|
|
5526
|
-
markdown: {
|
|
5527
|
-
...current.markdown ?? {},
|
|
5528
|
-
imageUploadDir
|
|
5529
|
-
}
|
|
5530
|
-
};
|
|
5531
|
-
await workspace.mkdir(".boring", { recursive: true });
|
|
5532
|
-
await workspace.writeFile(BORING_SETTINGS_PATH, `${JSON.stringify(next, null, 2)}
|
|
5533
|
-
`);
|
|
5534
|
-
return { settings: next };
|
|
5535
|
-
} catch (err) {
|
|
5536
|
-
return classifyError(err, reply, "settings");
|
|
5537
|
-
}
|
|
5538
|
-
});
|
|
5539
|
-
app.delete("/api/v1/files", async (request, reply) => {
|
|
5540
|
-
const query = request.query;
|
|
5541
|
-
const path4 = requireStringParam(query.path, "path", reply);
|
|
5542
|
-
if (path4 === null) return;
|
|
5543
|
-
try {
|
|
5544
|
-
const workspace = await resolveWorkspace(request);
|
|
5545
|
-
await workspace.unlink(path4);
|
|
5546
|
-
return { ok: true };
|
|
5547
|
-
} catch (err) {
|
|
5548
|
-
return classifyError(err, reply, "file");
|
|
5549
|
-
}
|
|
5550
|
-
});
|
|
5551
|
-
app.post("/api/v1/files/move", async (request, reply) => {
|
|
5552
|
-
const body = request.body;
|
|
5553
|
-
const from = requireStringParam(body?.from, "from", reply);
|
|
5554
|
-
if (from === null) return;
|
|
5555
|
-
const to = requireStringParam(body?.to, "to", reply);
|
|
5556
|
-
if (to === null) return;
|
|
5557
|
-
try {
|
|
5558
|
-
const workspace = await resolveWorkspace(request);
|
|
5559
|
-
await workspace.rename(from, to);
|
|
5560
|
-
return { ok: true };
|
|
5561
|
-
} catch (err) {
|
|
5562
|
-
return classifyError(err, reply, "file");
|
|
5563
|
-
}
|
|
5564
|
-
});
|
|
5565
|
-
app.post("/api/v1/dirs", async (request, reply) => {
|
|
5566
|
-
const body = request.body;
|
|
5567
|
-
const path4 = requireStringParam(body?.path, "path", reply);
|
|
5568
|
-
if (path4 === null) return;
|
|
5569
|
-
const recursive = body.recursive === true;
|
|
5570
|
-
try {
|
|
5571
|
-
const workspace = await resolveWorkspace(request);
|
|
5572
|
-
await workspace.mkdir(path4, { recursive });
|
|
5573
|
-
return { ok: true };
|
|
5574
|
-
} catch (err) {
|
|
5575
|
-
return classifyError(err, reply, "directory");
|
|
5576
|
-
}
|
|
5577
|
-
});
|
|
5578
|
-
app.get("/api/v1/stat", async (request, reply) => {
|
|
5579
|
-
const query = request.query;
|
|
5580
|
-
const path4 = requireStringParam(query.path, "path", reply);
|
|
5581
|
-
if (path4 === null) return;
|
|
5582
|
-
try {
|
|
5583
|
-
const workspace = await resolveWorkspace(request);
|
|
5584
|
-
const stat7 = await workspace.stat(path4);
|
|
5585
|
-
return stat7;
|
|
5586
|
-
} catch (err) {
|
|
5587
|
-
return classifyError(err, reply, "path");
|
|
5879
|
+
if (!state.sandboxReady || !state.harnessReady) {
|
|
5880
|
+
return reply.code(503).send({
|
|
5881
|
+
status: "provisioning",
|
|
5882
|
+
retryAfter: 2
|
|
5883
|
+
});
|
|
5588
5884
|
}
|
|
5885
|
+
return { status: "ready" };
|
|
5589
5886
|
});
|
|
5590
5887
|
done();
|
|
5591
5888
|
}
|
|
@@ -6027,6 +6324,60 @@ function parseFileChangeChunk(chunk2) {
|
|
|
6027
6324
|
return change;
|
|
6028
6325
|
}
|
|
6029
6326
|
|
|
6327
|
+
// src/server/harness/pi-coding-agent/projectPiDataMessages.ts
|
|
6328
|
+
function projectPiDataMessages(messages) {
|
|
6329
|
+
const dataParts = messages.flatMap((message) => message.parts ?? []).filter((part) => {
|
|
6330
|
+
const type = part.type;
|
|
6331
|
+
return typeof type === "string" && type.startsWith("data-pi-");
|
|
6332
|
+
});
|
|
6333
|
+
if (dataParts.length === 0) return messages;
|
|
6334
|
+
const projected = [];
|
|
6335
|
+
const ensureMessage = (id, role, text = "") => {
|
|
6336
|
+
let msg = projected.find((item) => item.id === id);
|
|
6337
|
+
if (!msg) {
|
|
6338
|
+
msg = { id, role, parts: text ? [{ type: "text", text }] : [] };
|
|
6339
|
+
projected.push(msg);
|
|
6340
|
+
} else if (text && !(msg.parts ?? []).some((part) => part.type === "text" && part.text)) {
|
|
6341
|
+
msg.parts = [...msg.parts ?? [], { type: "text", text }];
|
|
6342
|
+
}
|
|
6343
|
+
return msg;
|
|
6344
|
+
};
|
|
6345
|
+
for (const part of dataParts) {
|
|
6346
|
+
const data = part.data ?? {};
|
|
6347
|
+
const messageId = typeof data.messageId === "string" ? data.messageId : void 0;
|
|
6348
|
+
if (!messageId) continue;
|
|
6349
|
+
if (part.type === "data-pi-message-start" && (data.role === "user" || data.role === "assistant")) {
|
|
6350
|
+
ensureMessage(messageId, data.role, typeof data.text === "string" ? data.text : "");
|
|
6351
|
+
} else if (part.type === "data-pi-text-start") {
|
|
6352
|
+
const msg = ensureMessage(messageId, "assistant");
|
|
6353
|
+
if (!msg.parts?.some((p) => p.type === "text")) msg.parts = [...msg.parts ?? [], { type: "text", text: "" }];
|
|
6354
|
+
} else if (part.type === "data-pi-text-delta") {
|
|
6355
|
+
const msg = ensureMessage(messageId, "assistant");
|
|
6356
|
+
const delta = typeof data.delta === "string" ? data.delta : "";
|
|
6357
|
+
const index = (msg.parts ?? []).findIndex((p) => p.type === "text");
|
|
6358
|
+
if (index >= 0) {
|
|
6359
|
+
msg.parts = (msg.parts ?? []).map((p, i) => i === index && p.type === "text" ? { ...p, text: `${p.text}${delta}` } : p);
|
|
6360
|
+
} else {
|
|
6361
|
+
msg.parts = [...msg.parts ?? [], { type: "text", text: delta }];
|
|
6362
|
+
}
|
|
6363
|
+
} else if (part.type === "data-pi-text-end" || part.type === "data-pi-message-end" && data.role === "assistant") {
|
|
6364
|
+
const msg = ensureMessage(messageId, "assistant");
|
|
6365
|
+
const text = typeof data.text === "string" ? data.text : "";
|
|
6366
|
+
if (text && !(msg.parts ?? []).some((p) => p.type === "text" && p.text)) msg.parts = [...msg.parts ?? [], { type: "text", text }];
|
|
6367
|
+
}
|
|
6368
|
+
}
|
|
6369
|
+
if (projected.length === 0) return messages;
|
|
6370
|
+
const projectedIds = new Set(projected.map((message) => message.id));
|
|
6371
|
+
const preserved = messages.filter((message) => {
|
|
6372
|
+
if (projectedIds.has(message.id)) return false;
|
|
6373
|
+
return !(message.parts ?? []).some((part) => {
|
|
6374
|
+
const type = part.type;
|
|
6375
|
+
return typeof type === "string" && type.startsWith("data-pi-");
|
|
6376
|
+
});
|
|
6377
|
+
});
|
|
6378
|
+
return [...preserved, ...projected];
|
|
6379
|
+
}
|
|
6380
|
+
|
|
6030
6381
|
// src/server/http/routes/chat.ts
|
|
6031
6382
|
var chatBodySchema = z.object({
|
|
6032
6383
|
sessionId: z.string().min(1).max(128),
|
|
@@ -6052,6 +6403,35 @@ function chatRoutes(app, opts, done) {
|
|
|
6052
6403
|
const { sessionChangesTracker } = opts;
|
|
6053
6404
|
const validateBody = createBodyValidator(chatBodySchema);
|
|
6054
6405
|
const buffers = new StreamBufferStore();
|
|
6406
|
+
const lastFollowUpBySession = /* @__PURE__ */ new Map();
|
|
6407
|
+
const cancelledFollowUpsBySession = /* @__PURE__ */ new Map();
|
|
6408
|
+
const MAX_FOLLOWUP_CACHE = 5e3;
|
|
6409
|
+
function followUpCancelKeys(clientNonce, clientSeq) {
|
|
6410
|
+
return [
|
|
6411
|
+
clientNonce ? `nonce:${clientNonce}` : void 0,
|
|
6412
|
+
clientSeq !== void 0 ? `seq:${clientSeq}` : void 0
|
|
6413
|
+
].filter((key) => Boolean(key));
|
|
6414
|
+
}
|
|
6415
|
+
function isFollowUpCancelled(sessionId, clientNonce, clientSeq) {
|
|
6416
|
+
const cancelled = cancelledFollowUpsBySession.get(sessionId);
|
|
6417
|
+
if (!cancelled) return false;
|
|
6418
|
+
return followUpCancelKeys(clientNonce, clientSeq).some((key) => cancelled.has(key));
|
|
6419
|
+
}
|
|
6420
|
+
function markFollowUpCancelled(sessionId, clientNonce, clientSeq) {
|
|
6421
|
+
const keys = followUpCancelKeys(clientNonce, clientSeq);
|
|
6422
|
+
if (keys.length === 0) return;
|
|
6423
|
+
const cancelled = cancelledFollowUpsBySession.get(sessionId) ?? /* @__PURE__ */ new Set();
|
|
6424
|
+
for (const key of keys) cancelled.add(key);
|
|
6425
|
+
cancelledFollowUpsBySession.set(sessionId, cancelled);
|
|
6426
|
+
}
|
|
6427
|
+
function evictFollowupCache() {
|
|
6428
|
+
const keys = Array.from(/* @__PURE__ */ new Set([...lastFollowUpBySession.keys(), ...cancelledFollowUpsBySession.keys()]));
|
|
6429
|
+
if (keys.length <= MAX_FOLLOWUP_CACHE) return;
|
|
6430
|
+
for (let i = 0; i < keys.length - MAX_FOLLOWUP_CACHE; i++) {
|
|
6431
|
+
lastFollowUpBySession.delete(keys[i]);
|
|
6432
|
+
cancelledFollowUpsBySession.delete(keys[i]);
|
|
6433
|
+
}
|
|
6434
|
+
}
|
|
6055
6435
|
async function resolveRuntime(request) {
|
|
6056
6436
|
if (opts.getRuntime) return await opts.getRuntime(request);
|
|
6057
6437
|
if (opts.harness && opts.workdir) {
|
|
@@ -6230,13 +6610,49 @@ function chatRoutes(app, opts, done) {
|
|
|
6230
6610
|
error: { code: ERROR_CODE_VALIDATION_ERROR, message: "attachments is invalid" }
|
|
6231
6611
|
});
|
|
6232
6612
|
}
|
|
6613
|
+
const clientSeq = typeof body.clientSeq === "number" && Number.isFinite(body.clientSeq) ? body.clientSeq : void 0;
|
|
6614
|
+
const clientNonce = typeof body.clientNonce === "string" && body.clientNonce.length > 0 ? body.clientNonce : void 0;
|
|
6615
|
+
if (isFollowUpCancelled(sessionId, clientNonce, clientSeq)) {
|
|
6616
|
+
return reply.code(202).send({ queued: false, deleted: true });
|
|
6617
|
+
}
|
|
6618
|
+
if (clientSeq !== void 0) {
|
|
6619
|
+
const last = lastFollowUpBySession.get(sessionId);
|
|
6620
|
+
if (last !== void 0 && clientSeq <= last.seq) {
|
|
6621
|
+
if (clientSeq === last.seq && clientNonce && clientNonce === last.nonce) {
|
|
6622
|
+
return reply.code(202).send({ queued: true, duplicate: true });
|
|
6623
|
+
}
|
|
6624
|
+
return reply.code(409).send({
|
|
6625
|
+
error: { code: ERROR_CODE_CONFLICT, message: "followup_out_of_order" }
|
|
6626
|
+
});
|
|
6627
|
+
}
|
|
6628
|
+
}
|
|
6233
6629
|
const runtime = await resolveRuntime(request);
|
|
6234
|
-
|
|
6235
|
-
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
|
|
6239
|
-
|
|
6630
|
+
if (!runtime.harness.followUp) {
|
|
6631
|
+
return reply.code(409).send({
|
|
6632
|
+
error: { code: ERROR_CODE_CONFLICT, message: "followup_unsupported" }
|
|
6633
|
+
});
|
|
6634
|
+
}
|
|
6635
|
+
try {
|
|
6636
|
+
await runtime.harness.followUp(
|
|
6637
|
+
sessionId,
|
|
6638
|
+
body.message,
|
|
6639
|
+
parsedAttachments.data,
|
|
6640
|
+
typeof body.displayText === "string" && body.displayText.length > 0 ? body.displayText : body.message,
|
|
6641
|
+
{ clientNonce, clientSeq }
|
|
6642
|
+
);
|
|
6643
|
+
} catch (err) {
|
|
6644
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
6645
|
+
if (message === "followup_session_not_ready") {
|
|
6646
|
+
return reply.code(409).send({
|
|
6647
|
+
error: { code: ERROR_CODE_CONFLICT, message: "followup_session_not_ready" }
|
|
6648
|
+
});
|
|
6649
|
+
}
|
|
6650
|
+
throw err;
|
|
6651
|
+
}
|
|
6652
|
+
if (clientSeq !== void 0) {
|
|
6653
|
+
lastFollowUpBySession.set(sessionId, { seq: clientSeq, nonce: clientNonce });
|
|
6654
|
+
evictFollowupCache();
|
|
6655
|
+
}
|
|
6240
6656
|
return reply.code(202).send({ queued: true });
|
|
6241
6657
|
}
|
|
6242
6658
|
);
|
|
@@ -6244,8 +6660,14 @@ function chatRoutes(app, opts, done) {
|
|
|
6244
6660
|
"/api/v1/agent/chat/:sessionId/followup",
|
|
6245
6661
|
async (request, reply) => {
|
|
6246
6662
|
const { sessionId } = request.params;
|
|
6663
|
+
const query = request.query;
|
|
6664
|
+
const clientNonce = typeof query.clientNonce === "string" && query.clientNonce.length > 0 ? query.clientNonce : void 0;
|
|
6665
|
+
const rawClientSeq = typeof query.clientSeq === "string" ? Number(query.clientSeq) : query.clientSeq;
|
|
6666
|
+
const clientSeq = typeof rawClientSeq === "number" && Number.isFinite(rawClientSeq) ? rawClientSeq : void 0;
|
|
6247
6667
|
const runtime = await resolveRuntime(request);
|
|
6248
|
-
|
|
6668
|
+
markFollowUpCancelled(sessionId, clientNonce, clientSeq);
|
|
6669
|
+
evictFollowupCache();
|
|
6670
|
+
runtime.harness.clearFollowUp?.(sessionId, { clientNonce, clientSeq });
|
|
6249
6671
|
return reply.code(204).send();
|
|
6250
6672
|
}
|
|
6251
6673
|
);
|
|
@@ -6263,7 +6685,7 @@ function chatRoutes(app, opts, done) {
|
|
|
6263
6685
|
try {
|
|
6264
6686
|
const runtime = await resolveRuntime(request);
|
|
6265
6687
|
if (runtime.harness.sessions.saveMessages) {
|
|
6266
|
-
await runtime.harness.sessions.saveMessages(ctx, sessionId, body.messages);
|
|
6688
|
+
await runtime.harness.sessions.saveMessages(ctx, sessionId, projectPiDataMessages(body.messages));
|
|
6267
6689
|
}
|
|
6268
6690
|
return reply.code(204).send();
|
|
6269
6691
|
} catch {
|
|
@@ -6318,7 +6740,7 @@ function modelsRoutes(app, _opts, done) {
|
|
|
6318
6740
|
return a.id.localeCompare(b.id);
|
|
6319
6741
|
});
|
|
6320
6742
|
const configuredDefaultModel = readConfiguredDefaultModel();
|
|
6321
|
-
const defaultModel = configuredDefaultModel && models.some((m) => m.available && m.provider === configuredDefaultModel.provider && m.id === configuredDefaultModel.id) ? configuredDefaultModel :
|
|
6743
|
+
const defaultModel = configuredDefaultModel && models.some((m) => m.available && m.provider === configuredDefaultModel.provider && m.id === configuredDefaultModel.id) ? configuredDefaultModel : void 0;
|
|
6322
6744
|
const payload = defaultModel ? { models, defaultModel: { provider: defaultModel.provider, id: defaultModel.id } } : { models };
|
|
6323
6745
|
return reply.code(200).send(payload);
|
|
6324
6746
|
});
|
|
@@ -6326,26 +6748,55 @@ function modelsRoutes(app, _opts, done) {
|
|
|
6326
6748
|
}
|
|
6327
6749
|
|
|
6328
6750
|
// src/server/http/routes/skills.ts
|
|
6329
|
-
import {
|
|
6751
|
+
import {
|
|
6752
|
+
DefaultPackageManager,
|
|
6753
|
+
getAgentDir as getAgentDir3,
|
|
6754
|
+
loadSkills
|
|
6755
|
+
} from "@mariozechner/pi-coding-agent";
|
|
6330
6756
|
var CACHE_TTL_MS2 = 3e4;
|
|
6331
6757
|
function skillsRoutes(app, opts, done) {
|
|
6332
|
-
|
|
6333
|
-
app.get("/api/v1/agent/skills", (
|
|
6334
|
-
const now = Date.now();
|
|
6335
|
-
if (cached && cached.expiresAt > now) {
|
|
6336
|
-
return reply.code(200).send({ skills: cached.skills });
|
|
6337
|
-
}
|
|
6758
|
+
const cached = /* @__PURE__ */ new Map();
|
|
6759
|
+
app.get("/api/v1/agent/skills", async (request, reply) => {
|
|
6338
6760
|
try {
|
|
6339
|
-
const
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
|
|
6761
|
+
const workspaceRoot = opts.getWorkspaceRoot ? await opts.getWorkspaceRoot(request) : opts.workspaceRoot;
|
|
6762
|
+
const additionalSkillPaths = opts.getAdditionalSkillPaths ? await opts.getAdditionalSkillPaths(request) : opts.additionalSkillPaths;
|
|
6763
|
+
const piPackages = opts.getPiPackages ? await opts.getPiPackages(request) : opts.piPackages;
|
|
6764
|
+
const noSkills = opts.getNoSkills ? await opts.getNoSkills(request) : opts.noSkills;
|
|
6765
|
+
const cacheKey = JSON.stringify([workspaceRoot, additionalSkillPaths ?? [], piPackages ?? [], Boolean(noSkills)]);
|
|
6766
|
+
const now = Date.now();
|
|
6767
|
+
for (const [key, entry] of cached) {
|
|
6768
|
+
if (entry.expiresAt <= now) cached.delete(key);
|
|
6769
|
+
}
|
|
6770
|
+
const cachedEntry = cached.get(cacheKey);
|
|
6771
|
+
if (cachedEntry && cachedEntry.expiresAt > now) {
|
|
6772
|
+
return reply.code(200).send({ skills: cachedEntry.skills });
|
|
6773
|
+
}
|
|
6774
|
+
const agentDir = getAgentDir3();
|
|
6775
|
+
const packageSkillPaths = noSkills ? [] : await (async () => {
|
|
6776
|
+
const settingsManager = createResourceSettingsManager(
|
|
6777
|
+
workspaceRoot,
|
|
6778
|
+
agentDir,
|
|
6779
|
+
piPackages ?? []
|
|
6780
|
+
);
|
|
6781
|
+
const packageManager = new DefaultPackageManager({
|
|
6782
|
+
cwd: workspaceRoot,
|
|
6783
|
+
agentDir,
|
|
6784
|
+
settingsManager
|
|
6785
|
+
});
|
|
6786
|
+
const resolved = await packageManager.resolve();
|
|
6787
|
+
return resolved.skills.filter((resource) => resource.enabled).map((resource) => resource.path);
|
|
6788
|
+
})();
|
|
6789
|
+
const result = loadSkills({
|
|
6790
|
+
cwd: workspaceRoot,
|
|
6791
|
+
agentDir,
|
|
6792
|
+
skillPaths: [...packageSkillPaths, ...additionalSkillPaths ?? []],
|
|
6793
|
+
includeDefaults: false
|
|
6343
6794
|
});
|
|
6344
6795
|
const skills = result.skills.map((s) => ({
|
|
6345
6796
|
name: s.name,
|
|
6346
6797
|
description: s.description
|
|
6347
6798
|
}));
|
|
6348
|
-
cached
|
|
6799
|
+
cached.set(cacheKey, { skills, expiresAt: now + CACHE_TTL_MS2 });
|
|
6349
6800
|
return reply.code(200).send({ skills });
|
|
6350
6801
|
} catch {
|
|
6351
6802
|
return reply.code(200).send({ skills: [] });
|
|
@@ -6998,14 +7449,15 @@ async function createAgentApp(opts = {}) {
|
|
|
6998
7449
|
const sessionId = opts.sessionId ?? DEFAULT_SESSION_ID;
|
|
6999
7450
|
const templatePath = opts.templatePath ?? getEnv("BORING_AGENT_TEMPLATE_PATH");
|
|
7000
7451
|
const app = Fastify({ logger: opts.logger ?? true, bodyLimit: 16 * 1024 * 1024 });
|
|
7001
|
-
const resolvedMode = opts.mode ?? autoDetectMode();
|
|
7002
|
-
const
|
|
7452
|
+
const resolvedMode = opts.runtimeModeAdapter?.id ?? opts.mode ?? autoDetectMode();
|
|
7453
|
+
const modeAdapter = opts.runtimeModeAdapter ?? resolveMode(resolvedMode);
|
|
7454
|
+
const runtimeBundle = await modeAdapter.create({
|
|
7003
7455
|
workspaceRoot,
|
|
7004
7456
|
sessionId,
|
|
7005
7457
|
templatePath
|
|
7006
7458
|
});
|
|
7007
7459
|
const pluginTools = [];
|
|
7008
|
-
if (
|
|
7460
|
+
if (modeAdapter.workspaceFsCapability === "strong") {
|
|
7009
7461
|
const pluginResult = await loadPlugins({ cwd: workspaceRoot });
|
|
7010
7462
|
if (pluginResult.errors.length > 0) {
|
|
7011
7463
|
for (const e of pluginResult.errors) {
|
|
@@ -7025,12 +7477,10 @@ async function createAgentApp(opts = {}) {
|
|
|
7025
7477
|
const harness = createPiCodingAgentHarness({
|
|
7026
7478
|
tools,
|
|
7027
7479
|
cwd: workspaceRoot,
|
|
7480
|
+
sessionNamespace: opts.sessionNamespace,
|
|
7481
|
+
sessionDir: opts.sessionDir,
|
|
7028
7482
|
systemPromptAppend: opts.systemPromptAppend,
|
|
7029
|
-
resourceLoaderOptions:
|
|
7030
|
-
noContextFiles: true,
|
|
7031
|
-
noSkills: true,
|
|
7032
|
-
...opts.resourceLoaderOptions
|
|
7033
|
-
}
|
|
7483
|
+
resourceLoaderOptions: opts.resourceLoaderOptions
|
|
7034
7484
|
});
|
|
7035
7485
|
const sessionChangesTracker = new InMemorySessionChangesTracker();
|
|
7036
7486
|
const readyTracker = new ReadyStatusTracker({
|
|
@@ -7116,6 +7566,11 @@ function mergeTools(options) {
|
|
|
7116
7566
|
setLastRegistered(merged, tool);
|
|
7117
7567
|
}
|
|
7118
7568
|
for (const tool of options.extraTools ?? []) {
|
|
7569
|
+
if (merged.has(tool.name)) {
|
|
7570
|
+
options.logger?.warn(
|
|
7571
|
+
`[catalog] Tool "${tool.name}" overridden by extraTools`
|
|
7572
|
+
);
|
|
7573
|
+
}
|
|
7119
7574
|
setLastRegistered(merged, tool);
|
|
7120
7575
|
}
|
|
7121
7576
|
for (const plugin of options.pluginTools ?? []) {
|
|
@@ -7271,7 +7726,7 @@ function getRequestWorkspaceId(request) {
|
|
|
7271
7726
|
}
|
|
7272
7727
|
function isWorkspaceAgnosticAgentRequest(request) {
|
|
7273
7728
|
const pathname = request.url.split("?")[0] ?? request.url;
|
|
7274
|
-
return pathname === "/
|
|
7729
|
+
return pathname === "/health" || pathname === "/ready" || pathname === "/api/v1/agent/models" || pathname === "/api/v1/ready-status";
|
|
7275
7730
|
}
|
|
7276
7731
|
function extractHttpStatus2(error) {
|
|
7277
7732
|
const statusCode = error?.statusCode;
|
|
@@ -7281,6 +7736,11 @@ function extractHttpStatus2(error) {
|
|
|
7281
7736
|
const responseStatus = error?.response?.status;
|
|
7282
7737
|
return typeof responseStatus === "number" ? responseStatus : null;
|
|
7283
7738
|
}
|
|
7739
|
+
function normalizeSessionNamespace(value) {
|
|
7740
|
+
if (typeof value !== "string") return void 0;
|
|
7741
|
+
const trimmed = value.trim();
|
|
7742
|
+
return trimmed.length > 0 ? trimmed : void 0;
|
|
7743
|
+
}
|
|
7284
7744
|
function isExpiredSandboxRuntimeError(error) {
|
|
7285
7745
|
const code = error?.code;
|
|
7286
7746
|
if (code === ErrorCode.enum.SANDBOX_EXPIRED) return true;
|
|
@@ -7293,28 +7753,54 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
7293
7753
|
const workspaceRoot = opts.workspaceRoot ?? process.cwd();
|
|
7294
7754
|
const sessionId = opts.sessionId ?? DEFAULT_WORKSPACE_ID3;
|
|
7295
7755
|
const templatePath = opts.templatePath ?? getEnv("BORING_AGENT_TEMPLATE_PATH");
|
|
7296
|
-
const resolvedMode = opts.mode ?? autoDetectMode();
|
|
7297
|
-
const modeAdapter = selectRuntimeModeAdapter(resolvedMode, opts.sandboxHandleStore);
|
|
7756
|
+
const resolvedMode = opts.runtimeModeAdapter?.id ?? opts.mode ?? autoDetectMode();
|
|
7757
|
+
const modeAdapter = opts.runtimeModeAdapter ?? selectRuntimeModeAdapter(resolvedMode, opts.sandboxHandleStore);
|
|
7298
7758
|
app.addHook("onClose", async () => {
|
|
7299
7759
|
await modeAdapter.dispose?.();
|
|
7300
7760
|
});
|
|
7301
|
-
const requestScopedRuntime = typeof opts.getWorkspaceId === "function" || typeof opts.getWorkspaceRoot === "function";
|
|
7761
|
+
const requestScopedRuntime = typeof opts.getWorkspaceId === "function" || typeof opts.getWorkspaceRoot === "function" || typeof opts.getTemplatePath === "function" || typeof opts.getResourceLoaderOptions === "function" || typeof opts.getSessionNamespace === "function";
|
|
7302
7762
|
const sessionChangesTracker = new InMemorySessionChangesTracker();
|
|
7303
7763
|
const runtimeBindings = /* @__PURE__ */ new Map();
|
|
7764
|
+
const MAX_RUNTIME_BINDINGS = 256;
|
|
7765
|
+
function evictRuntimeBindings() {
|
|
7766
|
+
if (runtimeBindings.size <= MAX_RUNTIME_BINDINGS) return;
|
|
7767
|
+
const keys = Array.from(runtimeBindings.keys());
|
|
7768
|
+
for (let i = 0; i < keys.length - MAX_RUNTIME_BINDINGS; i++) {
|
|
7769
|
+
runtimeBindings.delete(keys[i]);
|
|
7770
|
+
}
|
|
7771
|
+
}
|
|
7304
7772
|
async function resolveRuntimeScope(workspaceId, request) {
|
|
7305
7773
|
const root = request && opts.getWorkspaceRoot ? await opts.getWorkspaceRoot(workspaceId, request) : workspaceRoot;
|
|
7774
|
+
const scopedTemplatePath = opts.getTemplatePath ? await opts.getTemplatePath({ workspaceId, workspaceRoot: root, request }) : templatePath;
|
|
7775
|
+
const resourceLoaderOptions = opts.getResourceLoaderOptions ? await opts.getResourceLoaderOptions({ workspaceId, workspaceRoot: root, request }) : opts.resourceLoaderOptions;
|
|
7776
|
+
const sessionNamespace = normalizeSessionNamespace(opts.getSessionNamespace ? await opts.getSessionNamespace({ workspaceId, workspaceRoot: root, request }) : opts.sessionNamespace);
|
|
7306
7777
|
return {
|
|
7307
7778
|
root,
|
|
7308
|
-
|
|
7779
|
+
templatePath: scopedTemplatePath,
|
|
7780
|
+
resourceLoaderOptions,
|
|
7781
|
+
sessionNamespace,
|
|
7782
|
+
key: JSON.stringify([
|
|
7783
|
+
resolvedMode,
|
|
7784
|
+
workspaceId,
|
|
7785
|
+
root,
|
|
7786
|
+
scopedTemplatePath ?? null,
|
|
7787
|
+
resourceLoaderOptions ?? null,
|
|
7788
|
+
sessionNamespace ?? null
|
|
7789
|
+
])
|
|
7309
7790
|
};
|
|
7310
7791
|
}
|
|
7311
|
-
async function
|
|
7312
|
-
const
|
|
7792
|
+
async function resolveSkillScope(workspaceId, request) {
|
|
7793
|
+
const root = request && opts.getWorkspaceRoot ? await opts.getWorkspaceRoot(workspaceId, request) : workspaceRoot;
|
|
7794
|
+
const resourceLoaderOptions = opts.getResourceLoaderOptions ? await opts.getResourceLoaderOptions({ workspaceId, workspaceRoot: root, request }) : opts.resourceLoaderOptions;
|
|
7795
|
+
return { root, resourceLoaderOptions };
|
|
7796
|
+
}
|
|
7797
|
+
async function createRuntimeBinding(workspaceId, scope, request) {
|
|
7798
|
+
const root = scope.root;
|
|
7313
7799
|
const runtimeBundle = await modeAdapter.create({
|
|
7314
7800
|
workspaceRoot: root,
|
|
7315
7801
|
sessionId: workspaceId,
|
|
7316
7802
|
workspaceId,
|
|
7317
|
-
templatePath:
|
|
7803
|
+
templatePath: scope.templatePath
|
|
7318
7804
|
});
|
|
7319
7805
|
const standardTools = [
|
|
7320
7806
|
...buildHarnessAgentTools(runtimeBundle),
|
|
@@ -7322,7 +7808,7 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
7322
7808
|
...buildUploadAgentTools(runtimeBundle)
|
|
7323
7809
|
];
|
|
7324
7810
|
const pluginTools = [];
|
|
7325
|
-
if (
|
|
7811
|
+
if (modeAdapter.workspaceFsCapability === "strong") {
|
|
7326
7812
|
const pluginResult = await loadPlugins({ cwd: root });
|
|
7327
7813
|
if (pluginResult.errors.length > 0) {
|
|
7328
7814
|
for (const e of pluginResult.errors) {
|
|
@@ -7339,7 +7825,8 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
7339
7825
|
const scopedExtraTools = opts.getExtraTools ? await opts.getExtraTools({
|
|
7340
7826
|
workspaceId,
|
|
7341
7827
|
workspaceRoot: root,
|
|
7342
|
-
runtimeMode: resolvedMode
|
|
7828
|
+
runtimeMode: resolvedMode,
|
|
7829
|
+
workspaceFsCapability: runtimeBundle.workspace.fsCapability
|
|
7343
7830
|
}) : [];
|
|
7344
7831
|
const tools = mergeTools({
|
|
7345
7832
|
standardTools,
|
|
@@ -7353,12 +7840,9 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
7353
7840
|
const harness = createPiCodingAgentHarness({
|
|
7354
7841
|
tools,
|
|
7355
7842
|
cwd: root,
|
|
7843
|
+
sessionNamespace: scope.sessionNamespace,
|
|
7356
7844
|
systemPromptAppend: opts.systemPromptAppend,
|
|
7357
|
-
resourceLoaderOptions:
|
|
7358
|
-
noContextFiles: true,
|
|
7359
|
-
noSkills: true,
|
|
7360
|
-
...opts.resourceLoaderOptions
|
|
7361
|
-
}
|
|
7845
|
+
resourceLoaderOptions: scope.resourceLoaderOptions
|
|
7362
7846
|
});
|
|
7363
7847
|
const readyTracker = new ReadyStatusTracker({
|
|
7364
7848
|
sandboxReady: resolvedMode !== "vercel-sandbox",
|
|
@@ -7384,8 +7868,9 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
7384
7868
|
await existing
|
|
7385
7869
|
);
|
|
7386
7870
|
}
|
|
7387
|
-
const created = createRuntimeBinding(workspaceId, scope
|
|
7871
|
+
const created = createRuntimeBinding(workspaceId, scope, request);
|
|
7388
7872
|
runtimeBindings.set(scope.key, created);
|
|
7873
|
+
evictRuntimeBindings();
|
|
7389
7874
|
try {
|
|
7390
7875
|
return await ensureRuntimeBindingReady(
|
|
7391
7876
|
workspaceId,
|
|
@@ -7400,8 +7885,9 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
7400
7885
|
async function recreateRuntimeBinding(workspaceId, scope) {
|
|
7401
7886
|
runtimeBindings.delete(scope.key);
|
|
7402
7887
|
evictSandboxHandleCacheForWorkspace(workspaceId);
|
|
7403
|
-
const created = createRuntimeBinding(workspaceId, scope
|
|
7888
|
+
const created = createRuntimeBinding(workspaceId, scope);
|
|
7404
7889
|
runtimeBindings.set(scope.key, created);
|
|
7890
|
+
evictRuntimeBindings();
|
|
7405
7891
|
try {
|
|
7406
7892
|
const binding = await created;
|
|
7407
7893
|
binding.lastHealthCheckMs = Date.now();
|
|
@@ -7431,6 +7917,15 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
7431
7917
|
}
|
|
7432
7918
|
}
|
|
7433
7919
|
const staticBinding = requestScopedRuntime ? null : await getOrCreateRuntimeBinding(sessionId);
|
|
7920
|
+
const skillsScopeByRequest = /* @__PURE__ */ new WeakMap();
|
|
7921
|
+
function getSkillsScopeForRequest(request) {
|
|
7922
|
+
let promise = skillsScopeByRequest.get(request);
|
|
7923
|
+
if (!promise) {
|
|
7924
|
+
promise = resolveSkillScope(getRequestWorkspaceId(request), request);
|
|
7925
|
+
skillsScopeByRequest.set(request, promise);
|
|
7926
|
+
}
|
|
7927
|
+
return promise;
|
|
7928
|
+
}
|
|
7434
7929
|
async function getBindingForRequest(request) {
|
|
7435
7930
|
if (staticBinding) return staticBinding;
|
|
7436
7931
|
return await getOrCreateRuntimeBinding(getRequestWorkspaceId(request), request);
|
|
@@ -7530,7 +8025,13 @@ var registerAgentRoutes = async (app, opts) => {
|
|
|
7530
8025
|
await app.register(modelsRoutes);
|
|
7531
8026
|
await app.register(skillsRoutes, {
|
|
7532
8027
|
workspaceRoot,
|
|
7533
|
-
additionalSkillPaths: opts.resourceLoaderOptions?.additionalSkillPaths
|
|
8028
|
+
additionalSkillPaths: opts.resourceLoaderOptions?.additionalSkillPaths,
|
|
8029
|
+
piPackages: opts.resourceLoaderOptions?.piPackages,
|
|
8030
|
+
noSkills: opts.resourceLoaderOptions?.noSkills,
|
|
8031
|
+
getWorkspaceRoot: staticBinding ? void 0 : async (request) => (await getSkillsScopeForRequest(request)).root,
|
|
8032
|
+
getAdditionalSkillPaths: staticBinding ? void 0 : async (request) => (await getSkillsScopeForRequest(request)).resourceLoaderOptions?.additionalSkillPaths,
|
|
8033
|
+
getPiPackages: staticBinding ? void 0 : async (request) => (await getSkillsScopeForRequest(request)).resourceLoaderOptions?.piPackages,
|
|
8034
|
+
getNoSkills: staticBinding ? void 0 : async (request) => (await getSkillsScopeForRequest(request)).resourceLoaderOptions?.noSkills
|
|
7534
8035
|
});
|
|
7535
8036
|
await app.register(sessionChangesRoutes, { tracker: sessionChangesTracker });
|
|
7536
8037
|
await app.register(
|
|
@@ -7563,6 +8064,7 @@ export {
|
|
|
7563
8064
|
createNodeWorkspace,
|
|
7564
8065
|
createVercelDeploymentSnapshotProvider,
|
|
7565
8066
|
createVercelSandboxWorkspace,
|
|
8067
|
+
fileRoutes,
|
|
7566
8068
|
hasBwrap,
|
|
7567
8069
|
mergePiPackageSources,
|
|
7568
8070
|
piPackageSourceKey,
|