agent-board-runner 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -58,3 +58,17 @@ Only needed to override the defaults — see `.env.example`.
58
58
  - `node-pty` is a native module — if `npm install` fails building it, install your platform's
59
59
  usual native build toolchain (e.g. `build-essential`/Xcode Command Line Tools/Visual Studio
60
60
  Build Tools) and retry.
61
+
62
+ ## Known limitation: OAuth login inside a review preview
63
+
64
+ Moving a card into "revisao" tunnels that project's own dev server through its own Quick
65
+ Tunnel, with a fresh random `*.trycloudflare.com` subdomain every time. If the project you're
66
+ reviewing signs in through a hosted auth provider (Supabase Auth, Auth0, etc.), the post-login
67
+ redirect will likely bounce back to `localhost` instead of the tunnel URL — the new subdomain
68
+ was never allow-listed with that provider.
69
+
70
+ Fix it once per project, in that project's own auth provider (not in Agent Board): add a
71
+ wildcard entry to its redirect URL allow list, e.g. on Supabase, Authentication > URL
72
+ Configuration > Additional Redirect URLs > `https://*.trycloudflare.com/**`. Providers without
73
+ wildcard support in that allow list (e.g. Firebase Auth) don't have an equivalent fix without
74
+ extra infrastructure.
package/dist/cli.js CHANGED
@@ -981,6 +981,10 @@ async function updateRunnerSetupTicket(supabase, runnerId, ticket, expiresAt, al
981
981
  const { error } = await supabase.from("runners").update({ setup_ticket: ticket, setup_ticket_expires_at: expiresAt, allowed_root_path: allowedRootPath }).eq("id", runnerId);
982
982
  if (error) throw error;
983
983
  }
