@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.
@@ -1489,286 +1489,827 @@ function createNodeWorkspace(root) {
1489
1489
  };
1490
1490
  }
1491
1491
 
1492
- // src/server/workspace/provisionRuntime.ts
1493
- import { createHash as createHash3 } from "crypto";
1494
- import { constants as constants2 } from "fs";
1495
- 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";
1496
- import { dirname as dirname4, join as join3, resolve as resolve3 } from "path";
1497
- import { fileURLToPath } from "url";
1498
- import { execFile } from "child_process";
1499
- import { promisify } from "util";
1500
- var execFileAsync = promisify(execFile);
1501
- var PROVISIONING_VERSION = 1;
1502
- function toPath(value) {
1503
- return value instanceof URL ? fileURLToPath(value) : value;
1504
- }
1505
- async function exists(path4) {
1506
- try {
1507
- await access2(path4, constants2.F_OK);
1508
- return true;
1509
- } catch {
1510
- return false;
1511
- }
1512
- }
1513
- async function run(cmd, args, cwd) {
1514
- await execFileAsync(cmd, args, {
1515
- cwd,
1516
- env: process.env,
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
- async function commandExists(cmd) {
1522
- try {
1523
- await execFileAsync(cmd, ["--version"], { maxBuffer: 1024 * 1024 });
1524
- return true;
1525
- } catch {
1526
- return false;
1527
- }
1518
+ function ensureWorkspaceContext(request, workspaceId, authenticated) {
1519
+ request.workspaceContext = {
1520
+ workspaceId,
1521
+ authenticated
1522
+ };
1528
1523
  }
1529
- async function hashPath(path4, hash) {
1530
- const info = await stat3(path4);
1531
- if (info.isDirectory()) {
1532
- const entries = (await readdir2(path4)).filter((entry) => entry !== "__pycache__" && entry !== "build" && !entry.endsWith(".egg-info")).sort();
1533
- for (const entry of entries) {
1534
- hash.update(`dir:${entry}
1535
- `);
1536
- await hashPath(join3(path4, entry), hash);
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
- return;
1539
- }
1540
- if (!info.isFile()) return;
1541
- hash.update(`file:${path4}
1542
- `);
1543
- hash.update(await readFile4(path4));
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
- for (const spec of provisioning.python ?? []) {
1558
- const projectFile = toPath(spec.projectFile);
1559
- hash.update(`python:${spec.id}:${projectFile}
1560
- `);
1561
- hash.update(JSON.stringify(spec.extraLibs ?? []));
1562
- hash.update(JSON.stringify(Object.fromEntries(Object.entries(spec.env ?? {}).map(([k, v]) => [k, String(v)]))));
1563
- if (await exists(projectFile)) await hashPath(dirname4(projectFile), hash);
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
- return `sha256:${hash.digest("hex")}`;
1567
- }
1568
- function collectEnv(contributions) {
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
- return env;
1556
+ ensureWorkspaceContext(request, workspaceId, true);
1557
+ };
1578
1558
  }
1579
- async function seedTemplates(workspaceRoot, contributions) {
1580
- for (const { provisioning } of contributions) {
1581
- for (const template of provisioning.templateDirs ?? []) {
1582
- await cp(toPath(template.path), resolve3(workspaceRoot, template.target ?? "."), {
1583
- recursive: true,
1584
- force: false,
1585
- errorOnExist: false
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
- async function ensurePython(workspaceRoot, specs) {
1591
- if (specs.length === 0) return;
1592
- const venvPython = join3(workspaceRoot, ".venv", "bin", "python");
1593
- const uv = await commandExists("uv");
1594
- if (!await exists(venvPython)) {
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
- for (const spec of specs) {
1599
- const projectDir = dirname4(toPath(spec.projectFile));
1600
- if (uv) {
1601
- await run("uv", ["pip", "install", "--python", venvPython, projectDir], workspaceRoot);
1602
- if (spec.extraLibs?.length) {
1603
- await run("uv", ["pip", "install", "--python", venvPython, ...spec.extraLibs], workspaceRoot);
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
- await run(venvPython, ["-m", "pip", "install", projectDir], workspaceRoot);
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
- async function writeExecutable(path4, body) {
1612
- await writeFile4(path4, body, "utf8");
1613
- await chmod3(path4, 493);
1614
- }
1615
- function bashSingleQuote(value) {
1616
- return `'${value.replaceAll("'", "'\\''")}'`;
1617
- }
1618
- function assertEnvKey(key) {
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
- async function isRuntimeMaterialized(workspaceRoot, contributions) {
1624
- const hasPython = contributions.some(({ provisioning }) => (provisioning.python ?? []).length > 0);
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
- async function writeShims(workspaceRoot, env) {
1629
- const shimDir = join3(workspaceRoot, ".boring-agent", "bin");
1630
- const venvBin = join3(workspaceRoot, ".venv", "bin");
1631
- await mkdir4(shimDir, { recursive: true });
1632
- const exports = Object.entries(env).map(([key, value]) => {
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
- return shimDir;
1668
- }
1669
- async function provisionRuntimeWorkspace({
1670
- workspaceRoot,
1671
- contributions,
1672
- force = false
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
- await mkdir4(workspaceRoot, { recursive: true });
1679
- const env = collectEnv(active);
1680
- const hash = await fingerprint(active);
1681
- const markerPath = join3(workspaceRoot, ".boring-agent", "provisioning.json");
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
- await seedTemplates(workspaceRoot, active);
1694
- await ensurePython(workspaceRoot, active.flatMap(({ provisioning }) => provisioning.python ?? []));
1695
- const actualBinDir = await writeShims(workspaceRoot, env);
1696
- await mkdir4(dirname4(markerPath), { recursive: true });
1697
- await writeFile4(markerPath, JSON.stringify({ v: PROVISIONING_VERSION, fingerprint: hash }, null, 2), "utf8");
1698
- return { fingerprint: hash, changed: true, env, binDir: actualBinDir };
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
- // src/server/workspace/createVercelSandboxWorkspace.ts
1702
- var VERCEL_SANDBOX_REMOTE_ROOT = "/vercel/sandbox";
1703
- var VERCEL_SANDBOX_WORKSPACE_ROOT = "/workspace";
1704
- var CACHE_TTL_MS = 15e3;
1705
- var CACHE_MAX_ENTRIES = 512;
1706
- var MAX_INLINE_WRITE_BYTES = 128 * 1024;
1707
- var metadataInvalidators = /* @__PURE__ */ new WeakMap();
1708
- function toSandboxPath(relPath) {
1709
- return validatePath(VERCEL_SANDBOX_REMOTE_ROOT, relPath);
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 createTimedLruCache(ttlMs, maxEntries) {
1712
- const entries = /* @__PURE__ */ new Map();
1713
- return {
1714
- get(key) {
1715
- const now = Date.now();
1716
- const entry = entries.get(key);
1717
- if (!entry) return void 0;
1718
- if (entry.expiresAt <= now) {
1719
- entries.delete(key);
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
- clear() {
1736
- entries.clear();
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 cloneStat(stat7) {
1741
- return {
1742
- size: stat7.size,
1743
- mtimeMs: stat7.mtimeMs,
1744
- kind: stat7.kind
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 cloneEntries(entries) {
1748
- return entries.map((entry) => ({ name: entry.name, kind: entry.kind }));
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 shellQuote2(value) {
1751
- return `'${value.replace(/'/g, `'\\''`)}'`;
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
- async function runJson(sandbox, script) {
1754
- const result = await sandbox.runCommand({ cmd: "sh", args: ["-c", script] });
1755
- const [out, err] = await Promise.all([
1756
- result.stdout?.() ?? Promise.resolve(""),
1757
- result.stderr?.() ?? Promise.resolve("")
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
- async function runShell(sandbox, script) {
1765
- const result = await sandbox.runCommand({ cmd: "sh", args: ["-c", script] });
1766
- if ((result.exitCode ?? 1) !== 0) {
1767
- const err = await (result.stderr?.() ?? Promise.resolve(""));
1768
- throw new Error(err || `sandbox command failed with exit code ${result.exitCode}`);
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 registerMetadataInvalidator(sandbox, invalidate) {
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 dirname5, join as join4 } from "path";
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(dirname5(markerPath), { recursive: true });
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.replaceAll(VERCEL_SANDBOX_WORKSPACE_ROOT, VERCEL_SANDBOX_REMOTE_ROOT);
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 log(msg, meta = {}) {
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
- log("cache hit", { hash, fileCount: files.length });
2991
+ log2("cache hit", { hash, fileCount: files.length });
2434
2992
  return { url: cached, hash };
2435
2993
  }
2436
2994
  const tarball = await buildTarGz(files);
2437
- log("tarball built", {
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
- log("uploaded", { hash, url, totalMs: Date.now() - startMs });
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, sessionDir) {
3540
+ constructor(cwd, options) {
3052
3541
  this.cwd = cwd;
3053
- this.sessionDir = sessionDir ?? defaultSessionDir(cwd);
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() || getEnv("ANTHROPIC_API_KEY")?.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 createResourceSettingsManager(cwd, agentDir, piPackages) {
3752
- const fileSettingsManager = SettingsManager.create(cwd, agentDir);
3753
- if (piPackages.length === 0) return fileSettingsManager;
3754
- const projectSettings = fileSettingsManager.getProjectSettings();
3755
- let globalSettingsJson = JSON.stringify(
3756
- fileSettingsManager.getGlobalSettings()
3757
- );
3758
- let projectSettingsJson = JSON.stringify({
3759
- ...projectSettings,
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
- globalSettingsJson = fn(globalSettingsJson) ?? globalSettingsJson;
3766
- } else {
3767
- projectSettingsJson = fn(projectSettingsJson) ?? projectSettingsJson;
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 SettingsManager.fromStorage(storage);
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 log2 = createLogger("pi-harness");
4320
+ var log3 = createLogger("pi-harness");
3786
4321
  var DEFAULT_ATTACHMENT_DIR = "assets/images";
3787
4322
  function extForAttachment(filename, contentType) {
3788
- const fromName = extname(filename).toLowerCase().replace(/^\./, "");
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 resourceLoader = opts.systemPromptAppend || opts.resourceLoaderOptions?.noContextFiles || opts.resourceLoaderOptions?.noSkills || (opts.resourceLoaderOptions?.additionalSkillPaths?.length ?? 0) > 0 || (opts.resourceLoaderOptions?.piPackages?.length ?? 0) > 0 ? (() => {
3839
- const agentDir = getAgentDir();
3840
- const additionalSkillPaths = opts.resourceLoaderOptions?.additionalSkillPaths ?? [];
3841
- const piPackages = opts.resourceLoaderOptions?.piPackages ?? [];
3842
- const settingsManager = createResourceSettingsManager(
3843
- ctx.workdir,
3844
- agentDir,
3845
- piPackages
3846
- );
3847
- return new DefaultResourceLoader({
3848
- cwd: ctx.workdir,
3849
- agentDir,
3850
- settingsManager,
3851
- ...opts.systemPromptAppend ? { appendSystemPrompt: [opts.systemPromptAppend] } : {},
3852
- ...opts.resourceLoaderOptions?.noContextFiles ? { noContextFiles: true } : {},
3853
- ...opts.resourceLoaderOptions?.noSkills ? { noSkills: true } : {},
3854
- ...additionalSkillPaths.length ? { additionalSkillPaths } : {},
3855
- ...opts.resourceLoaderOptions?.noSkills || additionalSkillPaths.length ? {
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, _displayText = text) {
4496
+ async followUp(sessionId, text, _attachments, displayText = text, options) {
3905
4497
  const handle = piSessions.get(sessionId);
3906
- if (!handle) return;
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(_sessionId) {
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
- nativeFollowUpPending.delete(input.sessionId);
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
- converted = namespaceInlinePartIds(dedupStartChunks(piEventToChunks(event)));
4019
- }
4020
- if (event.type === "message_update") {
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
- chunks.push(...converted);
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 id = inlineTurnIndex === 0 ? "0" : `turn-${inlineTurnIndex}:0`;
4797
+ const messageId = currentPiAssistantMessageId ?? "assistant-streaming";
4070
4798
  chunks.push(
4071
- { type: "text-start", id },
4072
- { type: "text-delta", id, delta: text },
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
- log2.error("attachment write failed", { workdir: ctx.workdir, error: msg });
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 extname2, resolve as resolve4, sep as sep3 } from "path";
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(extname2(e))).map((e) => ({ path: join7(dir, e), source }));
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 dirname6, isAbsolute as isAbsolute4, relative as relative4, resolve as resolve5 } from "path";
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(relative4(root, absolutePath));
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 = dirname6(current);
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(dirname6(absPath), target);
5175
+ const resolvedTarget = resolve5(dirname7(absPath), target);
4434
5176
  const nearestAncestor = await findNearestExistingAncestor(resolvedTarget);
4435
5177
  const realAncestor = await realpath2(nearestAncestor);
4436
- const rel2 = relative4(realRoot, realAncestor);
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(dirname6(absPath));
5197
+ const nearestAncestor = await findNearestExistingAncestor(dirname7(absPath));
4456
5198
  const realAncestor = await realpath2(nearestAncestor);
4457
- const rel2 = relative4(realRoot, realAncestor);
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 skillMarker = "/.agents/skills/";
4570
- const skillIndex = absolutePath.indexOf(skillMarker);
4571
- if (skillIndex >= 0) {
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 vercelReadOps(workspace) {
4594
- return {
5212
+ function boundFs(workspaceRoot) {
5213
+ const read = {
4595
5214
  async readFile(absolutePath) {
4596
- const rel = toRelPath(workspace, absolutePath);
4597
- const content = await workspace.readFile(rel);
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
- const rel = toRelPath(workspace, absolutePath);
4602
- await workspace.stat(rel);
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
- const rel = toRelPath(workspace, absolutePath);
4610
- await workspace.writeFile(rel, content);
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
- const rel = toRelPath(workspace, dir);
4614
- await workspace.mkdir(rel, { recursive: true });
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
- const rel = toRelPath(workspace, absolutePath);
4622
- const content = await workspace.readFile(rel);
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
- const rel = toRelPath(workspace, absolutePath);
4627
- await workspace.writeFile(rel, content);
5240
+ await assertWithinWorkspace(workspaceRoot, absolutePath);
5241
+ await writeFile8(absolutePath, content);
4628
5242
  },
4629
5243
  async access(absolutePath) {
4630
- const rel = toRelPath(workspace, absolutePath);
4631
- await workspace.stat(rel);
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
- const rel = toRelPath(workspace, absolutePath);
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 stat(absolutePath) {
4723
- const rel = toRelPath(workspace, absolutePath);
4724
- const s = await workspace.stat(rel);
4725
- return { isDirectory: () => s.kind === "dir" };
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
- function shellEscape(arg) {
4735
- return `'${arg.replace(/'/g, "'\\''")}'`;
4736
- }
4737
-
4738
- // src/server/tools/vercelGrepTool.ts
4739
- import {
4740
- createGrepToolDefinition,
4741
- formatSize,
4742
- truncateHead,
4743
- truncateLine
4744
- } from "@mariozechner/pi-coding-agent";
4745
- import { resolve as resolve6, relative as relative6 } from "path";
4746
-
4747
- // src/server/catalog/tools/_shared.ts
4748
- function makeError(message) {
4749
- return {
4750
- content: [{ type: "text", text: message }],
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
- } else if (event.type !== "context") {
4818
- continue;
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
- function buildSuccessResult(parsed, effectiveLimit, sandboxTruncated) {
4846
- if (parsed.matchCount === 0) {
4847
- return {
4848
- content: [{ type: "text", text: "No matches found" }],
4849
- details: void 0
4850
- };
4851
- }
4852
- const rawOutput = parsed.outputLines.join("\n");
4853
- const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
4854
- let output = truncation.content;
4855
- const details = {};
4856
- const notices = [];
4857
- if (parsed.matchLimitReached) {
4858
- notices.push(
4859
- `${effectiveLimit} matches limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`
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
- if (parsed.linesTruncated) {
4869
- notices.push(
4870
- `Some lines truncated to ${GREP_MAX_LINE_LENGTH} chars. Use read tool to see full lines`
4871
- );
4872
- details.linesTruncated = true;
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
- if (notices.length > 0) output += `
4875
-
4876
- [${notices.join(". ")}]`;
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
- content: [{ type: "text", text: output }],
4879
- details: Object.keys(details).length > 0 ? details : void 0
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 vercelGrepTool(sandbox, workspaceRoot) {
5342
+ function vercelReadOps(workspace) {
4883
5343
  return {
4884
- name: PI_GREP_TOOL.name,
4885
- description: PI_GREP_TOOL.description,
4886
- promptSnippet: PI_GREP_TOOL.promptSnippet,
4887
- parameters: PI_GREP_TOOL.parameters,
4888
- async execute(input, ctx) {
4889
- const params = input;
4890
- if (ctx.abortSignal.aborted) {
4891
- return makeError("grep aborted");
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
- // src/server/tools/filesystem/index.ts
4926
- function isTextContent(content) {
4927
- return content.type === "text" && typeof content.text === "string";
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 adaptPiTool(piTool) {
5367
+ function vercelEditOps(workspace) {
4930
5368
  return {
4931
- name: piTool.name,
4932
- description: piTool.description,
4933
- promptSnippet: piTool.promptSnippet,
4934
- parameters: piTool.parameters,
4935
- async execute(params, ctx) {
4936
- const result = await piTool.execute(
4937
- ctx.toolCallId,
4938
- params,
4939
- ctx.abortSignal,
4940
- ctx.onUpdate ? (update) => {
4941
- const text = update.content?.filter(isTextContent).map((c) => c.text).join("");
4942
- if (text) ctx.onUpdate?.(text);
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 buildFilesystemAgentTools(bundle) {
4956
- const cwd = bundle.workspace.root;
4957
- if (bundle.sandbox.provider === "vercel-sandbox") {
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
- const ops = boundFs(cwd);
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 bwrapSpawnHook(workspaceRoot) {
4987
- const args = buildBwrapArgs(workspaceRoot);
4988
- const bwrapPrefix = ["bwrap", ...args].map(shellEscape2).join(" ");
4989
- return (context) => ({
4990
- ...context,
4991
- command: `${bwrapPrefix} bash -lc ${shellEscape2(context.command)}`,
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 directSpawnHook(workspaceRoot) {
5000
- return (context) => ({
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 bashOptionsForMode(bundle) {
5006
- switch (bundle.sandbox.provider) {
5007
- case "vercel-sandbox":
5008
- return { operations: vercelBashOps(bundle.sandbox) };
5009
- case "bwrap":
5010
- return {
5011
- operations: createLocalBashOperations(),
5012
- spawnHook: bwrapSpawnHook(bundle.workspace.root)
5013
- };
5014
- default:
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 adaptPiTool2(piTool) {
5022
- return {
5023
- name: piTool.name,
5024
- description: piTool.description,
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 createExecuteIsolatedCodeTool(sandbox) {
5417
+ function vercelFindOps(sandbox, workspace) {
5048
5418
  return {
5049
- name: "execute_isolated_code",
5050
- description: "Execute code in an isolated sandbox environment.",
5051
- parameters: {
5052
- type: "object",
5053
- properties: {
5054
- code: { type: "string" },
5055
- language: { type: "string", enum: ["python", "shell"] },
5056
- image: { type: "string" },
5057
- packages: { type: "array", items: { type: "string" } },
5058
- sandboxId: { type: "string" },
5059
- vmSize: { type: "string", enum: ["xxs", "xs", "s", "m", "l"] }
5060
- },
5061
- required: ["code", "language"],
5062
- additionalProperties: false
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 execute(input) {
5065
- if (!sandbox.executeIsolatedCode) {
5066
- return {
5067
- content: [{ type: "text", text: "isolated-code capability is not available" }],
5068
- isError: true
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
- const code = input.code;
5072
- const language = input.language;
5073
- if (typeof code !== "string" || code.length === 0) {
5074
- return { content: [{ type: "text", text: "code is required" }], isError: true };
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 (language !== "python" && language !== "shell") {
5077
- return { content: [{ type: "text", text: "language must be python or shell" }], isError: true };
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 result = await sandbox.executeIsolatedCode({
5081
- code,
5082
- language,
5083
- image: typeof input.image === "string" ? input.image : void 0,
5084
- packages: Array.isArray(input.packages) ? input.packages.filter((v) => typeof v === "string") : void 0,
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 buildHarnessAgentTools(bundle) {
5103
- const tools = [
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/http/middleware.ts
5113
- var ERROR_CODE_AUTH_REQUIRED = "auth_required";
5114
- var ERROR_CODE_AUTH_INVALID = "auth_invalid";
5115
- var ERROR_CODE_INVALID_PATH = "invalid_path";
5116
- var ERROR_CODE_VALIDATION_ERROR = "validation_error";
5117
- var ERROR_CODE_PATH_REJECTED = "path_rejected";
5118
- var ERROR_CODE_NOT_FOUND = "not_found";
5119
- var ERROR_CODE_ALREADY_EXISTS = "already_exists";
5120
- var ERROR_CODE_CONFLICT = "conflict";
5121
- var ERROR_CODE_RANGE_NOT_SATISFIABLE = "range_not_satisfiable";
5122
- var ERROR_CODE_INTERNAL = "internal";
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
- error: {
5129
- code,
5130
- message,
5131
- ...field ? { field } : {}
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
- function ensureWorkspaceContext(request, workspaceId, authenticated) {
5136
- request.workspaceContext = {
5137
- workspaceId,
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
- if (!authToken) {
5151
- if (!warnedDevMode) {
5152
- warnedDevMode = true;
5153
- request.log.warn(DEV_MODE_WARNING);
5154
- opts.onDevModeWarning?.(DEV_MODE_WARNING);
5559
+ const isMatch = event.type === "match";
5560
+ if (isMatch) {
5561
+ matchCount += 1;
5562
+ if (matchCount > limit) {
5563
+ matchLimitReached = true;
5564
+ break;
5155
5565
  }
5156
- return;
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
- ensureWorkspaceContext(request, workspaceId, true);
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 createBodyValidator(schema) {
5177
- return async function validateBody(request, reply) {
5178
- const parsed = schema.safeParse(request.body);
5179
- if (!parsed.success) {
5180
- const firstIssue = parsed.error.issues[0];
5181
- const fieldName = firstIssue?.path?.map((segment) => String(segment)).join(".");
5182
- reply.code(400).send(
5183
- errorPayload(
5184
- ERROR_CODE_VALIDATION_ERROR,
5185
- firstIssue?.message ?? "Invalid request body",
5186
- fieldName || void 0
5187
- )
5188
- );
5189
- return;
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
- // src/server/http/routes/health.ts
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
- status: "ok",
5201
- version: opts.version,
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
- if (code === "EEXIST" || message.includes("EEXIST")) {
5254
- return reply.code(409).send({
5255
- error: { code: ERROR_CODE_ALREADY_EXISTS, message: `${subject} already exists` }
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
- return reply.code(500).send({
5259
- error: { code: ERROR_CODE_INTERNAL, message }
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 (value.includes("\0")) {
5270
- reply.code(400).send({
5271
- error: { code: ERROR_CODE_INVALID_PATH, message: "null bytes not allowed", field }
5272
- });
5273
- return null;
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
- return value;
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 parseWorkspaceSettings(raw) {
5278
- try {
5279
- const parsed = JSON.parse(raw);
5280
- const dir = parsed?.markdown?.imageUploadDir;
5281
- return {
5282
- ...parsed,
5283
- markdown: {
5284
- ...parsed.markdown ?? {},
5285
- imageUploadDir: typeof dir === "string" && dir.trim() ? dir.trim() : DEFAULT_MARKDOWN_IMAGE_UPLOAD_DIR
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
- } catch {
5289
- return defaultWorkspaceSettings();
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
- async function readWorkspaceSettings(workspace) {
5293
- try {
5294
- return parseWorkspaceSettings(await workspace.readFile(BORING_SETTINGS_PATH));
5295
- } catch (error) {
5296
- const code = error?.code;
5297
- if (code === "ENOENT") return defaultWorkspaceSettings();
5298
- throw error;
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
- function normalizeUploadDir(value) {
5302
- if (typeof value !== "string") return null;
5303
- const dir = value.trim().replace(/^\.\/+/, "").replace(/\/+/g, "/");
5304
- if (!dir || dir.includes("\0") || dir.startsWith("/") || dir.split("/").includes("..")) return null;
5305
- return dir.replace(/\/+$/, "");
5306
- }
5307
- function extForUpload(filename, contentType) {
5308
- const fromName = extname3(filename).toLowerCase().replace(/^\./, "");
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 basenameForUpload(filename) {
5318
- const base = filename.split("/").pop()?.split("\\").pop() ?? "image";
5319
- const withoutExt = base.replace(/\.[^.]*$/, "");
5320
- const safe = withoutExt.normalize("NFKD").replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
5321
- return safe || "image";
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 markdownUrlFor(sourcePath, assetPath) {
5324
- if (!sourcePath) return assetPath;
5325
- const fromDir = dirname7(sourcePath.replace(/\\/g, "/"));
5326
- const rel = relative7(fromDir === "." ? "" : fromDir, assetPath);
5327
- return rel && !rel.startsWith(".") ? rel : rel || assetPath;
5748
+ function directSpawnHook(workspaceRoot) {
5749
+ return (context) => ({
5750
+ ...context,
5751
+ env: withWorkspacePythonEnv({ workspaceRoot, env: context.env })
5752
+ });
5328
5753
  }
5329
- function contentTypeForPath(path4) {
5330
- switch (extname3(path4).toLowerCase()) {
5331
- case ".avif":
5332
- return "image/avif";
5333
- case ".gif":
5334
- return "image/gif";
5335
- case ".jpg":
5336
- case ".jpeg":
5337
- return "image/jpeg";
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 "application/octet-stream";
5764
+ return {
5765
+ operations: createLocalBashOperations(),
5766
+ spawnHook: directSpawnHook(bundle.workspace.root)
5767
+ };
5357
5768
  }
5358
5769
  }
5359
- function fileRoutes(app, opts, done) {
5360
- async function resolveWorkspace(request) {
5361
- if (opts.getWorkspace) return await opts.getWorkspace(request);
5362
- if (opts.workspace) return opts.workspace;
5363
- throw new Error("file route requires workspace or getWorkspace");
5364
- }
5365
- app.get("/api/v1/files/raw", async (request, reply) => {
5366
- const query = request.query;
5367
- const path4 = requireStringParam(query.path, "path", reply);
5368
- if (path4 === null) return;
5369
- try {
5370
- const workspace = await resolveWorkspace(request);
5371
- if (!workspace.readBinaryFile) {
5372
- return reply.code(501).send({
5373
- error: { code: ERROR_CODE_INTERNAL, message: "workspace does not support binary reads" }
5374
- });
5375
- }
5376
- const stat7 = await workspace.stat(path4);
5377
- if (stat7.kind !== "file") {
5378
- return reply.code(400).send({
5379
- error: { code: ERROR_CODE_VALIDATION_ERROR, message: "path is not a file", field: "path" }
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
- const expectedMtimeMs = typeof body.expectedMtimeMs === "number" ? body.expectedMtimeMs : null;
5415
- try {
5416
- const workspace = await resolveWorkspace(request);
5417
- if (expectedMtimeMs !== null) {
5418
- try {
5419
- const current = await workspace.stat(path4);
5420
- if (current.kind === "file" && current.mtimeMs !== expectedMtimeMs) {
5421
- return reply.code(409).send({
5422
- error: {
5423
- code: ERROR_CODE_CONFLICT,
5424
- message: "file has been modified since last read",
5425
- currentMtimeMs: current.mtimeMs,
5426
- expectedMtimeMs
5427
- }
5428
- });
5429
- }
5430
- } catch (statErr) {
5431
- const code = statErr?.code;
5432
- if (code === "ENOENT") {
5433
- return reply.code(409).send({
5434
- error: {
5435
- code: ERROR_CODE_CONFLICT,
5436
- message: "file no longer exists",
5437
- expectedMtimeMs
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
- if (body.createDirs) {
5445
- const dir = path4.includes("/") ? path4.slice(0, path4.lastIndexOf("/")) : void 0;
5446
- if (dir) await workspace.mkdir(dir, { recursive: true });
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
- const content = body.content;
5449
- const stat7 = workspace.writeFileWithStat ? await workspace.writeFileWithStat(path4, content) : await (async () => {
5450
- await workspace.writeFile(path4, content);
5451
- return await workspace.stat(path4);
5452
- })();
5453
- return { ok: true, mtimeMs: stat7.kind === "file" ? stat7.mtimeMs : void 0 };
5454
- } catch (err) {
5455
- return classifyError(err, reply, "file");
5456
- }
5457
- });
5458
- app.post("/api/v1/files/upload", async (request, reply) => {
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.put("/api/v1/workspace-settings", async (request, reply) => {
5512
- const body = request.body;
5513
- const incoming = body?.settings ?? body;
5514
- const markdown = incoming.markdown ?? {};
5515
- const imageUploadDir = normalizeUploadDir(markdown.imageUploadDir);
5516
- if (!imageUploadDir) {
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
- try {
5522
- const workspace = await resolveWorkspace(request);
5523
- const current = await readWorkspaceSettings(workspace);
5524
- const next = {
5525
- ...current,
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
- await runtime.harness.followUp?.(
6235
- sessionId,
6236
- body.message,
6237
- parsedAttachments.data,
6238
- typeof body.displayText === "string" && body.displayText.length > 0 ? body.displayText : body.message
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
- runtime.harness.clearFollowUp?.(sessionId);
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 : models.find((m) => m.available);
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 { loadSkills as loadSkills2 } from "@mariozechner/pi-coding-agent";
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
- let cached = null;
6333
- app.get("/api/v1/agent/skills", (_request, reply) => {
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 result = loadSkills2({
6340
- cwd: opts.workspaceRoot,
6341
- skillPaths: opts.additionalSkillPaths ?? [],
6342
- includeDefaults: true
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 = { skills, expiresAt: now + CACHE_TTL_MS2 };
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 runtimeBundle = await resolveMode(resolvedMode).create({
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 (resolvedMode !== "vercel-sandbox") {
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 === "/api/v1/agent/models" || pathname === "/api/v1/agent/skills" || pathname === "/api/v1/ready-status";
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
- key: `${resolvedMode}:${workspaceId}:${root}`
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 createRuntimeBinding(workspaceId, root, request) {
7312
- const scopedTemplatePath = opts.getTemplatePath ? await opts.getTemplatePath({ workspaceId, workspaceRoot: root, request }) : templatePath;
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: scopedTemplatePath
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 (resolvedMode !== "vercel-sandbox") {
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.root, request);
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.root);
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,