@hachej/boring-agent 0.1.13 → 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,7 +2698,7 @@ 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
 
@@ -2364,7 +2905,7 @@ import { createGzip } from "zlib";
2364
2905
  import { put } from "@vercel/blob";
2365
2906
  var urlCache = /* @__PURE__ */ new Map();
2366
2907
  var LOG_PREFIX = "[template-tarball]";
2367
- function log(msg, meta = {}) {
2908
+ function log2(msg, meta = {}) {
2368
2909
  const metaStr = Object.keys(meta).length > 0 ? ` ${JSON.stringify(meta)}` : "";
2369
2910
  process.stderr.write(`${LOG_PREFIX} ${msg}${metaStr}
2370
2911
  `);
@@ -2447,11 +2988,11 @@ async function packageTemplate(templatePath, opts = {}) {
2447
2988
  const hash = computeTemplateHash(files);
2448
2989
  const cached = urlCache.get(hash);
2449
2990
  if (cached) {
2450
- log("cache hit", { hash, fileCount: files.length });
2991
+ log2("cache hit", { hash, fileCount: files.length });
2451
2992
  return { url: cached, hash };
2452
2993
  }
2453
2994
  const tarball = await buildTarGz(files);
2454
- log("tarball built", {
2995
+ log2("tarball built", {
2455
2996
  hash,
2456
2997
  fileCount: files.length,
2457
2998
  sizeBytes: tarball.length,
@@ -2460,7 +3001,7 @@ async function packageTemplate(templatePath, opts = {}) {
2460
3001
  const upload = opts.uploadFn ?? defaultUpload;
2461
3002
  const url = await upload(hash, tarball);
2462
3003
  urlCache.set(hash, url);
2463
- log("uploaded", { hash, url, totalMs: Date.now() - startMs });
3004
+ log2("uploaded", { hash, url, totalMs: Date.now() - startMs });
2464
3005
  return { url, hash };
2465
3006
  }
2466
3007
 
@@ -2736,101 +3277,21 @@ import Fastify from "fastify";
2736
3277
  import { basename as basename2 } from "path";
2737
3278
 
2738
3279
  // src/server/harness/pi-coding-agent/createHarness.ts
3280
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
2739
3281
  import { mkdir as mkdir9, writeFile as writeFile7 } from "fs/promises";
2740
- import { extname, join as join6 } from "path";
3282
+ import { extname as extname2, join as join6 } from "path";
2741
3283
  import {
2742
- createAgentSession,
2743
- SessionManager,
2744
- AuthStorage,
2745
- ModelRegistry,
2746
- DefaultResourceLoader,
2747
- SettingsManager,
2748
- getAgentDir,
2749
- loadSkills
2750
- } from "@mariozechner/pi-coding-agent";
2751
-
2752
- // src/server/logging.ts
2753
- var SENSITIVE_KEYS = new Set([
2754
- "apiKey",
2755
- "api_key",
2756
- "token",
2757
- "secret",
2758
- "password",
2759
- "authorization",
2760
- "cookie",
2761
- "oidcToken",
2762
- "accessToken",
2763
- "refreshToken",
2764
- "ANTHROPIC_API_KEY",
2765
- "OPENAI_API_KEY",
2766
- "VERCEL_OIDC_TOKEN",
2767
- "VERCEL_TEAM_ID"
2768
- ].map((key) => key.toLowerCase()));
2769
- function isSensitiveKey(key) {
2770
- return SENSITIVE_KEYS.has(key.toLowerCase());
2771
- }
2772
- function redactValue(key, value, seen) {
2773
- if (key && isSensitiveKey(key) && value != null) return "***";
2774
- if (value == null || typeof value !== "object") return value;
2775
- if (value instanceof Date) return value;
2776
- if (seen.has(value)) return "[Circular]";
2777
- seen.add(value);
2778
- if (Array.isArray(value)) {
2779
- const out2 = value.map((item) => redactValue(void 0, item, seen));
2780
- seen.delete(value);
2781
- return out2;
2782
- }
2783
- const out = {};
2784
- for (const [childKey, childValue] of Object.entries(value)) {
2785
- out[childKey] = redactValue(childKey, childValue, seen);
2786
- }
2787
- seen.delete(value);
2788
- return out;
2789
- }
2790
- function redact(fields) {
2791
- const out = {};
2792
- const seen = /* @__PURE__ */ new WeakSet();
2793
- for (const [k, v] of Object.entries(fields)) {
2794
- out[k] = redactValue(k, v, seen);
2795
- }
2796
- return out;
2797
- }
2798
- var verbose = typeof process !== "undefined" && process.env?.BORING_AGENT_VERBOSE === "1";
2799
- function createLogger(prefix) {
2800
- function emit(level, msg, fields) {
2801
- const entry = {
2802
- level,
2803
- prefix,
2804
- msg,
2805
- ...fields ? redact(fields) : {},
2806
- t: (/* @__PURE__ */ new Date()).toISOString()
2807
- };
2808
- if (level === "error") {
2809
- console.error(JSON.stringify(entry));
2810
- } else if (level === "warn") {
2811
- console.warn(JSON.stringify(entry));
2812
- } else {
2813
- console.log(JSON.stringify(entry));
2814
- }
2815
- }
2816
- return {
2817
- debug(msg, fields) {
2818
- if (verbose) emit("debug", msg, fields);
2819
- },
2820
- info(msg, fields) {
2821
- emit("info", msg, fields);
2822
- },
2823
- warn(msg, fields) {
2824
- emit("warn", msg, fields);
2825
- },
2826
- error(msg, fields) {
2827
- emit("error", msg, fields);
2828
- }
2829
- };
2830
- }
3284
+ createAgentSession,
3285
+ SessionManager,
3286
+ AuthStorage,
3287
+ ModelRegistry,
3288
+ DefaultResourceLoader,
3289
+ SettingsManager as SettingsManager2,
3290
+ getAgentDir as getAgentDir2
3291
+ } from "@mariozechner/pi-coding-agent";
2831
3292
 
2832
3293
  // src/server/harness/pi-coding-agent/tool-adapter.ts
2833
- function adaptToolForPi(tool) {
3294
+ function adaptToolForPi(tool, sessionId) {
2834
3295
  return {
2835
3296
  name: tool.name,
2836
3297
  label: tool.name,
@@ -2841,7 +3302,8 @@ function adaptToolForPi(tool) {
2841
3302
  const result = await tool.execute(params, {
2842
3303
  toolCallId,
2843
3304
  abortSignal: signal ?? new AbortController().signal,
2844
- 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
2845
3307
  });
2846
3308
  if (result.isError) {
2847
3309
  throw new Error(result.content.map((c) => c.text).join("\n"));
@@ -2853,8 +3315,8 @@ function adaptToolForPi(tool) {
2853
3315
  }
2854
3316
  };
2855
3317
  }
2856
- function adaptToolsForPi(tools) {
2857
- return tools.map(adaptToolForPi);
3318
+ function adaptToolsForPi(tools, sessionId) {
3319
+ return tools.map((tool) => adaptToolForPi(tool, sessionId));
2858
3320
  }
2859
3321
 
2860
3322
  // src/server/harness/pi-coding-agent/stream-adapter.ts
@@ -3064,12 +3526,24 @@ function defaultSessionDir(cwd) {
3064
3526
  return join5(homedir3(), ".pi", "agent", "sessions", safePath);
3065
3527
  }
3066
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
+ }
3067
3537
  var PiSessionStore = class {
3068
3538
  cwd;
3069
3539
  sessionDir;
3070
- constructor(cwd, sessionDir) {
3540
+ constructor(cwd, options) {
3071
3541
  this.cwd = cwd;
3072
- 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));
3073
3547
  }
3074
3548
  async list(ctx) {
3075
3549
  const files = await readdir4(this.sessionDir).catch(() => []);
@@ -3505,7 +3979,7 @@ function createSessionTitleScheduler(opts) {
3505
3979
  if (!detail || detail.turnCount !== 1) return;
3506
3980
  if (hasCustomTitle(detail.title)) return;
3507
3981
  const fallbackTitle = formatFallbackTitle(getNow());
3508
- const apiKey = opts.getApiKey?.()?.trim() || getEnv("ANTHROPIC_API_KEY")?.trim() || "";
3982
+ const apiKey = opts.getApiKey?.()?.trim() || "";
3509
3983
  if (!apiKey || !fetchImpl) {
3510
3984
  opts.writeTitle(input.sessionId, fallbackTitle);
3511
3985
  return;
@@ -3533,6 +4007,7 @@ function createSessionTitleScheduler(opts) {
3533
4007
  }
3534
4008
 
3535
4009
  // src/server/models/modelConfig.ts
4010
+ import { getAgentDir, SettingsManager } from "@mariozechner/pi-coding-agent";
3536
4011
  var INFOMANIAK_PROVIDER = "infomaniak";
3537
4012
  var INFOMANIAK_API_BASE = "https://api.infomaniak.com";
3538
4013
  var DEFAULT_CUSTOM_MODEL_MAX_TOKENS = 16384;
@@ -3661,6 +4136,16 @@ function registerConfiguredModelProviders(registry) {
3661
4136
  }
3662
4137
  return registered;
3663
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
+ }
3664
4149
  function readConfiguredDefaultModel() {
3665
4150
  const encoded = clean(getEnv("BORING_AGENT_DEFAULT_MODEL"));
3666
4151
  if (encoded) {
@@ -3674,7 +4159,7 @@ function readConfiguredDefaultModel() {
3674
4159
  if (explicitProvider && explicitId) {
3675
4160
  return { provider: explicitProvider, id: explicitId };
3676
4161
  }
3677
- return readInfomaniakProvider()?.model ?? readCustomProvider()?.model;
4162
+ return readPiSettingsDefaultModel() ?? readInfomaniakProvider()?.model ?? readCustomProvider()?.model;
3678
4163
  }
3679
4164
 
3680
4165
  // src/server/piPackages.ts
@@ -3720,6 +4205,17 @@ function mergePiPackageSources(base = [], additional = []) {
3720
4205
  }
3721
4206
 
3722
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
+ }
3723
4219
  function extractUserMessageText(message) {
3724
4220
  const record = message;
3725
4221
  if (record?.role !== "user") return "";
@@ -3748,6 +4244,16 @@ function findLastAssistantMessage(messages) {
3748
4244
  }
3749
4245
  return void 0;
3750
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
+ }
3751
4257
  function resolveRequestedModel(modelRegistry, input) {
3752
4258
  const requestedId = input.model?.id;
3753
4259
  if (!input.model || !requestedId) return void 0;
@@ -3767,27 +4273,37 @@ function resolveDefaultModel(modelRegistry) {
3767
4273
  }
3768
4274
  return void 0;
3769
4275
  }
3770
- function createResourceSettingsManager(cwd, agentDir, piPackages) {
3771
- const fileSettingsManager = SettingsManager.create(cwd, agentDir);
3772
- if (piPackages.length === 0) return fileSettingsManager;
3773
- const projectSettings = fileSettingsManager.getProjectSettings();
3774
- let globalSettingsJson = JSON.stringify(
3775
- fileSettingsManager.getGlobalSettings()
3776
- );
3777
- let projectSettingsJson = JSON.stringify({
3778
- ...projectSettings,
3779
- 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)
3780
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;
3781
4293
  const storage = {
3782
4294
  withLock(scope, fn) {
3783
4295
  if (scope === "global") {
3784
- globalSettingsJson = fn(globalSettingsJson) ?? globalSettingsJson;
3785
- } else {
3786
- projectSettingsJson = fn(projectSettingsJson) ?? projectSettingsJson;
4296
+ const current2 = globalSettingsOverrideJson ?? readSettingsFileIfPresent(globalSettingsPath);
4297
+ const next2 = fn(current2);
4298
+ if (next2 !== void 0) globalSettingsOverrideJson = next2;
4299
+ return;
3787
4300
  }
4301
+ const current = projectSettingsOverrideJson ?? mergeInjectedProjectPackages(readSettingsFileIfPresent(projectSettingsPath), piPackages);
4302
+ const next = fn(current);
4303
+ if (next !== void 0) projectSettingsOverrideJson = next;
3788
4304
  }
3789
4305
  };
3790
- return SettingsManager.fromStorage(storage);
4306
+ return SettingsManager2.fromStorage(storage);
3791
4307
  }
3792
4308
  async function applyRequestedSessionOptions(handle, input) {
3793
4309
  const requestedModel = resolveRequestedModel(handle.modelRegistry, input);
@@ -3801,10 +4317,10 @@ async function applyRequestedSessionOptions(handle, input) {
3801
4317
  handle.piSession.setThinkingLevel(input.thinkingLevel);
3802
4318
  }
3803
4319
  }
3804
- var log2 = createLogger("pi-harness");
4320
+ var log3 = createLogger("pi-harness");
3805
4321
  var DEFAULT_ATTACHMENT_DIR = "assets/images";
3806
4322
  function extForAttachment(filename, contentType) {
3807
- const fromName = extname(filename).toLowerCase().replace(/^\./, "");
4323
+ const fromName = extname2(filename).toLowerCase().replace(/^\./, "");
3808
4324
  if (/^[a-z0-9]{1,12}$/.test(fromName)) return fromName;
3809
4325
  if (contentType === "image/jpeg") return "jpg";
3810
4326
  if (contentType === "image/png") return "png";
@@ -3820,7 +4336,10 @@ function basenameForAttachment(filename) {
3820
4336
  return safe || "image";
3821
4337
  }
3822
4338
  function createPiCodingAgentHarness(opts) {
3823
- const sessionStore = new PiSessionStore(opts.cwd);
4339
+ const sessionStore = new PiSessionStore(opts.cwd, {
4340
+ sessionNamespace: opts.sessionNamespace,
4341
+ sessionDir: opts.sessionDir
4342
+ });
3824
4343
  const piSessions = /* @__PURE__ */ new Map();
3825
4344
  const scheduleSessionTitle = createSessionTitleScheduler({
3826
4345
  loadSession: (sessionId) => sessionStore.load({ workspaceId: "default" }, sessionId),
@@ -3854,38 +4373,29 @@ function createPiCodingAgentHarness(opts) {
3854
4373
  }
3855
4374
  const resolvedModel = resolveRequestedModel(modelRegistry, input);
3856
4375
  const model = resolvedModel ?? resolveDefaultModel(modelRegistry);
3857
- const resourceLoader = opts.systemPromptAppend || opts.resourceLoaderOptions?.noContextFiles || opts.resourceLoaderOptions?.noSkills || (opts.resourceLoaderOptions?.additionalSkillPaths?.length ?? 0) > 0 || (opts.resourceLoaderOptions?.piPackages?.length ?? 0) > 0 ? (() => {
3858
- const agentDir = getAgentDir();
3859
- const additionalSkillPaths = opts.resourceLoaderOptions?.additionalSkillPaths ?? [];
3860
- const piPackages = opts.resourceLoaderOptions?.piPackages ?? [];
3861
- const settingsManager = createResourceSettingsManager(
3862
- ctx.workdir,
3863
- agentDir,
3864
- piPackages
3865
- );
3866
- return new DefaultResourceLoader({
3867
- cwd: ctx.workdir,
3868
- agentDir,
3869
- settingsManager,
3870
- ...opts.systemPromptAppend ? { appendSystemPrompt: [opts.systemPromptAppend] } : {},
3871
- ...opts.resourceLoaderOptions?.noContextFiles ? { noContextFiles: true } : {},
3872
- ...opts.resourceLoaderOptions?.noSkills ? { noSkills: true } : {},
3873
- ...additionalSkillPaths.length ? { additionalSkillPaths } : {},
3874
- ...opts.resourceLoaderOptions?.noSkills || additionalSkillPaths.length ? {
3875
- skillsOverride: () => loadSkills({
3876
- cwd: ctx.workdir,
3877
- agentDir,
3878
- skillPaths: additionalSkillPaths,
3879
- includeDefaults: false
3880
- })
3881
- } : {}
3882
- });
3883
- })() : 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
+ });
3884
4394
  await resourceLoader?.reload();
3885
4395
  const { session: piSession } = await createAgentSession({
3886
4396
  cwd: ctx.workdir,
3887
4397
  tools: [],
3888
- customTools: adaptToolsForPi(opts.tools),
4398
+ customTools: adaptToolsForPi(opts.tools, input.sessionId),
3889
4399
  model,
3890
4400
  thinkingLevel: input.thinkingLevel ?? "off",
3891
4401
  sessionManager,
@@ -3909,6 +4419,7 @@ function createPiCodingAgentHarness(opts) {
3909
4419
  if (!handle) return;
3910
4420
  handle.piSession.dispose();
3911
4421
  piSessions.delete(sessionId);
4422
+ clearNativeFollowUpWork(sessionId);
3912
4423
  }
3913
4424
  const originalDelete = sessionStore.delete.bind(sessionStore);
3914
4425
  sessionStore.delete = async (ctx, sessionId) => {
@@ -3916,17 +4427,95 @@ function createPiCodingAgentHarness(opts) {
3916
4427
  disposePiSession(sessionId);
3917
4428
  };
3918
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
+ }
3919
4492
  return {
3920
4493
  id: "pi-coding-agent",
3921
4494
  placement: "server",
3922
4495
  sessions: sessionStore,
3923
- async followUp(sessionId, text, _attachments, _displayText = text) {
4496
+ async followUp(sessionId, text, _attachments, displayText = text, options) {
3924
4497
  const handle = piSessions.get(sessionId);
3925
- 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);
3926
4507
  nativeFollowUpPending.add(sessionId);
3927
4508
  await handle.piSession.followUp(text);
3928
4509
  },
3929
- 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);
3930
4519
  },
3931
4520
  /**
3932
4521
  * Pi exposes the resolved system prompt as a getter on AgentSession.
@@ -3992,6 +4581,27 @@ function createPiCodingAgentHarness(opts) {
3992
4581
  }
3993
4582
  let sawTextChunk = false;
3994
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
+ }
3995
4605
  function namespaceInlinePartIds(input2) {
3996
4606
  if (inlineTurnIndex === 0) return input2;
3997
4607
  return input2.map((chunk2) => {
@@ -4002,6 +4612,23 @@ function createPiCodingAgentHarness(opts) {
4002
4612
  return chunk2;
4003
4613
  });
4004
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
+ }
4005
4632
  let promptSettled = false;
4006
4633
  let promptPromise = Promise.resolve();
4007
4634
  const unsubscribe = piSession.subscribe((event) => {
@@ -4024,56 +4651,138 @@ function createPiCodingAgentHarness(opts) {
4024
4651
  if (activeTools.size === 0) stopHeartbeat();
4025
4652
  }
4026
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
+ }
4027
4750
  if (event.type === "message_start" && event.message?.role === "user") {
4028
4751
  const text = extractUserMessageText(event.message);
4029
- 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;
4030
4753
  if (text && text !== input.message) {
4031
4754
  inlineTurnIndex += 1;
4032
- 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
+ }
4033
4767
  }
4034
4768
  } else if (event.type === "message_end" && event.message?.role === "user") {
4035
- converted = [];
4769
+ converted = piHistoryChunks;
4036
4770
  } else {
4037
- converted = namespaceInlinePartIds(dedupStartChunks(piEventToChunks(event)));
4038
- }
4039
- if (event.type === "message_update") {
4040
- const ame = event.assistantMessageEvent;
4041
- if (ame.type === "text_end" && typeof ame.content === "string" && ame.content.length > 0 && !textDeltaSeen.has(ame.contentIndex)) {
4042
- const id = inlineTurnIndex === 0 ? String(ame.contentIndex) : `turn-${inlineTurnIndex}:${ame.contentIndex}`;
4043
- converted = [
4044
- ...textStartSeen.has(ame.contentIndex) ? [] : [{ type: "text-start", id }],
4045
- { type: "text-delta", id, delta: ame.content },
4046
- ...converted
4047
- ];
4048
- textDeltaSeen.add(ame.contentIndex);
4049
- assistantText += ame.content;
4050
- }
4771
+ const sdkChunks = namespaceInlinePartIds(dedupStartChunks(piEventToChunks(event)));
4772
+ const sdkChunksForTurn = filterSdkChunksForCurrentSegment(sdkChunks);
4773
+ converted = [...piHistoryChunks, ...sdkChunksForTurn];
4051
4774
  }
4052
4775
  for (const chunk2 of converted) {
4053
4776
  const t = chunk2.type;
4054
- if (t === "text-delta") {
4777
+ if (t === "text-delta" || t === "data-pi-text-delta" || t === "data-pi-text-end") {
4055
4778
  sawTextChunk = true;
4056
4779
  }
4057
- }
4058
- chunks.push(...converted);
4059
- if (event.type === "message_end" && !sawTextChunk) {
4060
- const { role, text, errorText } = extractAssistantMessageText(
4061
- event.message
4062
- );
4063
- if (role === "assistant" && errorText.length > 0) {
4064
- chunks.push({ type: "error", errorText });
4065
- sawTextChunk = true;
4066
- } else if (role === "assistant" && text.length > 0) {
4067
- const id = inlineTurnIndex === 0 ? "0" : `turn-${inlineTurnIndex}:0`;
4068
- chunks.push(
4069
- { type: "text-start", id },
4070
- { type: "text-delta", id, delta: text },
4071
- { type: "text-end", id }
4072
- );
4780
+ const piEndData = chunk2.data;
4781
+ if (t === "data-pi-message-end" && piEndData?.role === "assistant" && typeof piEndData.text === "string" && piEndData.text.length > 0) {
4073
4782
  sawTextChunk = true;
4074
- assistantText += text;
4075
4783
  }
4076
4784
  }
4785
+ chunks.push(...converted);
4077
4786
  if (event.type === "agent_end") {
4078
4787
  if (!sawTextChunk) {
4079
4788
  const { role, text, errorText } = extractAssistantMessageText(
@@ -4085,11 +4794,10 @@ function createPiCodingAgentHarness(opts) {
4085
4794
  chunks.push({ type: "error", errorText });
4086
4795
  sawTextChunk = true;
4087
4796
  } else if (role === "assistant" && text.length > 0) {
4088
- const id = inlineTurnIndex === 0 ? "0" : `turn-${inlineTurnIndex}:0`;
4797
+ const messageId = currentPiAssistantMessageId ?? "assistant-streaming";
4089
4798
  chunks.push(
4090
- { type: "text-start", id },
4091
- { type: "text-delta", id, delta: text },
4092
- { 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 } }
4093
4801
  );
4094
4802
  sawTextChunk = true;
4095
4803
  assistantText += text;
@@ -4117,6 +4825,11 @@ function createPiCodingAgentHarness(opts) {
4117
4825
  continue;
4118
4826
  }
4119
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
+ }
4120
4833
  const bytes = Buffer.from(b64, "base64");
4121
4834
  const ext = extForAttachment(a.filename ?? "image", contentType);
4122
4835
  const base = basenameForAttachment(a.filename ?? "image");
@@ -4127,7 +4840,7 @@ function createPiCodingAgentHarness(opts) {
4127
4840
  savedPaths.push(relPath);
4128
4841
  } catch (err) {
4129
4842
  const msg = err instanceof Error ? err.message : String(err);
4130
- log2.error("attachment write failed", { workdir: ctx.workdir, error: msg });
4843
+ log3.error("attachment write failed", { workdir: ctx.workdir, error: msg });
4131
4844
  writeErrors.push(`${a.filename ?? "image"}: ${msg}`);
4132
4845
  }
4133
4846
  }
@@ -4151,6 +4864,12 @@ ${attachmentNotes.join("\n")}` : message,
4151
4864
  const prepared = await prepareTurn(message, attachments);
4152
4865
  return piSession.prompt(prepared.message, prepared.promptOpts).then(() => {
4153
4866
  promptSettled = true;
4867
+ if (!done && nativeFollowUpPending.has(input.sessionId)) {
4868
+ clearNativeFollowUpWork(input.sessionId);
4869
+ done = true;
4870
+ stopHeartbeat();
4871
+ if (wake) wake();
4872
+ }
4154
4873
  }).catch((err) => {
4155
4874
  promptSettled = true;
4156
4875
  streamError = err;
@@ -4207,7 +4926,7 @@ ${attachmentNotes.join("\n")}` : message,
4207
4926
 
4208
4927
  // src/server/harness/pi-coding-agent/pluginLoader.ts
4209
4928
  import { readdir as readdir5, stat as stat5, readFile as readFile7 } from "fs/promises";
4210
- 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";
4211
4930
  import { homedir as homedir4 } from "os";
4212
4931
  import { pathToFileURL } from "url";
4213
4932
  var VALID_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".mjs"]);
@@ -4233,7 +4952,7 @@ async function fileExists2(path4) {
4233
4952
  async function discoverFromDir(dir, source) {
4234
4953
  if (!await dirExists(dir)) return [];
4235
4954
  const entries = await readdir5(dir);
4236
- 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 }));
4237
4956
  }
4238
4957
  function extractTools(mod) {
4239
4958
  const tools = [];
@@ -4282,6 +5001,10 @@ async function loadNpmPlugin(pkgDir, importFn) {
4282
5001
  if (resolvedMain !== resolvedPkgDir && !resolvedMain.startsWith(resolvedPkgDir + sep3)) {
4283
5002
  return [];
4284
5003
  }
5004
+ const relPath = relative5(resolvedPkgDir, resolvedMain);
5005
+ if (relPath.startsWith("..")) {
5006
+ return [];
5007
+ }
4285
5008
  return loadModule(resolvedMain, importFn);
4286
5009
  }
4287
5010
  async function loadExtensionsJson(cwd) {
@@ -4363,7 +5086,7 @@ import {
4363
5086
  // src/server/tools/operations/bound.ts
4364
5087
  import { constants as constants3 } from "fs";
4365
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";
4366
- 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";
4367
5090
  function toPosixPath(value) {
4368
5091
  return value.split("\\").join("/");
4369
5092
  }
@@ -4420,7 +5143,7 @@ async function walkMatches(root, current, pattern, ignore, limit, out) {
4420
5143
  for (const entry of entries) {
4421
5144
  if (out.length >= limit) return;
4422
5145
  const absolutePath = resolve5(current, entry.name);
4423
- const relativePath = toPosixPath(relative4(root, absolutePath));
5146
+ const relativePath = toPosixPath(relative6(root, absolutePath));
4424
5147
  if (entry.isDirectory() && shouldSkipDir(relativePath, ignore)) continue;
4425
5148
  if (matchesGlob(relativePath, pattern)) {
4426
5149
  out.push(absolutePath);
@@ -4437,7 +5160,7 @@ async function findNearestExistingAncestor(absPath) {
4437
5160
  await stat6(current);
4438
5161
  return current;
4439
5162
  } catch {
4440
- const parent = dirname6(current);
5163
+ const parent = dirname7(current);
4441
5164
  if (parent === current) return current;
4442
5165
  current = parent;
4443
5166
  }
@@ -4449,10 +5172,10 @@ async function assertWithinWorkspace(workspaceRoot, absPath) {
4449
5172
  const s = await lstat3(absPath);
4450
5173
  if (s.isSymbolicLink()) {
4451
5174
  const target = await readlink(absPath);
4452
- const resolvedTarget = resolve5(dirname6(absPath), target);
5175
+ const resolvedTarget = resolve5(dirname7(absPath), target);
4453
5176
  const nearestAncestor = await findNearestExistingAncestor(resolvedTarget);
4454
5177
  const realAncestor = await realpath2(nearestAncestor);
4455
- const rel2 = relative4(realRoot, realAncestor);
5178
+ const rel2 = relative6(realRoot, realAncestor);
4456
5179
  if (rel2.startsWith("..") || isAbsolute4(rel2)) {
4457
5180
  throw new Error(`path "${absPath}" is outside workspace`);
4458
5181
  }
@@ -4462,1156 +5185,704 @@ async function assertWithinWorkspace(workspaceRoot, absPath) {
4462
5185
  if (err.message?.includes("is outside workspace")) {
4463
5186
  throw err;
4464
5187
  }
4465
- const code = err.code;
4466
- if (code !== "ENOENT") throw err;
4467
- }
4468
- let realCandidate;
4469
- try {
4470
- realCandidate = await realpath2(absPath);
4471
- } catch (err) {
4472
- const code = err.code;
4473
- if (code === "ENOENT") {
4474
- const nearestAncestor = await findNearestExistingAncestor(dirname6(absPath));
4475
- const realAncestor = await realpath2(nearestAncestor);
4476
- const rel2 = relative4(realRoot, realAncestor);
4477
- if (rel2.startsWith("..") || isAbsolute4(rel2)) {
4478
- throw new Error(`path "${absPath}" is outside workspace`);
4479
- }
4480
- return;
4481
- }
4482
- throw err;
4483
- }
4484
- const rel = relative4(realRoot, realCandidate);
4485
- if (rel.startsWith("..") || isAbsolute4(rel)) {
4486
- throw new Error(`path "${absPath}" is outside workspace`);
4487
- }
4488
- }
4489
- function boundFs(workspaceRoot) {
4490
- const read = {
4491
- async readFile(absolutePath) {
4492
- await assertWithinWorkspace(workspaceRoot, absolutePath);
4493
- return await readFile8(absolutePath);
4494
- },
4495
- async access(absolutePath) {
4496
- await assertWithinWorkspace(workspaceRoot, absolutePath);
4497
- await access3(absolutePath, constants3.R_OK);
4498
- }
4499
- };
4500
- const write = {
4501
- async writeFile(absolutePath, content) {
4502
- await assertWithinWorkspace(workspaceRoot, absolutePath);
4503
- await mkdir10(dirname6(absolutePath), { recursive: true });
4504
- await writeFile8(absolutePath, content);
4505
- },
4506
- async mkdir(dir) {
4507
- await assertWithinWorkspace(workspaceRoot, dir);
4508
- await mkdir10(dir, { recursive: true });
4509
- }
4510
- };
4511
- const edit = {
4512
- async readFile(absolutePath) {
4513
- await assertWithinWorkspace(workspaceRoot, absolutePath);
4514
- return await readFile8(absolutePath);
4515
- },
4516
- async writeFile(absolutePath, content) {
4517
- await assertWithinWorkspace(workspaceRoot, absolutePath);
4518
- await writeFile8(absolutePath, content);
4519
- },
4520
- async access(absolutePath) {
4521
- await assertWithinWorkspace(workspaceRoot, absolutePath);
4522
- await access3(absolutePath, constants3.R_OK | constants3.W_OK);
4523
- }
4524
- };
4525
- const find = {
4526
- async exists(absolutePath) {
4527
- await assertWithinWorkspace(workspaceRoot, absolutePath);
4528
- try {
4529
- await stat6(absolutePath);
4530
- return true;
4531
- } catch (err) {
4532
- if (err.code === "ENOENT") return false;
4533
- throw err;
4534
- }
4535
- },
4536
- async glob(pattern, cwd, options) {
4537
- await assertWithinWorkspace(workspaceRoot, cwd);
4538
- const matches = [];
4539
- await walkMatches(cwd, cwd, pattern, options.ignore, options.limit, matches);
4540
- return matches;
4541
- }
4542
- };
4543
- const grep = {
4544
- async isDirectory(absolutePath) {
4545
- await assertWithinWorkspace(workspaceRoot, absolutePath);
4546
- return (await stat6(absolutePath)).isDirectory();
4547
- },
4548
- async readFile(absolutePath) {
4549
- await assertWithinWorkspace(workspaceRoot, absolutePath);
4550
- return await readFile8(absolutePath, "utf8");
4551
- }
4552
- };
4553
- const ls = {
4554
- async exists(absolutePath) {
4555
- try {
4556
- await assertWithinWorkspace(workspaceRoot, absolutePath);
4557
- await stat6(absolutePath);
4558
- return true;
4559
- } catch {
4560
- return false;
4561
- }
4562
- },
4563
- async stat(absolutePath) {
4564
- await assertWithinWorkspace(workspaceRoot, absolutePath);
4565
- return await stat6(absolutePath);
4566
- },
4567
- async readdir(absolutePath) {
4568
- await assertWithinWorkspace(workspaceRoot, absolutePath);
4569
- return await readdir6(absolutePath);
4570
- }
4571
- };
4572
- return { read, write, edit, find, grep, ls };
4573
- }
4574
-
4575
- // src/server/tools/operations/vercel.ts
4576
- import { isAbsolute as isAbsolute5, relative as relative5 } from "path";
4577
- function rootAliases(workspace) {
4578
- const aliases = [workspace.root];
4579
- if (workspace.root === VERCEL_SANDBOX_WORKSPACE_ROOT) aliases.push(VERCEL_SANDBOX_REMOTE_ROOT);
4580
- if (workspace.root === VERCEL_SANDBOX_REMOTE_ROOT) aliases.push(VERCEL_SANDBOX_WORKSPACE_ROOT);
4581
- return aliases;
4582
- }
4583
- function isOutsideWorkspaceRel(rel) {
4584
- return rel === ".." || rel.startsWith("../") || rel.startsWith("..\\") || isAbsolute5(rel);
4585
- }
4586
- function toRelPath(workspace, absolutePath) {
4587
- for (const root of rootAliases(workspace)) {
4588
- const rel = relative5(root, absolutePath);
4589
- if (!isOutsideWorkspaceRel(rel)) return rel;
4590
- }
4591
- const skillMarker = "/.agents/skills/";
4592
- const skillIndex = absolutePath.indexOf(skillMarker);
4593
- if (skillIndex >= 0) {
4594
- const skillPath = absolutePath.slice(skillIndex + skillMarker.length);
4595
- if (skillPath.includes("\0") || skillPath.split(/[\\/]+/).includes("..")) {
4596
- throw new Error(`path "${absolutePath}" escapes the workspace skills directory`);
4597
- }
4598
- return `.agents/skills/${skillPath}`;
4599
- }
4600
- throw new Error(
4601
- `path "${absolutePath}" is outside workspace; use a path relative to the workspace root or under ${rootAliases(workspace).join(" / ")}`
4602
- );
4603
- }
4604
- function vercelBashOps(sandbox) {
4605
- return {
4606
- exec(command, cwd, { onData, signal, timeout, env }) {
4607
- const filteredEnv = env ? Object.fromEntries(Object.entries(env).filter((e) => e[1] != null)) : void 0;
4608
- return sandbox.exec(command, {
4609
- cwd,
4610
- env: filteredEnv,
4611
- signal,
4612
- timeoutMs: timeout ? timeout * 1e3 : void 0,
4613
- onStdout: (chunk2) => onData(Buffer.from(chunk2)),
4614
- onStderr: (chunk2) => onData(Buffer.from(chunk2))
4615
- }).then((result) => ({ exitCode: result.exitCode }));
4616
- }
4617
- };
5188
+ const code = err.code;
5189
+ if (code !== "ENOENT") throw err;
5190
+ }
5191
+ let realCandidate;
5192
+ try {
5193
+ realCandidate = await realpath2(absPath);
5194
+ } catch (err) {
5195
+ const code = err.code;
5196
+ if (code === "ENOENT") {
5197
+ const nearestAncestor = await findNearestExistingAncestor(dirname7(absPath));
5198
+ const realAncestor = await realpath2(nearestAncestor);
5199
+ const rel2 = relative6(realRoot, realAncestor);
5200
+ if (rel2.startsWith("..") || isAbsolute4(rel2)) {
5201
+ throw new Error(`path "${absPath}" is outside workspace`);
5202
+ }
5203
+ return;
5204
+ }
5205
+ throw err;
5206
+ }
5207
+ const rel = relative6(realRoot, realCandidate);
5208
+ if (rel.startsWith("..") || isAbsolute4(rel)) {
5209
+ throw new Error(`path "${absPath}" is outside workspace`);
5210
+ }
4618
5211
  }
4619
- function vercelReadOps(workspace) {
4620
- return {
5212
+ function boundFs(workspaceRoot) {
5213
+ const read = {
4621
5214
  async readFile(absolutePath) {
4622
- const rel = toRelPath(workspace, absolutePath);
4623
- const content = await workspace.readFile(rel);
4624
- return Buffer.from(content, "utf-8");
5215
+ await assertWithinWorkspace(workspaceRoot, absolutePath);
5216
+ return await readFile8(absolutePath);
4625
5217
  },
4626
5218
  async access(absolutePath) {
4627
- const rel = toRelPath(workspace, absolutePath);
4628
- await workspace.stat(rel);
5219
+ await assertWithinWorkspace(workspaceRoot, absolutePath);
5220
+ await access3(absolutePath, constants3.R_OK);
4629
5221
  }
4630
5222
  };
4631
- }
4632
- function vercelWriteOps(workspace) {
4633
- return {
5223
+ const write = {
4634
5224
  async writeFile(absolutePath, content) {
4635
- const rel = toRelPath(workspace, absolutePath);
4636
- await workspace.writeFile(rel, content);
5225
+ await assertWithinWorkspace(workspaceRoot, absolutePath);
5226
+ await mkdir10(dirname7(absolutePath), { recursive: true });
5227
+ await writeFile8(absolutePath, content);
4637
5228
  },
4638
5229
  async mkdir(dir) {
4639
- const rel = toRelPath(workspace, dir);
4640
- await workspace.mkdir(rel, { recursive: true });
5230
+ await assertWithinWorkspace(workspaceRoot, dir);
5231
+ await mkdir10(dir, { recursive: true });
4641
5232
  }
4642
5233
  };
4643
- }
4644
- function vercelEditOps(workspace) {
4645
- return {
5234
+ const edit = {
4646
5235
  async readFile(absolutePath) {
4647
- const rel = toRelPath(workspace, absolutePath);
4648
- const content = await workspace.readFile(rel);
4649
- return Buffer.from(content, "utf-8");
5236
+ await assertWithinWorkspace(workspaceRoot, absolutePath);
5237
+ return await readFile8(absolutePath);
4650
5238
  },
4651
5239
  async writeFile(absolutePath, content) {
4652
- const rel = toRelPath(workspace, absolutePath);
4653
- await workspace.writeFile(rel, content);
5240
+ await assertWithinWorkspace(workspaceRoot, absolutePath);
5241
+ await writeFile8(absolutePath, content);
4654
5242
  },
4655
5243
  async access(absolutePath) {
4656
- const rel = toRelPath(workspace, absolutePath);
4657
- await workspace.stat(rel);
5244
+ await assertWithinWorkspace(workspaceRoot, absolutePath);
5245
+ await access3(absolutePath, constants3.R_OK | constants3.W_OK);
4658
5246
  }
4659
5247
  };
4660
- }
4661
- function toRemotePath2(value) {
4662
- if (value === VERCEL_SANDBOX_WORKSPACE_ROOT) return VERCEL_SANDBOX_REMOTE_ROOT;
4663
- if (value.startsWith(`${VERCEL_SANDBOX_WORKSPACE_ROOT}/`)) {
4664
- return `${VERCEL_SANDBOX_REMOTE_ROOT}${value.slice(VERCEL_SANDBOX_WORKSPACE_ROOT.length)}`;
4665
- }
4666
- return value;
4667
- }
4668
- function findPredicate(pattern) {
4669
- const isPathShaped = pattern.includes("/") || pattern.includes("**");
4670
- if (!isPathShaped) return `-name ${shellEscape(pattern)}`;
4671
- let translated = pattern.replaceAll("**", "*").replace(/^\/+/, "");
4672
- if (!translated.startsWith("*")) translated = `*${translated}`;
4673
- return `-path ${shellEscape(translated)}`;
4674
- }
4675
- function findIgnoreArgs(cwd, ignore) {
4676
- return ignore.map((pattern) => `! -path ${shellEscape(`${cwd}/${pattern.replace(/^\/+/, "")}/*`)}`).join(" ");
4677
- }
4678
- function fallbackFindCommand(pattern, cwd, options) {
4679
- return [
4680
- "find",
4681
- shellEscape(cwd),
4682
- "-maxdepth 20",
4683
- "-type f",
4684
- findIgnoreArgs(cwd, options.ignore),
4685
- findPredicate(pattern),
4686
- `| head -n ${Math.max(1, Math.trunc(options.limit))}`
4687
- ].filter(Boolean).join(" ");
4688
- }
4689
- function isFdMissing(result) {
4690
- if (result.exitCode !== 127) return false;
4691
- const stderr = Buffer.from(result.stderr).toString("utf-8");
4692
- return /\bfd: (?:not found|command not found)\b/i.test(stderr);
4693
- }
4694
- function vercelFindOps(sandbox, workspace) {
4695
- return {
5248
+ const find = {
4696
5249
  async exists(absolutePath) {
4697
- if (workspace) {
4698
- try {
4699
- const rel = toRelPath(workspace, absolutePath);
4700
- await workspace.stat(rel);
4701
- return true;
4702
- } catch {
4703
- return false;
4704
- }
5250
+ await assertWithinWorkspace(workspaceRoot, absolutePath);
5251
+ try {
5252
+ await stat6(absolutePath);
5253
+ return true;
5254
+ } catch (err) {
5255
+ if (err.code === "ENOENT") return false;
5256
+ throw err;
4705
5257
  }
4706
- const result = await sandbox.exec(`test -e ${shellEscape(absolutePath)}`, {
4707
- timeoutMs: 5e3
4708
- });
4709
- return result.exitCode === 0;
4710
5258
  },
4711
5259
  async glob(pattern, cwd, options) {
4712
- const remoteCwd = toRemotePath2(cwd);
4713
- const args = ["fd", "--glob", "--no-require-git", "--max-results", String(options.limit)];
4714
- for (const ig of options.ignore) {
4715
- args.push("--exclude", ig);
4716
- }
4717
- args.push(pattern, remoteCwd);
4718
- let result = await sandbox.exec(args.map(shellEscape).join(" "), {
4719
- timeoutMs: 3e4,
4720
- maxOutputBytes: 1048576
4721
- });
4722
- if (isFdMissing(result)) {
4723
- result = await sandbox.exec(fallbackFindCommand(pattern, remoteCwd, options), {
4724
- timeoutMs: 3e4,
4725
- maxOutputBytes: 1048576
4726
- });
4727
- }
4728
- if (result.exitCode !== 0 && result.exitCode !== 1) {
4729
- const stderr = Buffer.from(result.stderr).toString("utf-8").trim();
4730
- throw new Error(`file search failed (exit ${result.exitCode}): ${stderr}`);
4731
- }
4732
- const stdout = Buffer.from(result.stdout).toString("utf-8");
4733
- return stdout.split("\n").filter(Boolean);
5260
+ await assertWithinWorkspace(workspaceRoot, cwd);
5261
+ const matches = [];
5262
+ await walkMatches(cwd, cwd, pattern, options.ignore, options.limit, matches);
5263
+ return matches;
4734
5264
  }
4735
5265
  };
4736
- }
4737
- function vercelLsOps(workspace) {
4738
- return {
4739
- async exists(absolutePath) {
4740
- try {
4741
- const rel = toRelPath(workspace, absolutePath);
4742
- await workspace.stat(rel);
4743
- return true;
4744
- } catch {
4745
- return false;
4746
- }
4747
- },
4748
- async stat(absolutePath) {
4749
- const rel = toRelPath(workspace, absolutePath);
4750
- const s = await workspace.stat(rel);
4751
- return { isDirectory: () => s.kind === "dir" };
5266
+ const grep = {
5267
+ async isDirectory(absolutePath) {
5268
+ await assertWithinWorkspace(workspaceRoot, absolutePath);
5269
+ return (await stat6(absolutePath)).isDirectory();
4752
5270
  },
4753
- async readdir(absolutePath) {
4754
- const rel = toRelPath(workspace, absolutePath);
4755
- const entries = await workspace.readdir(rel);
4756
- return entries.map((e) => e.name);
5271
+ async readFile(absolutePath) {
5272
+ await assertWithinWorkspace(workspaceRoot, absolutePath);
5273
+ return await readFile8(absolutePath, "utf8");
4757
5274
  }
4758
5275
  };
4759
- }
4760
- function shellEscape(arg) {
4761
- return `'${arg.replace(/'/g, "'\\''")}'`;
4762
- }
4763
-
4764
- // src/server/tools/vercelGrepTool.ts
4765
- import {
4766
- createGrepToolDefinition,
4767
- formatSize,
4768
- truncateHead,
4769
- truncateLine
4770
- } from "@mariozechner/pi-coding-agent";
4771
- import { resolve as resolve6, relative as relative6 } from "path";
4772
-
4773
- // src/server/catalog/tools/_shared.ts
4774
- function makeError(message) {
4775
- return {
4776
- content: [{ type: "text", text: message }],
4777
- isError: true
4778
- };
4779
- }
4780
- function bytesWritten(content) {
4781
- return new TextEncoder().encode(content).byteLength;
4782
- }
4783
- var decoder = new TextDecoder("utf-8", { fatal: false });
4784
- function decode(bytes) {
4785
- return decoder.decode(bytes);
4786
- }
4787
-
4788
- // src/server/tools/vercelGrepTool.ts
4789
- var PI_GREP_TOOL = createGrepToolDefinition("/");
4790
- var DEFAULT_LIMIT2 = 100;
4791
- var GREP_MAX_LINE_LENGTH = 500;
4792
- var GREP_TIMEOUT_MS = 3e4;
4793
- var GREP_MAX_OUTPUT_BYTES = 2097152;
4794
- function quoteShell(value) {
4795
- return `'${value.replace(/'/g, `'\\''`)}'`;
4796
- }
4797
- function numberParam(value, fallback, min = 1) {
4798
- if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
4799
- return Math.max(min, Math.floor(value));
4800
- }
4801
- function optionalStringParam(value) {
4802
- return typeof value === "string" && value.length > 0 ? value : void 0;
4803
- }
4804
- function buildRgCommand(params) {
4805
- const args = ["--json", "--line-number", "--color=never", "--hidden"];
4806
- if (params.ignoreCase === true) args.push("--ignore-case");
4807
- if (params.literal === true) args.push("--fixed-strings");
4808
- const glob = optionalStringParam(params.glob);
4809
- if (glob) args.push("--glob", quoteShell(glob));
4810
- const context = numberParam(params.context, 0, 0);
4811
- if (context > 0) args.push("--context", String(context));
4812
- const pattern = params.pattern;
4813
- const searchPath = optionalStringParam(params.path) ?? ".";
4814
- args.push("--", quoteShell(pattern), quoteShell(searchPath));
4815
- return `rg ${args.join(" ")}`;
4816
- }
4817
- function parseRgJson(stdout, limit) {
4818
- const outputLines = [];
4819
- let matchCount = 0;
4820
- let matchLimitReached = false;
4821
- let linesTruncated = false;
4822
- for (const rawLine of stdout.split("\n")) {
4823
- if (rawLine.trim().length === 0) continue;
4824
- let event;
4825
- try {
4826
- event = JSON.parse(rawLine);
4827
- } catch {
4828
- continue;
4829
- }
4830
- const filePath = event.data?.path?.text;
4831
- const lineNumber = event.data?.line_number;
4832
- const lineText = event.data?.lines?.text;
4833
- if (!filePath || typeof lineNumber !== "number" || typeof lineText !== "string") {
4834
- continue;
4835
- }
4836
- const isMatch = event.type === "match";
4837
- if (isMatch) {
4838
- matchCount += 1;
4839
- if (matchCount > limit) {
4840
- matchLimitReached = true;
4841
- break;
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;
4842
5284
  }
4843
- } else if (event.type !== "context") {
4844
- 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);
4845
5293
  }
4846
- const sanitized = lineText.replace(/\r\n/g, "\n").replace(/\r/g, "").replace(/\n$/, "");
4847
- const { text, wasTruncated } = truncateLine(sanitized);
4848
- if (wasTruncated) linesTruncated = true;
4849
- const separator = isMatch ? ":" : "-";
4850
- outputLines.push(`${filePath}${separator}${lineNumber}${separator} ${text}`);
4851
- }
4852
- return { outputLines, matchCount, matchLimitReached, linesTruncated };
4853
- }
4854
- function syntheticExecTruncation(output) {
4855
- const outputLines = output.length === 0 ? 0 : output.split("\n").length;
4856
- const outputBytes = bytesWritten(output);
4857
- return {
4858
- content: output,
4859
- truncated: true,
4860
- truncatedBy: "bytes",
4861
- totalLines: outputLines,
4862
- totalBytes: outputBytes,
4863
- outputLines,
4864
- outputBytes,
4865
- lastLinePartial: false,
4866
- firstLineExceedsLimit: false,
4867
- maxLines: Number.MAX_SAFE_INTEGER,
4868
- maxBytes: GREP_MAX_OUTPUT_BYTES
4869
5294
  };
5295
+ return { read, write, edit, find, grep, ls };
4870
5296
  }
4871
- function buildSuccessResult(parsed, effectiveLimit, sandboxTruncated) {
4872
- if (parsed.matchCount === 0) {
4873
- return {
4874
- content: [{ type: "text", text: "No matches found" }],
4875
- details: void 0
4876
- };
4877
- }
4878
- const rawOutput = parsed.outputLines.join("\n");
4879
- const truncation = truncateHead(rawOutput, { maxLines: Number.MAX_SAFE_INTEGER });
4880
- let output = truncation.content;
4881
- const details = {};
4882
- const notices = [];
4883
- if (parsed.matchLimitReached) {
4884
- notices.push(
4885
- `${effectiveLimit} matches limit reached. Use limit=${effectiveLimit * 2} for more, or refine pattern`
4886
- );
4887
- details.matchLimitReached = effectiveLimit;
4888
- }
4889
- if (truncation.truncated || sandboxTruncated) {
4890
- const effectiveTruncation = truncation.truncated ? truncation : syntheticExecTruncation(output);
4891
- notices.push(`${formatSize(effectiveTruncation.maxBytes)} limit reached`);
4892
- 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;
4893
5313
  }
4894
- if (parsed.linesTruncated) {
4895
- notices.push(
4896
- `Some lines truncated to ${GREP_MAX_LINE_LENGTH} chars. Use read tool to see full lines`
4897
- );
4898
- 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}`;
4899
5322
  }
4900
- if (notices.length > 0) output += `
4901
-
4902
- [${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) {
4903
5328
  return {
4904
- content: [{ type: "text", text: output }],
4905
- 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
+ }
4906
5340
  };
4907
5341
  }
4908
- function vercelGrepTool(sandbox, workspaceRoot) {
5342
+ function vercelReadOps(workspace) {
4909
5343
  return {
4910
- name: PI_GREP_TOOL.name,
4911
- description: PI_GREP_TOOL.description,
4912
- promptSnippet: PI_GREP_TOOL.promptSnippet,
4913
- parameters: PI_GREP_TOOL.parameters,
4914
- async execute(input, ctx) {
4915
- const params = input;
4916
- if (ctx.abortSignal.aborted) {
4917
- return makeError("grep aborted");
4918
- }
4919
- if (typeof params.pattern !== "string" || params.pattern.length === 0) {
4920
- return makeError("pattern is required");
4921
- }
4922
- if (workspaceRoot && typeof params.path === "string" && params.path.length > 0) {
4923
- const resolved = resolve6(workspaceRoot, params.path);
4924
- const rel = relative6(workspaceRoot, resolved);
4925
- if (rel.startsWith("..") || rel.startsWith("/")) {
4926
- return makeError(`path "${params.path}" is outside workspace`);
4927
- }
4928
- }
4929
- try {
4930
- const result = await sandbox.exec(buildRgCommand(params), {
4931
- signal: ctx.abortSignal,
4932
- timeoutMs: GREP_TIMEOUT_MS,
4933
- maxOutputBytes: GREP_MAX_OUTPUT_BYTES
4934
- });
4935
- if (result.exitCode !== 0 && result.exitCode !== 1) {
4936
- const stderr = decode(result.stderr).trim();
4937
- const message = stderr || `ripgrep exited with code ${result.exitCode}`;
4938
- return makeError(`grep failed: ${message}`);
4939
- }
4940
- const limit = numberParam(params.limit, DEFAULT_LIMIT2);
4941
- const parsed = parseRgJson(decode(result.stdout), limit);
4942
- return buildSuccessResult(parsed, limit, result.truncated);
4943
- } catch (error) {
4944
- const message = error instanceof Error ? error.message : "unknown error";
4945
- return makeError(`grep failed: ${message}`);
4946
- }
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);
4947
5352
  }
4948
5353
  };
4949
5354
  }
4950
-
4951
- // src/server/tools/filesystem/index.ts
4952
- function isTextContent(content) {
4953
- 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
+ };
4954
5366
  }
4955
- function adaptPiTool(piTool) {
5367
+ function vercelEditOps(workspace) {
4956
5368
  return {
4957
- name: piTool.name,
4958
- description: piTool.description,
4959
- promptSnippet: piTool.promptSnippet,
4960
- parameters: piTool.parameters,
4961
- async execute(params, ctx) {
4962
- const result = await piTool.execute(
4963
- ctx.toolCallId,
4964
- params,
4965
- ctx.abortSignal,
4966
- ctx.onUpdate ? (update) => {
4967
- const text = update.content?.filter(isTextContent).map((c) => c.text).join("");
4968
- if (text) ctx.onUpdate?.(text);
4969
- } : void 0,
4970
- {}
4971
- );
4972
- const textContent = (result.content ?? []).filter(isTextContent).map((c) => ({ type: "text", text: c.text }));
4973
- return {
4974
- content: textContent.length > 0 ? textContent : [{ type: "text", text: "" }],
4975
- isError: false,
4976
- details: result.details
4977
- };
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);
4978
5381
  }
4979
5382
  };
4980
5383
  }
4981
- function buildFilesystemAgentTools(bundle) {
4982
- const cwd = bundle.workspace.root;
4983
- if (bundle.sandbox.provider === "vercel-sandbox") {
4984
- return [
4985
- adaptPiTool(createReadToolDefinition(cwd, { operations: vercelReadOps(bundle.workspace) })),
4986
- adaptPiTool(createWriteToolDefinition(cwd, { operations: vercelWriteOps(bundle.workspace) })),
4987
- adaptPiTool(createEditToolDefinition(cwd, { operations: vercelEditOps(bundle.workspace) })),
4988
- adaptPiTool(createFindToolDefinition(cwd, { operations: vercelFindOps(bundle.sandbox, bundle.workspace) })),
4989
- vercelGrepTool(bundle.sandbox, cwd),
4990
- adaptPiTool(createLsToolDefinition(cwd, { operations: vercelLsOps(bundle.workspace) }))
4991
- ];
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)}`;
4992
5388
  }
4993
- const ops = boundFs(cwd);
4994
- return [
4995
- adaptPiTool(createReadToolDefinition(cwd, { operations: ops.read })),
4996
- adaptPiTool(createWriteToolDefinition(cwd, { operations: ops.write })),
4997
- adaptPiTool(createEditToolDefinition(cwd, { operations: ops.edit })),
4998
- adaptPiTool(createFindToolDefinition(cwd, { operations: ops.find })),
4999
- adaptPiTool(createGrepToolDefinition2(cwd, { operations: ops.grep })),
5000
- adaptPiTool(createLsToolDefinition(cwd, { operations: ops.ls }))
5001
- ];
5002
- }
5003
-
5004
- // src/server/tools/harness/index.ts
5005
- import {
5006
- createBashToolDefinition,
5007
- createLocalBashOperations
5008
- } from "@mariozechner/pi-coding-agent";
5009
- function shellEscape2(s) {
5010
- return `'${s.replace(/'/g, "'\\''")}'`;
5389
+ return value;
5011
5390
  }
5012
- function bwrapSpawnHook(workspaceRoot) {
5013
- const args = buildBwrapArgs(workspaceRoot);
5014
- const bwrapPrefix = ["bwrap", ...args].map(shellEscape2).join(" ");
5015
- return (context) => ({
5016
- ...context,
5017
- command: `${bwrapPrefix} bash -lc ${shellEscape2(context.command)}`,
5018
- env: withWorkspacePythonEnv({
5019
- workspaceRoot,
5020
- env: context.env,
5021
- sandboxRoot: "/workspace"
5022
- })
5023
- });
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)}`;
5024
5397
  }
5025
- function directSpawnHook(workspaceRoot) {
5026
- return (context) => ({
5027
- ...context,
5028
- env: withWorkspacePythonEnv({ workspaceRoot, env: context.env })
5029
- });
5398
+ function findIgnoreArgs(cwd, ignore) {
5399
+ return ignore.map((pattern) => `! -path ${shellEscape(`${cwd}/${pattern.replace(/^\/+/, "")}/*`)}`).join(" ");
5030
5400
  }
5031
- function bashOptionsForMode(bundle) {
5032
- switch (bundle.sandbox.provider) {
5033
- case "vercel-sandbox":
5034
- return { operations: vercelBashOps(bundle.sandbox) };
5035
- case "bwrap":
5036
- return {
5037
- operations: createLocalBashOperations(),
5038
- spawnHook: bwrapSpawnHook(bundle.workspace.root)
5039
- };
5040
- default:
5041
- return {
5042
- operations: createLocalBashOperations(),
5043
- spawnHook: directSpawnHook(bundle.workspace.root)
5044
- };
5045
- }
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(" ");
5046
5411
  }
5047
- function adaptPiTool2(piTool) {
5048
- return {
5049
- name: piTool.name,
5050
- description: piTool.description,
5051
- promptSnippet: piTool.promptSnippet,
5052
- parameters: piTool.parameters,
5053
- async execute(params, ctx) {
5054
- const result = await piTool.execute(
5055
- ctx.toolCallId,
5056
- params,
5057
- ctx.abortSignal,
5058
- ctx.onUpdate ? (update) => {
5059
- const text = update.content.filter((c) => c.type === "text").map((c) => c.text).join("");
5060
- ctx.onUpdate(text);
5061
- } : void 0,
5062
- {}
5063
- );
5064
- const textContent = (result.content ?? []).filter((c) => c.type === "text").map((c) => ({ type: "text", text: c.text }));
5065
- return {
5066
- content: textContent.length > 0 ? textContent : [{ type: "text", text: "" }],
5067
- isError: false,
5068
- details: result.details
5069
- };
5070
- }
5071
- };
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);
5072
5416
  }
5073
- function createExecuteIsolatedCodeTool(sandbox) {
5417
+ function vercelFindOps(sandbox, workspace) {
5074
5418
  return {
5075
- name: "execute_isolated_code",
5076
- description: "Execute code in an isolated sandbox environment.",
5077
- parameters: {
5078
- type: "object",
5079
- properties: {
5080
- code: { type: "string" },
5081
- language: { type: "string", enum: ["python", "shell"] },
5082
- image: { type: "string" },
5083
- packages: { type: "array", items: { type: "string" } },
5084
- sandboxId: { type: "string" },
5085
- vmSize: { type: "string", enum: ["xxs", "xs", "s", "m", "l"] }
5086
- },
5087
- required: ["code", "language"],
5088
- 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;
5089
5433
  },
5090
- async execute(input) {
5091
- if (!sandbox.executeIsolatedCode) {
5092
- return {
5093
- content: [{ type: "text", text: "isolated-code capability is not available" }],
5094
- isError: true
5095
- };
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);
5096
5439
  }
5097
- const code = input.code;
5098
- const language = input.language;
5099
- if (typeof code !== "string" || code.length === 0) {
5100
- 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
+ });
5101
5450
  }
5102
- if (language !== "python" && language !== "shell") {
5103
- 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}`);
5104
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) {
5105
5463
  try {
5106
- const result = await sandbox.executeIsolatedCode({
5107
- code,
5108
- language,
5109
- image: typeof input.image === "string" ? input.image : void 0,
5110
- packages: Array.isArray(input.packages) ? input.packages.filter((v) => typeof v === "string") : void 0,
5111
- sandboxId: typeof input.sandboxId === "string" ? input.sandboxId : void 0,
5112
- vmSize: input.vmSize === "xxs" || input.vmSize === "xs" || input.vmSize === "s" || input.vmSize === "m" || input.vmSize === "l" ? input.vmSize : void 0
5113
- });
5114
- return {
5115
- content: [{ type: "text", text: JSON.stringify(result) }],
5116
- isError: result.exitCode !== 0,
5117
- details: result
5118
- };
5119
- } catch (error) {
5120
- return {
5121
- content: [{ type: "text", text: error instanceof Error ? error.message : "execute_isolated_code failed" }],
5122
- isError: true
5123
- };
5464
+ const rel = toRelPath(workspace, absolutePath);
5465
+ await workspace.stat(rel);
5466
+ return true;
5467
+ } catch {
5468
+ return false;
5124
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);
5125
5480
  }
5126
5481
  };
5127
5482
  }
5128
- function buildHarnessAgentTools(bundle) {
5129
- const tools = [
5130
- adaptPiTool2(createBashToolDefinition(bundle.workspace.root, bashOptionsForMode(bundle)))
5131
- ];
5132
- if (bundle.sandbox.capabilities.includes("isolated-code")) {
5133
- tools.push(createExecuteIsolatedCodeTool(bundle.sandbox));
5134
- }
5135
- return tools;
5483
+ function shellEscape(arg) {
5484
+ return `'${arg.replace(/'/g, "'\\''")}'`;
5136
5485
  }
5137
5486
 
5138
- // src/server/http/middleware.ts
5139
- var ERROR_CODE_AUTH_REQUIRED = "auth_required";
5140
- var ERROR_CODE_AUTH_INVALID = "auth_invalid";
5141
- var ERROR_CODE_INVALID_PATH = "invalid_path";
5142
- var ERROR_CODE_VALIDATION_ERROR = "validation_error";
5143
- var ERROR_CODE_PATH_REJECTED = "path_rejected";
5144
- var ERROR_CODE_NOT_FOUND = "not_found";
5145
- var ERROR_CODE_ALREADY_EXISTS = "already_exists";
5146
- var ERROR_CODE_CONFLICT = "conflict";
5147
- var ERROR_CODE_RANGE_NOT_SATISFIABLE = "range_not_satisfiable";
5148
- var ERROR_CODE_INTERNAL = "internal";
5149
- var ERROR_CODE_NOT_IMPLEMENTED = "not_implemented";
5150
- var DEV_MODE_WARNING = "No auth token set \u2014 running in dev mode";
5151
- var DEFAULT_WORKSPACE_ID = "default";
5152
- 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) {
5153
5498
  return {
5154
- error: {
5155
- code,
5156
- message,
5157
- ...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;
5158
5552
  }
5159
- };
5160
- }
5161
- function ensureWorkspaceContext(request, workspaceId, authenticated) {
5162
- request.workspaceContext = {
5163
- workspaceId,
5164
- authenticated
5165
- };
5166
- }
5167
- function createAuthMiddleware(opts = {}) {
5168
- let warnedDevMode = false;
5169
- return async function authMiddleware(request, reply) {
5170
- const workspaceId = opts.workspaceId ?? DEFAULT_WORKSPACE_ID;
5171
- const authToken = opts.authToken?.trim() || void 0;
5172
- ensureWorkspaceContext(request, workspaceId, false);
5173
- if (opts.publicPaths?.includes(request.url.split("?")[0])) {
5174
- 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;
5175
5558
  }
5176
- if (!authToken) {
5177
- if (!warnedDevMode) {
5178
- warnedDevMode = true;
5179
- request.log.warn(DEV_MODE_WARNING);
5180
- 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;
5181
5565
  }
5182
- return;
5183
- }
5184
- const header = request.headers.authorization;
5185
- if (typeof header !== "string" || !header.startsWith("Bearer ")) {
5186
- reply.code(401).send(
5187
- errorPayload(
5188
- ERROR_CODE_AUTH_REQUIRED,
5189
- "Missing Bearer token"
5190
- )
5191
- );
5192
- return;
5193
- }
5194
- const candidateToken = header.slice("Bearer ".length);
5195
- if (candidateToken !== authToken) {
5196
- reply.code(403).send(errorPayload(ERROR_CODE_AUTH_INVALID, "Invalid token"));
5197
- return;
5566
+ } else if (event.type !== "context") {
5567
+ continue;
5198
5568
  }
5199
- ensureWorkspaceContext(request, workspaceId, true);
5200
- };
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 };
5201
5576
  }
5202
- function createBodyValidator(schema) {
5203
- return async function validateBody(request, reply) {
5204
- const parsed = schema.safeParse(request.body);
5205
- if (!parsed.success) {
5206
- const firstIssue = parsed.error.issues[0];
5207
- const fieldName = firstIssue?.path?.map((segment) => String(segment)).join(".");
5208
- reply.code(400).send(
5209
- errorPayload(
5210
- ERROR_CODE_VALIDATION_ERROR,
5211
- firstIssue?.message ?? "Invalid request body",
5212
- fieldName || void 0
5213
- )
5214
- );
5215
- return;
5216
- }
5217
- 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
5218
5592
  };
5219
5593
  }
5220
-
5221
- // src/server/http/routes/health.ts
5222
- function healthRoutes(app, opts, done) {
5223
- const startTime = Date.now();
5224
- app.get("/health", async () => {
5594
+ function buildSuccessResult(parsed, effectiveLimit, sandboxTruncated) {
5595
+ if (parsed.matchCount === 0) {
5225
5596
  return {
5226
- status: "ok",
5227
- version: opts.version,
5228
- uptime: Math.floor((Date.now() - startTime) / 1e3)
5597
+ content: [{ type: "text", text: "No matches found" }],
5598
+ details: void 0
5229
5599
  };
5230
- });
5231
- app.get("/ready", async (_request, reply) => {
5232
- const state = opts.getReadiness();
5233
- if (state.degradedReason) {
5234
- return reply.code(503).send({
5235
- status: "degraded",
5236
- reason: state.degradedReason
5237
- });
5238
- }
5239
- if (!state.sandboxReady || !state.harnessReady) {
5240
- return reply.code(503).send({
5241
- status: "provisioning",
5242
- retryAfter: 2
5243
- });
5244
- }
5245
- return { status: "ready" };
5246
- });
5247
- done();
5248
- }
5249
-
5250
- // src/server/http/routes/file.ts
5251
- import { dirname as dirname7, extname as extname3, relative as relative7 } from "path/posix";
5252
- var BORING_SETTINGS_PATH = ".boring/settings";
5253
- var DEFAULT_MARKDOWN_IMAGE_UPLOAD_DIR = "assets/images";
5254
- var MAX_UPLOAD_BYTES = 10 * 1024 * 1024;
5255
- function defaultWorkspaceSettings() {
5256
- return { markdown: { imageUploadDir: DEFAULT_MARKDOWN_IMAGE_UPLOAD_DIR } };
5257
- }
5258
- function isPathValidationError(err) {
5259
- return err instanceof Error && typeof err.reason === "string";
5260
- }
5261
- function classifyError(err, reply, subject) {
5262
- if (isPathValidationError(err)) {
5263
- return reply.code(403).send({
5264
- error: { code: ERROR_CODE_PATH_REJECTED, message: "path traversal rejected" }
5265
- });
5266
- }
5267
- const message = err instanceof Error ? err.message : "unknown error";
5268
- const code = err?.code;
5269
- if (code === "EPERM" || message.includes("traversal") || message.includes("EPERM")) {
5270
- return reply.code(403).send({
5271
- error: { code: ERROR_CODE_PATH_REJECTED, message: "path traversal rejected" }
5272
- });
5273
- }
5274
- if (code === "ENOENT" || message.includes("ENOENT")) {
5275
- return reply.code(404).send({
5276
- error: { code: ERROR_CODE_NOT_FOUND, message: `${subject} not found` }
5277
- });
5278
5600
  }
5279
- if (code === "EEXIST" || message.includes("EEXIST")) {
5280
- return reply.code(409).send({
5281
- error: { code: ERROR_CODE_ALREADY_EXISTS, message: `${subject} already exists` }
5282
- });
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;
5283
5611
  }
5284
- return reply.code(500).send({
5285
- error: { code: ERROR_CODE_INTERNAL, message }
5286
- });
5287
- }
5288
- function requireStringParam(value, field, reply) {
5289
- if (typeof value !== "string" || value.length === 0) {
5290
- reply.code(400).send({
5291
- error: { code: ERROR_CODE_VALIDATION_ERROR, message: `${field} is required`, field }
5292
- });
5293
- 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;
5294
5616
  }
5295
- if (value.includes("\0")) {
5296
- reply.code(400).send({
5297
- error: { code: ERROR_CODE_INVALID_PATH, message: "null bytes not allowed", field }
5298
- });
5299
- 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;
5300
5622
  }
5301
- 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
+ };
5302
5630
  }
5303
- function parseWorkspaceSettings(raw) {
5304
- try {
5305
- const parsed = JSON.parse(raw);
5306
- const dir = parsed?.markdown?.imageUploadDir;
5307
- return {
5308
- ...parsed,
5309
- markdown: {
5310
- ...parsed.markdown ?? {},
5311
- 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");
5312
5641
  }
5313
- };
5314
- } catch {
5315
- return defaultWorkspaceSettings();
5316
- }
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
+ };
5317
5703
  }
5318
- async function readWorkspaceSettings(workspace) {
5319
- try {
5320
- return parseWorkspaceSettings(await workspace.readFile(BORING_SETTINGS_PATH));
5321
- } catch (error) {
5322
- const code = error?.code;
5323
- if (code === "ENOENT") return defaultWorkspaceSettings();
5324
- 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
+ ];
5325
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
+ ];
5326
5725
  }
5327
- function normalizeUploadDir(value) {
5328
- if (typeof value !== "string") return null;
5329
- const dir = value.trim().replace(/^\.\/+/, "").replace(/\/+/g, "/");
5330
- if (!dir || dir.includes("\0") || dir.startsWith("/") || dir.split("/").includes("..")) return null;
5331
- return dir.replace(/\/+$/, "");
5332
- }
5333
- function extForUpload(filename, contentType) {
5334
- const fromName = extname3(filename).toLowerCase().replace(/^\./, "");
5335
- if (/^[a-z0-9]{1,12}$/.test(fromName)) return fromName;
5336
- if (contentType === "image/jpeg") return "jpg";
5337
- if (contentType === "image/png") return "png";
5338
- if (contentType === "image/gif") return "gif";
5339
- if (contentType === "image/webp") return "webp";
5340
- if (contentType === "image/svg+xml") return "svg";
5341
- 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, "'\\''")}'`;
5342
5734
  }
5343
- function basenameForUpload(filename) {
5344
- const base = filename.split("/").pop()?.split("\\").pop() ?? "image";
5345
- const withoutExt = base.replace(/\.[^.]*$/, "");
5346
- const safe = withoutExt.normalize("NFKD").replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
5347
- 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
+ });
5348
5747
  }
5349
- function markdownUrlFor(sourcePath, assetPath) {
5350
- if (!sourcePath) return assetPath;
5351
- const fromDir = dirname7(sourcePath.replace(/\\/g, "/"));
5352
- const rel = relative7(fromDir === "." ? "" : fromDir, assetPath);
5353
- return rel && !rel.startsWith(".") ? rel : rel || assetPath;
5748
+ function directSpawnHook(workspaceRoot) {
5749
+ return (context) => ({
5750
+ ...context,
5751
+ env: withWorkspacePythonEnv({ workspaceRoot, env: context.env })
5752
+ });
5354
5753
  }
5355
- function contentTypeForPath(path4) {
5356
- switch (extname3(path4).toLowerCase()) {
5357
- case ".avif":
5358
- return "image/avif";
5359
- case ".gif":
5360
- return "image/gif";
5361
- case ".jpg":
5362
- case ".jpeg":
5363
- return "image/jpeg";
5364
- case ".png":
5365
- return "image/png";
5366
- case ".svg":
5367
- return "image/svg+xml";
5368
- case ".webp":
5369
- return "image/webp";
5370
- case ".css":
5371
- return "text/css; charset=utf-8";
5372
- case ".html":
5373
- case ".htm":
5374
- return "text/html; charset=utf-8";
5375
- case ".js":
5376
- return "text/javascript; charset=utf-8";
5377
- case ".mjs":
5378
- return "text/javascript; charset=utf-8";
5379
- case ".pdf":
5380
- 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
+ };
5381
5763
  default:
5382
- return "application/octet-stream";
5764
+ return {
5765
+ operations: createLocalBashOperations(),
5766
+ spawnHook: directSpawnHook(bundle.workspace.root)
5767
+ };
5383
5768
  }
5384
5769
  }
5385
- function fileRoutes(app, opts, done) {
5386
- async function resolveWorkspace(request) {
5387
- if (opts.getWorkspace) return await opts.getWorkspace(request);
5388
- if (opts.workspace) return opts.workspace;
5389
- throw new Error("file route requires workspace or getWorkspace");
5390
- }
5391
- app.get("/api/v1/files/raw", async (request, reply) => {
5392
- const query = request.query;
5393
- const path4 = requireStringParam(query.path, "path", reply);
5394
- if (path4 === null) return;
5395
- try {
5396
- const workspace = await resolveWorkspace(request);
5397
- if (!workspace.readBinaryFile) {
5398
- return reply.code(501).send({
5399
- error: { code: ERROR_CODE_INTERNAL, message: "workspace does not support binary reads" }
5400
- });
5401
- }
5402
- const stat7 = await workspace.stat(path4);
5403
- if (stat7.kind !== "file") {
5404
- return reply.code(400).send({
5405
- error: { code: ERROR_CODE_VALIDATION_ERROR, message: "path is not a file", field: "path" }
5406
- });
5407
- }
5408
- const bytes = await workspace.readBinaryFile(path4);
5409
- return reply.header("content-type", contentTypeForPath(path4)).header("content-length", String(bytes.byteLength)).header("cache-control", "no-store").send(Buffer.from(bytes));
5410
- } catch (err) {
5411
- return classifyError(err, reply, "file");
5412
- }
5413
- });
5414
- app.get("/api/v1/files", async (request, reply) => {
5415
- const query = request.query;
5416
- const path4 = requireStringParam(query.path, "path", reply);
5417
- if (path4 === null) return;
5418
- try {
5419
- const workspace = await resolveWorkspace(request);
5420
- if (workspace.readFileWithStat) {
5421
- const { content: content2, stat: stat8 } = await workspace.readFileWithStat(path4);
5422
- return { content: content2, mtimeMs: stat8.kind === "file" ? stat8.mtimeMs : void 0 };
5423
- }
5424
- const content = await workspace.readFile(path4);
5425
- const stat7 = await workspace.stat(path4);
5426
- return { content, mtimeMs: stat7.kind === "file" ? stat7.mtimeMs : void 0 };
5427
- } catch (err) {
5428
- return classifyError(err, reply, "file");
5429
- }
5430
- });
5431
- app.post("/api/v1/files", async (request, reply) => {
5432
- const body = request.body;
5433
- const path4 = requireStringParam(body?.path, "path", reply);
5434
- if (path4 === null) return;
5435
- if (typeof body.content !== "string") {
5436
- return reply.code(400).send({
5437
- error: { code: ERROR_CODE_VALIDATION_ERROR, message: "content is required", field: "content" }
5438
- });
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
+ };
5439
5793
  }
5440
- const expectedMtimeMs = typeof body.expectedMtimeMs === "number" ? body.expectedMtimeMs : null;
5441
- try {
5442
- const workspace = await resolveWorkspace(request);
5443
- if (expectedMtimeMs !== null) {
5444
- try {
5445
- const current = await workspace.stat(path4);
5446
- if (current.kind === "file" && current.mtimeMs !== expectedMtimeMs) {
5447
- return reply.code(409).send({
5448
- error: {
5449
- code: ERROR_CODE_CONFLICT,
5450
- message: "file has been modified since last read",
5451
- currentMtimeMs: current.mtimeMs,
5452
- expectedMtimeMs
5453
- }
5454
- });
5455
- }
5456
- } catch (statErr) {
5457
- const code = statErr?.code;
5458
- if (code === "ENOENT") {
5459
- return reply.code(409).send({
5460
- error: {
5461
- code: ERROR_CODE_CONFLICT,
5462
- message: "file no longer exists",
5463
- expectedMtimeMs
5464
- }
5465
- });
5466
- }
5467
- throw statErr;
5468
- }
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
+ };
5469
5819
  }
5470
- if (body.createDirs) {
5471
- const dir = path4.includes("/") ? path4.slice(0, path4.lastIndexOf("/")) : void 0;
5472
- 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 };
5473
5824
  }
5474
- const content = body.content;
5475
- const stat7 = workspace.writeFileWithStat ? await workspace.writeFileWithStat(path4, content) : await (async () => {
5476
- await workspace.writeFile(path4, content);
5477
- return await workspace.stat(path4);
5478
- })();
5479
- return { ok: true, mtimeMs: stat7.kind === "file" ? stat7.mtimeMs : void 0 };
5480
- } catch (err) {
5481
- return classifyError(err, reply, "file");
5482
- }
5483
- });
5484
- app.post("/api/v1/files/upload", async (request, reply) => {
5485
- const body = request.body;
5486
- const filename = requireStringParam(body?.filename, "filename", reply);
5487
- if (filename === null) return;
5488
- const contentBase64 = requireStringParam(body?.contentBase64, "contentBase64", reply);
5489
- if (contentBase64 === null) return;
5490
- const contentType = typeof body.contentType === "string" ? body.contentType.trim().toLowerCase() : "";
5491
- if (!contentType.startsWith("image/")) {
5492
- return reply.code(400).send({
5493
- error: { code: ERROR_CODE_VALIDATION_ERROR, message: "contentType must be an image/* MIME type", field: "contentType" }
5494
- });
5495
- }
5496
- try {
5497
- const workspace = await resolveWorkspace(request);
5498
- const settings = await readWorkspaceSettings(workspace);
5499
- const dir = normalizeUploadDir(body.directory) ?? normalizeUploadDir(settings.markdown?.imageUploadDir) ?? DEFAULT_MARKDOWN_IMAGE_UPLOAD_DIR;
5500
- const ext = extForUpload(filename, contentType);
5501
- const base = basenameForUpload(filename);
5502
- const unique = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
5503
- const path4 = `${dir}/${base}-${unique}.${ext}`;
5504
- const bytes = Buffer.from(contentBase64, "base64");
5505
- if (bytes.byteLength === 0 || bytes.byteLength > MAX_UPLOAD_BYTES) {
5506
- return reply.code(400).send({
5507
- 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
5508
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
+ };
5509
5847
  }
5510
- await workspace.mkdir(dir, { recursive: true });
5511
- const stat7 = workspace.writeBinaryFileWithStat ? await workspace.writeBinaryFileWithStat(path4, bytes) : await (async () => {
5512
- if (!workspace.writeBinaryFile) {
5513
- throw new Error("workspace does not support binary uploads");
5514
- }
5515
- await workspace.writeBinaryFile(path4, bytes);
5516
- return await workspace.stat(path4);
5517
- })();
5518
- const sourcePath = typeof body.sourcePath === "string" && !body.sourcePath.includes("\0") ? body.sourcePath : null;
5519
- return {
5520
- ok: true,
5521
- path: path4,
5522
- markdownUrl: markdownUrlFor(sourcePath, path4),
5523
- mtimeMs: stat7.kind === "file" ? stat7.mtimeMs : void 0
5524
- };
5525
- } catch (err) {
5526
- return classifyError(err, reply, "upload");
5527
- }
5528
- });
5529
- app.get("/api/v1/workspace-settings", async (request, reply) => {
5530
- try {
5531
- const workspace = await resolveWorkspace(request);
5532
- return { settings: await readWorkspaceSettings(workspace) };
5533
- } catch (err) {
5534
- return classifyError(err, reply, "settings");
5535
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
+ };
5536
5870
  });
5537
- app.put("/api/v1/workspace-settings", async (request, reply) => {
5538
- const body = request.body;
5539
- const incoming = body?.settings ?? body;
5540
- const markdown = incoming.markdown ?? {};
5541
- const imageUploadDir = normalizeUploadDir(markdown.imageUploadDir);
5542
- if (!imageUploadDir) {
5543
- return reply.code(400).send({
5544
- 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
5545
5877
  });
5546
5878
  }
5547
- try {
5548
- const workspace = await resolveWorkspace(request);
5549
- const current = await readWorkspaceSettings(workspace);
5550
- const next = {
5551
- ...current,
5552
- markdown: {
5553
- ...current.markdown ?? {},
5554
- imageUploadDir
5555
- }
5556
- };
5557
- await workspace.mkdir(".boring", { recursive: true });
5558
- await workspace.writeFile(BORING_SETTINGS_PATH, `${JSON.stringify(next, null, 2)}
5559
- `);
5560
- return { settings: next };
5561
- } catch (err) {
5562
- return classifyError(err, reply, "settings");
5563
- }
5564
- });
5565
- app.delete("/api/v1/files", async (request, reply) => {
5566
- const query = request.query;
5567
- const path4 = requireStringParam(query.path, "path", reply);
5568
- if (path4 === null) return;
5569
- try {
5570
- const workspace = await resolveWorkspace(request);
5571
- await workspace.unlink(path4);
5572
- return { ok: true };
5573
- } catch (err) {
5574
- return classifyError(err, reply, "file");
5575
- }
5576
- });
5577
- app.post("/api/v1/files/move", async (request, reply) => {
5578
- const body = request.body;
5579
- const from = requireStringParam(body?.from, "from", reply);
5580
- if (from === null) return;
5581
- const to = requireStringParam(body?.to, "to", reply);
5582
- if (to === null) return;
5583
- try {
5584
- const workspace = await resolveWorkspace(request);
5585
- await workspace.rename(from, to);
5586
- return { ok: true };
5587
- } catch (err) {
5588
- return classifyError(err, reply, "file");
5589
- }
5590
- });
5591
- app.post("/api/v1/dirs", async (request, reply) => {
5592
- const body = request.body;
5593
- const path4 = requireStringParam(body?.path, "path", reply);
5594
- if (path4 === null) return;
5595
- const recursive = body.recursive === true;
5596
- try {
5597
- const workspace = await resolveWorkspace(request);
5598
- await workspace.mkdir(path4, { recursive });
5599
- return { ok: true };
5600
- } catch (err) {
5601
- return classifyError(err, reply, "directory");
5602
- }
5603
- });
5604
- app.get("/api/v1/stat", async (request, reply) => {
5605
- const query = request.query;
5606
- const path4 = requireStringParam(query.path, "path", reply);
5607
- if (path4 === null) return;
5608
- try {
5609
- const workspace = await resolveWorkspace(request);
5610
- const stat7 = await workspace.stat(path4);
5611
- return stat7;
5612
- } catch (err) {
5613
- return classifyError(err, reply, "path");
5879
+ if (!state.sandboxReady || !state.harnessReady) {
5880
+ return reply.code(503).send({
5881
+ status: "provisioning",
5882
+ retryAfter: 2
5883
+ });
5614
5884
  }
5885
+ return { status: "ready" };
5615
5886
  });
5616
5887
  done();
5617
5888
  }
@@ -6053,6 +6324,60 @@ function parseFileChangeChunk(chunk2) {
6053
6324
  return change;
6054
6325
  }
6055
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
+
6056
6381
  // src/server/http/routes/chat.ts
6057
6382
  var chatBodySchema = z.object({
6058
6383
  sessionId: z.string().min(1).max(128),
@@ -6078,6 +6403,35 @@ function chatRoutes(app, opts, done) {
6078
6403
  const { sessionChangesTracker } = opts;
6079
6404
  const validateBody = createBodyValidator(chatBodySchema);
6080
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
+ }
6081
6435
  async function resolveRuntime(request) {
6082
6436
  if (opts.getRuntime) return await opts.getRuntime(request);
6083
6437
  if (opts.harness && opts.workdir) {
@@ -6256,13 +6610,49 @@ function chatRoutes(app, opts, done) {
6256
6610
  error: { code: ERROR_CODE_VALIDATION_ERROR, message: "attachments is invalid" }
6257
6611
  });
6258
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
+ }
6259
6629
  const runtime = await resolveRuntime(request);
6260
- await runtime.harness.followUp?.(
6261
- sessionId,
6262
- body.message,
6263
- parsedAttachments.data,
6264
- typeof body.displayText === "string" && body.displayText.length > 0 ? body.displayText : body.message
6265
- );
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
+ }
6266
6656
  return reply.code(202).send({ queued: true });
6267
6657
  }
6268
6658
  );
@@ -6270,8 +6660,14 @@ function chatRoutes(app, opts, done) {
6270
6660
  "/api/v1/agent/chat/:sessionId/followup",
6271
6661
  async (request, reply) => {
6272
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;
6273
6667
  const runtime = await resolveRuntime(request);
6274
- runtime.harness.clearFollowUp?.(sessionId);
6668
+ markFollowUpCancelled(sessionId, clientNonce, clientSeq);
6669
+ evictFollowupCache();
6670
+ runtime.harness.clearFollowUp?.(sessionId, { clientNonce, clientSeq });
6275
6671
  return reply.code(204).send();
6276
6672
  }
6277
6673
  );
@@ -6289,7 +6685,7 @@ function chatRoutes(app, opts, done) {
6289
6685
  try {
6290
6686
  const runtime = await resolveRuntime(request);
6291
6687
  if (runtime.harness.sessions.saveMessages) {
6292
- await runtime.harness.sessions.saveMessages(ctx, sessionId, body.messages);
6688
+ await runtime.harness.sessions.saveMessages(ctx, sessionId, projectPiDataMessages(body.messages));
6293
6689
  }
6294
6690
  return reply.code(204).send();
6295
6691
  } catch {
@@ -6344,7 +6740,7 @@ function modelsRoutes(app, _opts, done) {
6344
6740
  return a.id.localeCompare(b.id);
6345
6741
  });
6346
6742
  const configuredDefaultModel = readConfiguredDefaultModel();
6347
- 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;
6348
6744
  const payload = defaultModel ? { models, defaultModel: { provider: defaultModel.provider, id: defaultModel.id } } : { models };
6349
6745
  return reply.code(200).send(payload);
6350
6746
  });
@@ -6352,26 +6748,55 @@ function modelsRoutes(app, _opts, done) {
6352
6748
  }
6353
6749
 
6354
6750
  // src/server/http/routes/skills.ts
6355
- 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";
6356
6756
  var CACHE_TTL_MS2 = 3e4;
6357
6757
  function skillsRoutes(app, opts, done) {
6358
- let cached = null;
6359
- app.get("/api/v1/agent/skills", (_request, reply) => {
6360
- const now = Date.now();
6361
- if (cached && cached.expiresAt > now) {
6362
- return reply.code(200).send({ skills: cached.skills });
6363
- }
6758
+ const cached = /* @__PURE__ */ new Map();
6759
+ app.get("/api/v1/agent/skills", async (request, reply) => {
6364
6760
  try {
6365
- const result = loadSkills2({
6366
- cwd: opts.workspaceRoot,
6367
- skillPaths: opts.additionalSkillPaths ?? [],
6368
- 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
6369
6794
  });
6370
6795
  const skills = result.skills.map((s) => ({
6371
6796
  name: s.name,
6372
6797
  description: s.description
6373
6798
  }));
6374
- cached = { skills, expiresAt: now + CACHE_TTL_MS2 };
6799
+ cached.set(cacheKey, { skills, expiresAt: now + CACHE_TTL_MS2 });
6375
6800
  return reply.code(200).send({ skills });
6376
6801
  } catch {
6377
6802
  return reply.code(200).send({ skills: [] });
@@ -7052,12 +7477,10 @@ async function createAgentApp(opts = {}) {
7052
7477
  const harness = createPiCodingAgentHarness({
7053
7478
  tools,
7054
7479
  cwd: workspaceRoot,
7480
+ sessionNamespace: opts.sessionNamespace,
7481
+ sessionDir: opts.sessionDir,
7055
7482
  systemPromptAppend: opts.systemPromptAppend,
7056
- resourceLoaderOptions: {
7057
- noContextFiles: true,
7058
- noSkills: true,
7059
- ...opts.resourceLoaderOptions
7060
- }
7483
+ resourceLoaderOptions: opts.resourceLoaderOptions
7061
7484
  });
7062
7485
  const sessionChangesTracker = new InMemorySessionChangesTracker();
7063
7486
  const readyTracker = new ReadyStatusTracker({
@@ -7143,6 +7566,11 @@ function mergeTools(options) {
7143
7566
  setLastRegistered(merged, tool);
7144
7567
  }
7145
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
+ }
7146
7574
  setLastRegistered(merged, tool);
7147
7575
  }
7148
7576
  for (const plugin of options.pluginTools ?? []) {
@@ -7298,7 +7726,7 @@ function getRequestWorkspaceId(request) {
7298
7726
  }
7299
7727
  function isWorkspaceAgnosticAgentRequest(request) {
7300
7728
  const pathname = request.url.split("?")[0] ?? request.url;
7301
- 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";
7302
7730
  }
7303
7731
  function extractHttpStatus2(error) {
7304
7732
  const statusCode = error?.statusCode;
@@ -7308,6 +7736,11 @@ function extractHttpStatus2(error) {
7308
7736
  const responseStatus = error?.response?.status;
7309
7737
  return typeof responseStatus === "number" ? responseStatus : null;
7310
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
+ }
7311
7744
  function isExpiredSandboxRuntimeError(error) {
7312
7745
  const code = error?.code;
7313
7746
  if (code === ErrorCode.enum.SANDBOX_EXPIRED) return true;
@@ -7325,23 +7758,49 @@ var registerAgentRoutes = async (app, opts) => {
7325
7758
  app.addHook("onClose", async () => {
7326
7759
  await modeAdapter.dispose?.();
7327
7760
  });
7328
- 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";
7329
7762
  const sessionChangesTracker = new InMemorySessionChangesTracker();
7330
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
+ }
7331
7772
  async function resolveRuntimeScope(workspaceId, request) {
7332
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);
7333
7777
  return {
7334
7778
  root,
7335
- 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
+ ])
7336
7790
  };
7337
7791
  }
7338
- async function createRuntimeBinding(workspaceId, root, request) {
7339
- 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;
7340
7799
  const runtimeBundle = await modeAdapter.create({
7341
7800
  workspaceRoot: root,
7342
7801
  sessionId: workspaceId,
7343
7802
  workspaceId,
7344
- templatePath: scopedTemplatePath
7803
+ templatePath: scope.templatePath
7345
7804
  });
7346
7805
  const standardTools = [
7347
7806
  ...buildHarnessAgentTools(runtimeBundle),
@@ -7381,12 +7840,9 @@ var registerAgentRoutes = async (app, opts) => {
7381
7840
  const harness = createPiCodingAgentHarness({
7382
7841
  tools,
7383
7842
  cwd: root,
7843
+ sessionNamespace: scope.sessionNamespace,
7384
7844
  systemPromptAppend: opts.systemPromptAppend,
7385
- resourceLoaderOptions: {
7386
- noContextFiles: true,
7387
- noSkills: true,
7388
- ...opts.resourceLoaderOptions
7389
- }
7845
+ resourceLoaderOptions: scope.resourceLoaderOptions
7390
7846
  });
7391
7847
  const readyTracker = new ReadyStatusTracker({
7392
7848
  sandboxReady: resolvedMode !== "vercel-sandbox",
@@ -7412,8 +7868,9 @@ var registerAgentRoutes = async (app, opts) => {
7412
7868
  await existing
7413
7869
  );
7414
7870
  }
7415
- const created = createRuntimeBinding(workspaceId, scope.root, request);
7871
+ const created = createRuntimeBinding(workspaceId, scope, request);
7416
7872
  runtimeBindings.set(scope.key, created);
7873
+ evictRuntimeBindings();
7417
7874
  try {
7418
7875
  return await ensureRuntimeBindingReady(
7419
7876
  workspaceId,
@@ -7428,8 +7885,9 @@ var registerAgentRoutes = async (app, opts) => {
7428
7885
  async function recreateRuntimeBinding(workspaceId, scope) {
7429
7886
  runtimeBindings.delete(scope.key);
7430
7887
  evictSandboxHandleCacheForWorkspace(workspaceId);
7431
- const created = createRuntimeBinding(workspaceId, scope.root);
7888
+ const created = createRuntimeBinding(workspaceId, scope);
7432
7889
  runtimeBindings.set(scope.key, created);
7890
+ evictRuntimeBindings();
7433
7891
  try {
7434
7892
  const binding = await created;
7435
7893
  binding.lastHealthCheckMs = Date.now();
@@ -7459,6 +7917,15 @@ var registerAgentRoutes = async (app, opts) => {
7459
7917
  }
7460
7918
  }
7461
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
+ }
7462
7929
  async function getBindingForRequest(request) {
7463
7930
  if (staticBinding) return staticBinding;
7464
7931
  return await getOrCreateRuntimeBinding(getRequestWorkspaceId(request), request);
@@ -7558,7 +8025,13 @@ var registerAgentRoutes = async (app, opts) => {
7558
8025
  await app.register(modelsRoutes);
7559
8026
  await app.register(skillsRoutes, {
7560
8027
  workspaceRoot,
7561
- 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
7562
8035
  });
7563
8036
  await app.register(sessionChangesRoutes, { tracker: sessionChangesTracker });
7564
8037
  await app.register(
@@ -7591,6 +8064,7 @@ export {
7591
8064
  createNodeWorkspace,
7592
8065
  createVercelDeploymentSnapshotProvider,
7593
8066
  createVercelSandboxWorkspace,
8067
+ fileRoutes,
7594
8068
  hasBwrap,
7595
8069
  mergePiPackageSources,
7596
8070
  piPackageSourceKey,