984
+ async function updateRunnerAgentTicket(supabase, runnerId, ticket, expiresAt) {
985
+ const { error } = await supabase.from("runners").update({ agent_ticket: ticket, agent_ticket_expires_at: expiresAt }).eq("id", runnerId);
986
+ if (error) throw error;
987
+ }
984
988
  async function listMyProjects(supabase) {
985
989
  const { data, error } = await supabase.from("projects").select("id, runner_id, repo_path, logo_path");
986
990
  if (error) throw error;
@@ -1340,6 +1344,7 @@ async function handleCardReachedFim(supabase, config, card) {
1340
1344
  import { execFile as execFile2 } from "node:child_process";
1341
1345
  import { randomUUID as randomUUID6 } from "node:crypto";
1342
1346
  import { existsSync as existsSync4, lstatSync, readdirSync, readFileSync as readFileSync4 } from "node:fs";
1347
+ import { homedir as homedir4 } from "node:os";
1343
1348
  import { join as join8, resolve as resolve2, sep } from "node:path";
1344
1349
  import { parse } from "node:url";
1345
1350
  import { promisify as promisify2 } from "node:util";
@@ -1460,6 +1465,7 @@ var FILES_TICKET_REFRESH_MARGIN_MS = FILES_TICKET_TTL_MS / 3;
1460
1465
  var MAX_FILE_SIZE_BYTES = 1e6;
1461
1466
  var MAX_LOGO_SIZE_BYTES = 5e6;
1462
1467
  var IGNORED_ENTRY_NAMES = /* @__PURE__ */ new Set([".git", ".claude", "node_modules", "dist", "build"]);
1468
+ var AGENT_SAFE_SUBDIR_NAMES = /* @__PURE__ */ new Set(["skills", "commands", "agents", "rules"]);
1463
1469
  var BRAND_ICON_BASE_NAMES = ["logo", "favicon"];
1464
1470
  var MIME_TYPES_BY_EXT = {
1465
1471
  svg: "image/svg+xml",
@@ -1472,6 +1478,7 @@ var MIME_TYPES_BY_EXT = {
1472
1478
  };
1473
1479
  var ticketsByProject = /* @__PURE__ */ new Map();
1474
1480
  var setupTicket = null;
1481
+ var agentTicket = null;
1475
1482
  var statuslineSupabase = null;
1476
1483
  var statuslineConfig = null;
1477
1484
  var MAX_STATUSLINE_BODY_BYTES = 16e3;
@@ -1522,6 +1529,23 @@ function authenticateSetup(query) {
1522
1529
  if (setupTicket.ticket !== ticket || setupTicket.expiresAt < Date.now()) return null;
1523
1530
  return setupTicket;
1524
1531
  }
1532
+ async function ensureAgentTicket(supabase, config) {
1533
+ if (agentTicket && agentTicket.expiresAt - Date.now() > FILES_TICKET_REFRESH_MARGIN_MS) return;
1534
+ const ticket = randomUUID6();
1535
+ const expiresAtMs = Date.now() + FILES_TICKET_TTL_MS;
1536
+ agentTicket = { ticket, expiresAt: expiresAtMs, repoRoot: resolve2(homedir4(), ".claude") };
1537
+ try {
1538
+ await updateRunnerAgentTicket(supabase, config.runnerId, ticket, new Date(expiresAtMs).toISOString());
1539
+ } catch (err) {
1540
+ logger.runner.error("FILES", `Failed to publish agent ticket for runner ${config.runnerId}:`, err);
1541
+ }
1542
+ }
1543
+ function authenticateAgent(query) {
1544
+ const ticket = typeof query.ticket === "string" ? query.ticket : null;
1545
+ if (!ticket || !agentTicket) return null;
1546
+ if (agentTicket.ticket !== ticket || agentTicket.expiresAt < Date.now()) return null;
1547
+ return agentTicket;
1548
+ }
1525
1549
  function resolveWithinRepo(repoRoot, relPath) {
1526
1550
  const normalizedRel = relPath.replace(/^[/\\]+/, "");
1527
1551
  const resolved = resolve2(repoRoot, normalizedRel);
@@ -1531,7 +1555,7 @@ function resolveWithinRepo(repoRoot, relPath) {
1531
1555
  }
1532
1556
  return resolved;
1533
1557
  }
1534
- function listDirectory(repoRoot, relPath) {
1558
+ function listDirectory(repoRoot, relPath, options) {
1535
1559
  const target = resolveWithinRepo(repoRoot, relPath);
1536
1560
  let stat;
1537
1561
  try {
@@ -1542,12 +1566,45 @@ function listDirectory(repoRoot, relPath) {
1542
1566
  if (stat.isSymbolicLink()) throw new HttpError(403, "Links simb\xF3licos n\xE3o s\xE3o suportados.");
1543
1567
  if (!stat.isDirectory()) throw new HttpError(400, "O caminho informado n\xE3o \xE9 um diret\xF3rio.");
1544
1568
  const prefix = relPath.replace(/\/+$/, "");
1545
- return readdirSync(target, { withFileTypes: true }).filter((dirent) => !dirent.isSymbolicLink() && !IGNORED_ENTRY_NAMES.has(dirent.name)).map((dirent) => ({
1569
+ return readdirSync(target, { withFileTypes: true }).filter(
1570
+ (dirent) => !dirent.isSymbolicLink() && (!IGNORED_ENTRY_NAMES.has(dirent.name) || options?.includeDotClaude && dirent.name === ".claude")
1571
+ ).map((dirent) => ({
1546
1572
  name: dirent.name,
1547
1573
  path: prefix ? `${prefix}/${dirent.name}` : dirent.name,
1548
1574
  type: dirent.isDirectory() ? "dir" : "file"
1549
1575
  })).sort((a, b) => a.type !== b.type ? a.type === "dir" ? -1 : 1 : a.name.localeCompare(b.name));
1550
1576
  }
1577
+ function listSafeAgentDirectory(baseDir, relPath, atClaudeRoot) {
1578
+ const entries = listDirectory(baseDir, relPath, { includeDotClaude: true });
1579
+ const segments = relPath === "" ? [] : relPath.split("/");
1580
+ if (atClaudeRoot) {
1581
+ if (segments.length === 0) {
1582
+ return entries.filter(
1583
+ (entry) => entry.type === "file" && entry.name === "CLAUDE.md" || entry.type === "dir" && AGENT_SAFE_SUBDIR_NAMES.has(entry.name)
1584
+ );
1585
+ }
1586
+ return entries;
1587
+ }
1588
+ const isRepoRootLevel = segments.length === 0 || segments.length === 1 && segments[0] !== ".claude";
1589
+ if (isRepoRootLevel) {
1590
+ return entries.filter((entry) => entry.type === "file" && entry.name === "CLAUDE.md" || entry.type === "dir" && entry.name === ".claude");
1591
+ }
1592
+ if (segments[segments.length - 1] === ".claude") {
1593
+ return entries.filter((entry) => entry.type === "dir" && AGENT_SAFE_SUBDIR_NAMES.has(entry.name));
1594
+ }
1595
+ return entries;
1596
+ }
1597
+ function assertSafeAgentPath(relPath, atClaudeRoot) {
1598
+ const segments = relPath.split("/");
1599
+ function matches(rest2) {
1600
+ if (rest2.length === 1 && rest2[0] === "CLAUDE.md") return true;
1601
+ if (atClaudeRoot) return rest2.length >= 2 && AGENT_SAFE_SUBDIR_NAMES.has(rest2[0]);
1602
+ return rest2.length >= 3 && rest2[0] === ".claude" && AGENT_SAFE_SUBDIR_NAMES.has(rest2[1]);
1603
+ }
1604
+ if (matches(segments)) return;
1605
+ if (!atClaudeRoot && segments.length > 1 && matches(segments.slice(1))) return;
1606
+ throw new HttpError(403, "Caminho fora da \xE1rea permitida para o Agente.");
1607
+ }
1551
1608
  function listDirectoryForBrowse(rootPath, relPath) {
1552
1609
  return listDirectory(rootPath, relPath).map((entry) => ({
1553
1610
  ...entry,
@@ -1688,9 +1745,19 @@ async function handleStatuslineReport(req, res, supabase, config) {
1688
1745
  sendJson(res, 500, { error: "Erro interno do runner." });
1689
1746
  }
1690
1747
  }
1691
- async function handleBranchRequest(entry, res) {
1748
+ async function handleBranchRequest(entry, res, relPath) {
1749
+ let cwd;
1692
1750
  try {
1693
- const { stdout } = await execFileAsync2("git", ["branch", "--show-current"], { cwd: entry.repoRoot });
1751
+ cwd = relPath ? resolveWithinRepo(entry.repoRoot, relPath) : entry.repoRoot;
1752
+ } catch (err) {
1753
+ if (err instanceof HttpError) {
1754
+ sendJson(res, err.status, { error: err.message });
1755
+ return;
1756
+ }
1757
+ throw err;
1758
+ }
1759
+ try {
1760
+ const { stdout } = await execFileAsync2("git", ["branch", "--show-current"], { cwd });
1694
1761
  sendJson(res, 200, { branch: stdout.trim() });
1695
1762
  } catch (err) {
1696
1763
  logger.runner.error("FILES", "Unexpected error handling a branch request:", err);
@@ -1735,13 +1802,36 @@ function handleRequest(req, res) {
1735
1802
  }
1736
1803
  return;
1737
1804
  }
1805
+ if (pathname === "/global-agent-tree" || pathname === "/global-agent-file") {
1806
+ const agentEntry = authenticateAgent(query);
1807
+ if (!agentEntry) {
1808
+ sendJson(res, 401, { error: "Ticket inv\xE1lido ou expirado." });
1809
+ return;
1810
+ }
1811
+ try {
1812
+ if (pathname === "/global-agent-tree") {
1813
+ sendJson(res, 200, { entries: listSafeAgentDirectory(agentEntry.repoRoot, relPath, true) });
1814
+ } else {
1815
+ assertSafeAgentPath(relPath, true);
1816
+ sendJson(res, 200, readTextFile(agentEntry.repoRoot, relPath));
1817
+ }
1818
+ } catch (err) {
1819
+ if (err instanceof HttpError) {
1820
+ sendJson(res, err.status, { error: err.message });
1821
+ return;
1822
+ }
1823
+ logger.runner.error("FILES", "Unexpected error handling a global agent request:", err);
1824
+ sendJson(res, 500, { error: "Erro interno do runner." });
1825
+ }
1826
+ return;
1827
+ }
1738
1828
  const entry = authenticate(query);
1739
1829
  if (!entry) {
1740
1830
  sendJson(res, 401, { error: "Ticket inv\xE1lido, expirado, ou projeto desconhecido." });
1741
1831
  return;
1742
1832
  }
1743
1833
  if (pathname === "/branch") {
1744
- void handleBranchRequest(entry, res);
1834
+ void handleBranchRequest(entry, res, relPath);
1745
1835
  return;
1746
1836
  }
1747
1837
  try {
@@ -1753,6 +1843,15 @@ function handleRequest(req, res) {
1753
1843
  sendJson(res, 200, readTextFile(entry.repoRoot, relPath));
1754
1844
  return;
1755
1845
  }
1846
+ if (pathname === "/agent-tree") {
1847
+ sendJson(res, 200, { entries: listSafeAgentDirectory(entry.repoRoot, relPath, false) });
1848
+ return;
1849
+ }
1850
+ if (pathname === "/agent-file") {
1851
+ assertSafeAgentPath(relPath, false);
1852
+ sendJson(res, 200, readTextFile(entry.repoRoot, relPath));
1853
+ return;
1854
+ }
1756
1855
  if (pathname === "/logo") {
1757
1856
  if (!relPath) throw new HttpError(404, "Nenhuma logo configurada para este projeto.");
1758
1857
  const { buffer, contentType } = readImageFile(entry.repoRoot, relPath);
@@ -2112,6 +2211,7 @@ async function startRunner() {
2112
2211
  void ensureTerminalTunnel(supabase, config, terminalPort).catch((err) => logger.runner.error("CLOUDFLARE", "ensureTerminalTunnel failed:", err));
2113
2212
  void ensureFilesTickets(supabase, config).catch((err) => logger.runner.error("FILES", "ensureFilesTickets failed:", err));
2114
2213
  void ensureSetupTicket(supabase, config).catch((err) => logger.runner.error("FILES", "ensureSetupTicket failed:", err));
2214
+ void ensureAgentTicket(supabase, config).catch((err) => logger.runner.error("FILES", "ensureAgentTicket failed:", err));
2115
2215
  let shuttingDown = false;
2116
2216
  for (const signal of ["SIGINT", "SIGTERM"]) {
2117
2217
  process.on(signal, () => {
@@ -2177,6 +2277,7 @@ async function startRunner() {
2177
2277
  await ensureTerminalTunnel(supabase, config, terminalPort);
2178
2278
  await ensureFilesTickets(supabase, config);
2179
2279
  await ensureSetupTicket(supabase, config);
2280
+ await ensureAgentTicket(supabase, config);
2180
2281
  await scanForWork(supabase, config);
2181
2282
  await reconcileUsageLimitedSessions(supabase, config).catch((err) => logger.runner.error("ORCHESTRATOR", "reconcileUsageLimitedSessions failed:", err));
2182
2283
  await checkPendingDeploys(supabase, config).catch((err) => logger.runner.error("DEPLOY", "checkPendingDeploys failed:", err));
package/dist/index.js CHANGED
@@ -920,6 +920,10 @@ async function updateRunnerSetupTicket(supabase, runnerId, ticket, expiresAt, al
920
920
  const { error } = await supabase.from("runners").update({ setup_ticket: ticket, setup_ticket_expires_at: expiresAt, allowed_root_path: allowedRootPath }).eq("id", runnerId);
921
921
  if (error) throw error;
922
922
  }
923
+ async function updateRunnerAgentTicket(supabase, runnerId, ticket, expiresAt) {
924
+ const { error } = await supabase.from("runners").update({ agent_ticket: ticket, agent_ticket_expires_at: expiresAt }).eq("id", runnerId);
925
+ if (error) throw error;
926
+ }
923
927
  async function listMyProjects(supabase) {
924
928
  const { data, error } = await supabase.from("projects").select("id, runner_id, repo_path, logo_path");
925
929
  if (error) throw error;
@@ -1279,6 +1283,7 @@ async function handleCardReachedFim(supabase, config, card) {
1279
1283
  import { execFile as execFile2 } from "node:child_process";
1280
1284
  import { randomUUID as randomUUID5 } from "node:crypto";
1281
1285
  import { existsSync as existsSync4, lstatSync, readdirSync, readFileSync as readFileSync4 } from "node:fs";
1286
+ import { homedir as homedir4 } from "node:os";
1282
1287
  import { join as join8, resolve as resolve2, sep } from "node:path";
1283
1288
  import { parse } from "node:url";
1284
1289
  import { promisify as promisify2 } from "node:util";
@@ -1399,6 +1404,7 @@ var FILES_TICKET_REFRESH_MARGIN_MS = FILES_TICKET_TTL_MS / 3;
1399
1404
  var MAX_FILE_SIZE_BYTES = 1e6;
1400
1405
  var MAX_LOGO_SIZE_BYTES = 5e6;
1401
1406
  var IGNORED_ENTRY_NAMES = /* @__PURE__ */ new Set([".git", ".claude", "node_modules", "dist", "build"]);
1407
+ var AGENT_SAFE_SUBDIR_NAMES = /* @__PURE__ */ new Set(["skills", "commands", "agents", "rules"]);
1402
1408
  var BRAND_ICON_BASE_NAMES = ["logo", "favicon"];
1403
1409
  var MIME_TYPES_BY_EXT = {
1404
1410
  svg: "image/svg+xml",
@@ -1411,6 +1417,7 @@ var MIME_TYPES_BY_EXT = {
1411
1417
  };
1412
1418
  var ticketsByProject = /* @__PURE__ */ new Map();
1413
1419
  var setupTicket = null;
1420
+ var agentTicket = null;
1414
1421
  var statuslineSupabase = null;
1415
1422
  var statuslineConfig = null;
1416
1423
  var MAX_STATUSLINE_BODY_BYTES = 16e3;
@@ -1461,6 +1468,23 @@ function authenticateSetup(query) {
1461
1468
  if (setupTicket.ticket !== ticket || setupTicket.expiresAt < Date.now()) return null;
1462
1469
  return setupTicket;
1463
1470
  }
1471
+ async function ensureAgentTicket(supabase, config) {
1472
+ if (agentTicket && agentTicket.expiresAt - Date.now() > FILES_TICKET_REFRESH_MARGIN_MS) return;
1473
+ const ticket = randomUUID5();
1474
+ const expiresAtMs = Date.now() + FILES_TICKET_TTL_MS;
1475
+ agentTicket = { ticket, expiresAt: expiresAtMs, repoRoot: resolve2(homedir4(), ".claude") };
1476
+ try {
1477
+ await updateRunnerAgentTicket(supabase, config.runnerId, ticket, new Date(expiresAtMs).toISOString());
1478
+ } catch (err) {
1479
+ logger.runner.error("FILES", `Failed to publish agent ticket for runner ${config.runnerId}:`, err);
1480
+ }
1481
+ }
1482
+ function authenticateAgent(query) {
1483
+ const ticket = typeof query.ticket === "string" ? query.ticket : null;
1484
+ if (!ticket || !agentTicket) return null;
1485
+ if (agentTicket.ticket !== ticket || agentTicket.expiresAt < Date.now()) return null;
1486
+ return agentTicket;
1487
+ }
1464
1488
  function resolveWithinRepo(repoRoot, relPath) {
1465
1489
  const normalizedRel = relPath.replace(/^[/\\]+/, "");
1466
1490
  const resolved = resolve2(repoRoot, normalizedRel);
@@ -1470,7 +1494,7 @@ function resolveWithinRepo(repoRoot, relPath) {
1470
1494
  }
1471
1495
  return resolved;
1472
1496
  }
1473
- function listDirectory(repoRoot, relPath) {
1497
+ function listDirectory(repoRoot, relPath, options) {
1474
1498
  const target = resolveWithinRepo(repoRoot, relPath);
1475
1499
  let stat;
1476
1500
  try {
@@ -1481,12 +1505,45 @@ function listDirectory(repoRoot, relPath) {
1481
1505
  if (stat.isSymbolicLink()) throw new HttpError(403, "Links simb\xF3licos n\xE3o s\xE3o suportados.");
1482
1506
  if (!stat.isDirectory()) throw new HttpError(400, "O caminho informado n\xE3o \xE9 um diret\xF3rio.");
1483
1507
  const prefix = relPath.replace(/\/+$/, "");
1484
- return readdirSync(target, { withFileTypes: true }).filter((dirent) => !dirent.isSymbolicLink() && !IGNORED_ENTRY_NAMES.has(dirent.name)).map((dirent) => ({
1508
+ return readdirSync(target, { withFileTypes: true }).filter(
1509
+ (dirent) => !dirent.isSymbolicLink() && (!IGNORED_ENTRY_NAMES.has(dirent.name) || options?.includeDotClaude && dirent.name === ".claude")
1510
+ ).map((dirent) => ({
1485
1511
  name: dirent.name,
1486
1512
  path: prefix ? `${prefix}/${dirent.name}` : dirent.name,
1487
1513
  type: dirent.isDirectory() ? "dir" : "file"
1488
1514
  })).sort((a, b) => a.type !== b.type ? a.type === "dir" ? -1 : 1 : a.name.localeCompare(b.name));
1489
1515
  }
1516
+ function listSafeAgentDirectory(baseDir, relPath, atClaudeRoot) {
1517
+ const entries = listDirectory(baseDir, relPath, { includeDotClaude: true });
1518
+ const segments = relPath === "" ? [] : relPath.split("/");
1519
+ if (atClaudeRoot) {
1520
+ if (segments.length === 0) {
1521
+ return entries.filter(
1522
+ (entry) => entry.type === "file" && entry.name === "CLAUDE.md" || entry.type === "dir" && AGENT_SAFE_SUBDIR_NAMES.has(entry.name)
1523
+ );
1524
+ }
1525
+ return entries;
1526
+ }
1527
+ const isRepoRootLevel = segments.length === 0 || segments.length === 1 && segments[0] !== ".claude";
1528
+ if (isRepoRootLevel) {
1529
+ return entries.filter((entry) => entry.type === "file" && entry.name === "CLAUDE.md" || entry.type === "dir" && entry.name === ".claude");
1530
+ }
1531
+ if (segments[segments.length - 1] === ".claude") {
1532
+ return entries.filter((entry) => entry.type === "dir" && AGENT_SAFE_SUBDIR_NAMES.has(entry.name));
1533
+ }
1534
+ return entries;
1535
+ }
1536
+ function assertSafeAgentPath(relPath, atClaudeRoot) {
1537
+ const segments = relPath.split("/");
1538
+ function matches(rest) {
1539
+ if (rest.length === 1 && rest[0] === "CLAUDE.md") return true;
1540
+ if (atClaudeRoot) return rest.length >= 2 && AGENT_SAFE_SUBDIR_NAMES.has(rest[0]);
1541
+ return rest.length >= 3 && rest[0] === ".claude" && AGENT_SAFE_SUBDIR_NAMES.has(rest[1]);
1542
+ }
1543
+ if (matches(segments)) return;
1544
+ if (!atClaudeRoot && segments.length > 1 && matches(segments.slice(1))) return;
1545
+ throw new HttpError(403, "Caminho fora da \xE1rea permitida para o Agente.");
1546
+ }
1490
1547
  function listDirectoryForBrowse(rootPath, relPath) {
1491
1548
  return listDirectory(rootPath, relPath).map((entry) => ({
1492
1549
  ...entry,
@@ -1627,9 +1684,19 @@ async function handleStatuslineReport(req, res, supabase, config) {
1627
1684
  sendJson(res, 500, { error: "Erro interno do runner." });
1628
1685
  }
1629
1686
  }
1630
- async function handleBranchRequest(entry, res) {
1687
+ async function handleBranchRequest(entry, res, relPath) {
1688
+ let cwd;
1631
1689
  try {
1632
- const { stdout } = await execFileAsync2("git", ["branch", "--show-current"], { cwd: entry.repoRoot });
1690
+ cwd = relPath ? resolveWithinRepo(entry.repoRoot, relPath) : entry.repoRoot;
1691
+ } catch (err) {
1692
+ if (err instanceof HttpError) {
1693
+ sendJson(res, err.status, { error: err.message });
1694
+ return;
1695
+ }
1696
+ throw err;
1697
+ }
1698
+ try {
1699
+ const { stdout } = await execFileAsync2("git", ["branch", "--show-current"], { cwd });
1633
1700
  sendJson(res, 200, { branch: stdout.trim() });
1634
1701
  } catch (err) {
1635
1702
  logger.runner.error("FILES", "Unexpected error handling a branch request:", err);
@@ -1674,13 +1741,36 @@ function handleRequest(req, res) {
1674
1741
  }
1675
1742
  return;
1676
1743
  }
1744
+ if (pathname === "/global-agent-tree" || pathname === "/global-agent-file") {
1745
+ const agentEntry = authenticateAgent(query);
1746
+ if (!agentEntry) {
1747
+ sendJson(res, 401, { error: "Ticket inv\xE1lido ou expirado." });
1748
+ return;
1749
+ }
1750
+ try {
1751
+ if (pathname === "/global-agent-tree") {
1752
+ sendJson(res, 200, { entries: listSafeAgentDirectory(agentEntry.repoRoot, relPath, true) });
1753
+ } else {
1754
+ assertSafeAgentPath(relPath, true);
1755
+ sendJson(res, 200, readTextFile(agentEntry.repoRoot, relPath));
1756
+ }
1757
+ } catch (err) {
1758
+ if (err instanceof HttpError) {
1759
+ sendJson(res, err.status, { error: err.message });
1760
+ return;
1761
+ }
1762
+ logger.runner.error("FILES", "Unexpected error handling a global agent request:", err);
1763
+ sendJson(res, 500, { error: "Erro interno do runner." });
1764
+ }
1765
+ return;
1766
+ }
1677
1767
  const entry = authenticate(query);
1678
1768
  if (!entry) {
1679
1769
  sendJson(res, 401, { error: "Ticket inv\xE1lido, expirado, ou projeto desconhecido." });
1680
1770
  return;
1681
1771
  }
1682
1772
  if (pathname === "/branch") {
1683
- void handleBranchRequest(entry, res);
1773
+ void handleBranchRequest(entry, res, relPath);
1684
1774
  return;
1685
1775
  }
1686
1776
  try {
@@ -1692,6 +1782,15 @@ function handleRequest(req, res) {
1692
1782
  sendJson(res, 200, readTextFile(entry.repoRoot, relPath));
1693
1783
  return;
1694
1784
  }
1785
+ if (pathname === "/agent-tree") {
1786
+ sendJson(res, 200, { entries: listSafeAgentDirectory(entry.repoRoot, relPath, false) });
1787
+ return;
1788
+ }
1789
+ if (pathname === "/agent-file") {
1790
+ assertSafeAgentPath(relPath, false);
1791
+ sendJson(res, 200, readTextFile(entry.repoRoot, relPath));
1792
+ return;
1793
+ }
1695
1794
  if (pathname === "/logo") {
1696
1795
  if (!relPath) throw new HttpError(404, "Nenhuma logo configurada para este projeto.");
1697
1796
  const { buffer, contentType } = readImageFile(entry.repoRoot, relPath);
@@ -2051,6 +2150,7 @@ async function startRunner() {
2051
2150
  void ensureTerminalTunnel(supabase, config, terminalPort).catch((err) => logger.runner.error("CLOUDFLARE", "ensureTerminalTunnel failed:", err));
2052
2151
  void ensureFilesTickets(supabase, config).catch((err) => logger.runner.error("FILES", "ensureFilesTickets failed:", err));
2053
2152
  void ensureSetupTicket(supabase, config).catch((err) => logger.runner.error("FILES", "ensureSetupTicket failed:", err));
2153
+ void ensureAgentTicket(supabase, config).catch((err) => logger.runner.error("FILES", "ensureAgentTicket failed:", err));
2054
2154
  let shuttingDown = false;
2055
2155
  for (const signal of ["SIGINT", "SIGTERM"]) {
2056
2156
  process.on(signal, () => {
@@ -2116,6 +2216,7 @@ async function startRunner() {
2116
2216
  await ensureTerminalTunnel(supabase, config, terminalPort);
2117
2217
  await ensureFilesTickets(supabase, config);
2118
2218
  await ensureSetupTicket(supabase, config);
2219
+ await ensureAgentTicket(supabase, config);
2119
2220
  await scanForWork(supabase, config);
2120
2221
  await reconcileUsageLimitedSessions(supabase, config).catch((err) => logger.runner.error("ORCHESTRATOR", "reconcileUsageLimitedSessions failed:", err));
2121
2222
  await checkPendingDeploys(supabase, config).catch((err) => logger.runner.error("DEPLOY", "checkPendingDeploys failed:", err));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-board-runner",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Local runner for Agent Board — signs in with your own Supabase account (no shared secrets) and drives Claude Code against repos under your local repos root.",
5
5
  "type": "module",
6
6
  "bin": {