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 +14 -0
- package/dist/cli.js +106 -5
- package/dist/index.js +106 -5
- package/package.json +1 -1
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(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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.
|
|
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": {
|