agent-board-runner 0.1.14 → 0.1.16
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/dist/cli.js +191 -39
- package/dist/index.js +191 -39
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2527,7 +2527,7 @@ async function ensureBranchWatchers(supabase, config) {
|
|
|
2527
2527
|
// src/columnActions.ts
|
|
2528
2528
|
import { exec } from "node:child_process";
|
|
2529
2529
|
import { join as join12 } from "node:path";
|
|
2530
|
-
import { promisify as
|
|
2530
|
+
import { promisify as promisify5 } from "node:util";
|
|
2531
2531
|
|
|
2532
2532
|
// src/orchestrator.ts
|
|
2533
2533
|
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
@@ -2733,10 +2733,177 @@ ${list}`;
|
|
|
2733
2733
|
}
|
|
2734
2734
|
|
|
2735
2735
|
// src/mcpContextNote.ts
|
|
2736
|
-
function buildInstructionsWithMcpContext(instructions, cardId, projectId) {
|
|
2737
|
-
|
|
2736
|
+
function buildInstructionsWithMcpContext(instructions, cardId, projectId, approvedCommands) {
|
|
2737
|
+
const base = `${instructions}
|
|
2738
2738
|
|
|
2739
2739
|
Agent Board MCP: this card's id is \`${cardId}\` and its project id is \`${projectId}\` \u2014 pass these directly to board tools (\`update_card\`, \`create_card\`, \`add_card_comment\`, etc.), no lookup needed. If this task turns out too large for one session, use \`create_card\` to split it into subtask cards (chain them with \`prerequisiteCardId\` when one depends on another) instead of trying to fit everything here. If part of the delivery needs a human to do something you can't do yourself (rotate a secret, approve something in a third-party dashboard, delete a resource manually, etc.), register each step with \`add_manual_actions\` instead of just mentioning it in your final message \u2014 that's the only place humans will reliably see it. Before finishing, post a short summary of what you did as a comment on this card via \`add_card_comment\` so the project's humans have visibility without opening this terminal.`;
|
|
2740
|
+
if (approvedCommands.length === 0) return base;
|
|
2741
|
+
const names = approvedCommands.map((command2) => command2.description ? `${command2.name} (${command2.description})` : command2.name).join(", ");
|
|
2742
|
+
return `${base}
|
|
2743
|
+
|
|
2744
|
+
This project already has approved custom command templates you can invoke instead of running the equivalent shell command yourself or proposing a near-duplicate: ${names}. Call \`list_project_commands\` first to see each one's declared arguments, then \`run_project_command\` to invoke it.`;
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
// src/projectCommands.ts
|
|
2748
|
+
import { execFile as execFile4 } from "node:child_process";
|
|
2749
|
+
import { promisify as promisify4 } from "node:util";
|
|
2750
|
+
|
|
2751
|
+
// src/runnerRpcServer.ts
|
|
2752
|
+
import { parse as parse3 } from "node:url";
|
|
2753
|
+
import { WebSocketServer as WebSocketServer2 } from "ws";
|
|
2754
|
+
var handlers = /* @__PURE__ */ new Map();
|
|
2755
|
+
function registerRunnerRpcHandler(method, handler) {
|
|
2756
|
+
handlers.set(method, handler);
|
|
2757
|
+
}
|
|
2758
|
+
function isRpcRequestMessage(value) {
|
|
2759
|
+
if (!value || typeof value !== "object") return false;
|
|
2760
|
+
const candidate = value;
|
|
2761
|
+
return candidate.type === "request" && typeof candidate.id === "string" && typeof candidate.method === "string";
|
|
2762
|
+
}
|
|
2763
|
+
async function handleRequest2(socket, message) {
|
|
2764
|
+
const handler = handlers.get(message.method);
|
|
2765
|
+
let response;
|
|
2766
|
+
if (!handler) {
|
|
2767
|
+
response = { type: "response", id: message.id, ok: false, error: `Unknown runner RPC method: ${message.method}` };
|
|
2768
|
+
} else {
|
|
2769
|
+
try {
|
|
2770
|
+
const result = await handler(message.payload);
|
|
2771
|
+
response = { type: "response", id: message.id, ok: true, result };
|
|
2772
|
+
} catch (err) {
|
|
2773
|
+
response = { type: "response", id: message.id, ok: false, error: err instanceof Error ? err.message : "Internal runner error." };
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
if (socket.readyState === socket.OPEN) socket.send(JSON.stringify(response));
|
|
2777
|
+
}
|
|
2778
|
+
function attachRunnerRpcServer() {
|
|
2779
|
+
const wss = new WebSocketServer2({ noServer: true });
|
|
2780
|
+
wss.on("connection", (socket, request) => {
|
|
2781
|
+
const { query } = parse3(request.url ?? "", true);
|
|
2782
|
+
const entry = authenticateAgent(query);
|
|
2783
|
+
if (!entry) {
|
|
2784
|
+
logger.runner.warn("RUNNER-RPC", "Runner RPC WS rejected: invalid or expired agent ticket.");
|
|
2785
|
+
socket.close(4401, "Invalid or expired ticket");
|
|
2786
|
+
return;
|
|
2787
|
+
}
|
|
2788
|
+
socket.on("message", (raw) => {
|
|
2789
|
+
let parsed;
|
|
2790
|
+
try {
|
|
2791
|
+
parsed = JSON.parse(raw.toString());
|
|
2792
|
+
} catch {
|
|
2793
|
+
return;
|
|
2794
|
+
}
|
|
2795
|
+
if (!isRpcRequestMessage(parsed)) return;
|
|
2796
|
+
void handleRequest2(socket, parsed);
|
|
2797
|
+
});
|
|
2798
|
+
});
|
|
2799
|
+
logger.runner.info("RUNNER-RPC", "Runner RPC WebSocket handler attached.");
|
|
2800
|
+
return wss;
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2803
|
+
// src/vaultSecrets.ts
|
|
2804
|
+
async function resolveProjectSecretsEnv(supabase, projectId, repoId) {
|
|
2805
|
+
try {
|
|
2806
|
+
const { data, error } = await supabase.rpc("resolve_project_secrets", { p_project_id: projectId, p_repo_id: repoId });
|
|
2807
|
+
if (error) throw error;
|
|
2808
|
+
const rows = data ?? [];
|
|
2809
|
+
const env = {};
|
|
2810
|
+
for (const row of rows) env[row.name] = row.value;
|
|
2811
|
+
if (rows.length > 0) {
|
|
2812
|
+
logger.runner.info(
|
|
2813
|
+
"VAULT",
|
|
2814
|
+
`Resolved ${rows.length} secret(s) for project ${projectId}${repoId ? ` (repo ${repoId})` : ""}: ${rows.map((r) => r.name).join(", ")}.`
|
|
2815
|
+
);
|
|
2816
|
+
}
|
|
2817
|
+
return env;
|
|
2818
|
+
} catch (err) {
|
|
2819
|
+
logger.runner.error("VAULT", `Failed to resolve secrets for project ${projectId}${repoId ? ` (repo ${repoId})` : ""}:`, err);
|
|
2820
|
+
return {};
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
// src/projectCommands.ts
|
|
2825
|
+
function registerProjectCommandRpcHandler(supabase, config) {
|
|
2826
|
+
registerRunnerRpcHandler("run_project_command", (payload) => runProjectCommand(supabase, config, payload));
|
|
2827
|
+
}
|
|
2828
|
+
async function listApprovedProjectCommands(supabase, projectId) {
|
|
2829
|
+
try {
|
|
2830
|
+
const { data, error } = await supabase.rpc("resolve_project_commands", { p_project_id: projectId, p_repo_id: null });
|
|
2831
|
+
if (error) throw error;
|
|
2832
|
+
return (data ?? []).map((row) => ({ name: row.name, description: row.description }));
|
|
2833
|
+
} catch (err) {
|
|
2834
|
+
logger.runner.warn("PROJECT_COMMAND", `Failed to resolve approved commands for project ${projectId}:`, err);
|
|
2835
|
+
return [];
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
var execFileAsync4 = promisify4(execFile4);
|
|
2839
|
+
var OUTPUT_TAIL_CHARS = 2e4;
|
|
2840
|
+
var COMMAND_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
2841
|
+
var MAX_BUFFER = 10 * 1024 * 1024;
|
|
2842
|
+
var SECRET_PLACEHOLDER_RE = /\{\{secret:([A-Z][A-Z0-9_]*)\}\}/g;
|
|
2843
|
+
var SECRET_ENV_PLACEHOLDER_RE = /^\{\{secret_env:([A-Z][A-Z0-9_]*)\}\}$/;
|
|
2844
|
+
function isRunProjectCommandPayload(value) {
|
|
2845
|
+
if (!value || typeof value !== "object") return false;
|
|
2846
|
+
const candidate = value;
|
|
2847
|
+
return typeof candidate.projectId === "string" && Array.isArray(candidate.argv) && candidate.argv.length > 0 && candidate.argv.every((entry) => typeof entry === "string");
|
|
2848
|
+
}
|
|
2849
|
+
function tail(text) {
|
|
2850
|
+
return text.length > OUTPUT_TAIL_CHARS ? text.slice(text.length - OUTPUT_TAIL_CHARS) : text;
|
|
2851
|
+
}
|
|
2852
|
+
async function runProjectCommand(supabase, config, payload) {
|
|
2853
|
+
if (!isRunProjectCommandPayload(payload)) throw new Error("Malformed run_project_command payload.");
|
|
2854
|
+
const { projectId, argv } = payload;
|
|
2855
|
+
const registered = config.registeredProjects.find((project) => project.id === projectId);
|
|
2856
|
+
if (!registered) throw new Error("This runner does not have that project registered.");
|
|
2857
|
+
const secretsEnv = await resolveProjectSecretsEnv(supabase, projectId, null);
|
|
2858
|
+
const referencedSecretNames = /* @__PURE__ */ new Set();
|
|
2859
|
+
const resolvedArgv = [];
|
|
2860
|
+
for (const part of argv) {
|
|
2861
|
+
const envOnlyMatch = part.match(SECRET_ENV_PLACEHOLDER_RE);
|
|
2862
|
+
if (envOnlyMatch) {
|
|
2863
|
+
const name = envOnlyMatch[1];
|
|
2864
|
+
if (secretsEnv[name] === void 0) throw new Error(`Secret "${name}" referenced by this command is no longer configured for this project.`);
|
|
2865
|
+
referencedSecretNames.add(name);
|
|
2866
|
+
continue;
|
|
2867
|
+
}
|
|
2868
|
+
resolvedArgv.push(
|
|
2869
|
+
part.replace(SECRET_PLACEHOLDER_RE, (_match, name) => {
|
|
2870
|
+
const value = secretsEnv[name];
|
|
2871
|
+
if (value === void 0) throw new Error(`Secret "${name}" referenced by this command is no longer configured for this project.`);
|
|
2872
|
+
referencedSecretNames.add(name);
|
|
2873
|
+
return value;
|
|
2874
|
+
})
|
|
2875
|
+
);
|
|
2876
|
+
}
|
|
2877
|
+
const secretEnv = { ...process.env };
|
|
2878
|
+
for (const name of referencedSecretNames) secretEnv[name] = secretsEnv[name];
|
|
2879
|
+
function redact(text) {
|
|
2880
|
+
let redacted = text;
|
|
2881
|
+
for (const name of referencedSecretNames) {
|
|
2882
|
+
const value = secretsEnv[name];
|
|
2883
|
+
if (value) redacted = redacted.split(value).join("***");
|
|
2884
|
+
}
|
|
2885
|
+
return redacted;
|
|
2886
|
+
}
|
|
2887
|
+
try {
|
|
2888
|
+
const { stdout, stderr } = await execFileAsync4(resolvedArgv[0], resolvedArgv.slice(1), {
|
|
2889
|
+
cwd: registered.repoPath,
|
|
2890
|
+
env: secretEnv,
|
|
2891
|
+
timeout: COMMAND_TIMEOUT_MS,
|
|
2892
|
+
maxBuffer: MAX_BUFFER
|
|
2893
|
+
});
|
|
2894
|
+
logger.runner.info("PROJECT_COMMAND", `Ran "${argv[0]}" for project ${projectId} (exit 0).`);
|
|
2895
|
+
return { ok: true, exitCode: 0, stdout: tail(redact(stdout)), stderr: tail(redact(stderr)) };
|
|
2896
|
+
} catch (err) {
|
|
2897
|
+
const failure = err;
|
|
2898
|
+
const exitCode = typeof failure.code === "number" ? failure.code : null;
|
|
2899
|
+
logger.runner.warn("PROJECT_COMMAND", `"${argv[0]}" failed for project ${projectId} (exit ${exitCode ?? "?"}).`);
|
|
2900
|
+
return {
|
|
2901
|
+
ok: false,
|
|
2902
|
+
exitCode,
|
|
2903
|
+
stdout: tail(redact(failure.stdout ?? "")),
|
|
2904
|
+
stderr: tail(redact(failure.stderr ?? failure.message))
|
|
2905
|
+
};
|
|
2906
|
+
}
|
|
2740
2907
|
}
|
|
2741
2908
|
|
|
2742
2909
|
// src/prototypeGateNote.ts
|
|
@@ -3037,6 +3204,7 @@ async function spawnSessionPty(supabase, config, entry) {
|
|
|
3037
3204
|
});
|
|
3038
3205
|
} else {
|
|
3039
3206
|
const attachmentPaths = await downloadCardAttachments(supabase, cardId);
|
|
3207
|
+
const approvedCommands = await listApprovedProjectCommands(supabase, card.project_id);
|
|
3040
3208
|
const instructions = buildInstructionsWithMcpContext(
|
|
3041
3209
|
buildInstructionsWithPrototypeGate(
|
|
3042
3210
|
buildInstructionsWithFileReferences(
|
|
@@ -3046,7 +3214,8 @@ async function spawnSessionPty(supabase, config, entry) {
|
|
|
3046
3214
|
card.prototype_first
|
|
3047
3215
|
),
|
|
3048
3216
|
cardId,
|
|
3049
|
-
card.project_id
|
|
3217
|
+
card.project_id,
|
|
3218
|
+
approvedCommands
|
|
3050
3219
|
);
|
|
3051
3220
|
const spawned = spawnAgentPty(registered.repoPath, cardId, card.title, instructions, sessionId, ticket, card.plan_first);
|
|
3052
3221
|
pty = spawned.pty;
|
|
@@ -3245,32 +3414,11 @@ async function restartCardSession(supabase, config, card) {
|
|
|
3245
3414
|
await enqueueCard(supabase, config, card);
|
|
3246
3415
|
}
|
|
3247
3416
|
|
|
3248
|
-
// src/vaultSecrets.ts
|
|
3249
|
-
async function resolveProjectSecretsEnv(supabase, projectId, repoId) {
|
|
3250
|
-
try {
|
|
3251
|
-
const { data, error } = await supabase.rpc("resolve_project_secrets", { p_project_id: projectId, p_repo_id: repoId });
|
|
3252
|
-
if (error) throw error;
|
|
3253
|
-
const rows = data ?? [];
|
|
3254
|
-
const env = {};
|
|
3255
|
-
for (const row of rows) env[row.name] = row.value;
|
|
3256
|
-
if (rows.length > 0) {
|
|
3257
|
-
logger.runner.info(
|
|
3258
|
-
"VAULT",
|
|
3259
|
-
`Resolved ${rows.length} secret(s) for project ${projectId}${repoId ? ` (repo ${repoId})` : ""}: ${rows.map((r) => r.name).join(", ")}.`
|
|
3260
|
-
);
|
|
3261
|
-
}
|
|
3262
|
-
return env;
|
|
3263
|
-
} catch (err) {
|
|
3264
|
-
logger.runner.error("VAULT", `Failed to resolve secrets for project ${projectId}${repoId ? ` (repo ${repoId})` : ""}:`, err);
|
|
3265
|
-
return {};
|
|
3266
|
-
}
|
|
3267
|
-
}
|
|
3268
|
-
|
|
3269
3417
|
// src/columnActions.ts
|
|
3270
|
-
var execAsync =
|
|
3271
|
-
var
|
|
3272
|
-
function
|
|
3273
|
-
return text.length >
|
|
3418
|
+
var execAsync = promisify5(exec);
|
|
3419
|
+
var OUTPUT_TAIL_CHARS2 = 4e3;
|
|
3420
|
+
function tail2(text) {
|
|
3421
|
+
return text.length > OUTPUT_TAIL_CHARS2 ? text.slice(text.length - OUTPUT_TAIL_CHARS2) : text;
|
|
3274
3422
|
}
|
|
3275
3423
|
function resolveTargets(project, children, repoRoot, column, timing) {
|
|
3276
3424
|
if (children.length > 0) {
|
|
@@ -3284,11 +3432,11 @@ var EXEC_OPTIONS = { maxBuffer: 10 * 1024 * 1024 };
|
|
|
3284
3432
|
async function runCommand(target, extraEnv) {
|
|
3285
3433
|
try {
|
|
3286
3434
|
const { stdout, stderr } = await execAsync(target.command, { ...EXEC_OPTIONS, cwd: target.cwd, env: { ...process.env, ...extraEnv } });
|
|
3287
|
-
return { ok: true, output:
|
|
3435
|
+
return { ok: true, output: tail2(`${stdout}${stderr}`) };
|
|
3288
3436
|
} catch (err) {
|
|
3289
3437
|
const { stdout, stderr } = err;
|
|
3290
3438
|
const message = err instanceof Error ? err.message : String(err);
|
|
3291
|
-
return { ok: false, output:
|
|
3439
|
+
return { ok: false, output: tail2(`${stdout ?? ""}${stderr ?? ""}` || message) };
|
|
3292
3440
|
}
|
|
3293
3441
|
}
|
|
3294
3442
|
var inFlightColumns = /* @__PURE__ */ new Set();
|
|
@@ -3619,12 +3767,12 @@ function stopAll() {
|
|
|
3619
3767
|
}
|
|
3620
3768
|
|
|
3621
3769
|
// src/git.ts
|
|
3622
|
-
import { execFile as
|
|
3623
|
-
import { promisify as
|
|
3624
|
-
var
|
|
3770
|
+
import { execFile as execFile5 } from "node:child_process";
|
|
3771
|
+
import { promisify as promisify6 } from "node:util";
|
|
3772
|
+
var execFileAsync5 = promisify6(execFile5);
|
|
3625
3773
|
async function pushBranchToMain(repoPath) {
|
|
3626
3774
|
try {
|
|
3627
|
-
await
|
|
3775
|
+
await execFileAsync5("git", ["push", "origin", "HEAD:main"], { cwd: repoPath });
|
|
3628
3776
|
return { ok: true };
|
|
3629
3777
|
} catch (err) {
|
|
3630
3778
|
const stderr = err.stderr;
|
|
@@ -3884,14 +4032,14 @@ async function checkPendingDevServerRestarts(supabase, config) {
|
|
|
3884
4032
|
}
|
|
3885
4033
|
|
|
3886
4034
|
// src/terminalServer.ts
|
|
3887
|
-
import { parse as
|
|
3888
|
-
import { WebSocketServer as
|
|
4035
|
+
import { parse as parse4 } from "node:url";
|
|
4036
|
+
import { WebSocketServer as WebSocketServer3 } from "ws";
|
|
3889
4037
|
var FRAME_TYPE_RESIZE = 0;
|
|
3890
4038
|
var FRAME_TYPE_IMAGE = 1;
|
|
3891
4039
|
function attachTerminalServer() {
|
|
3892
|
-
const wss = new
|
|
4040
|
+
const wss = new WebSocketServer3({ noServer: true });
|
|
3893
4041
|
wss.on("connection", (socket, request) => {
|
|
3894
|
-
const { query } =
|
|
4042
|
+
const { query } = parse4(request.url ?? "", true);
|
|
3895
4043
|
const sessionId = typeof query.sessionId === "string" ? query.sessionId : null;
|
|
3896
4044
|
const ticket = typeof query.ticket === "string" ? query.ticket : null;
|
|
3897
4045
|
if (!sessionId || !ticket) {
|
|
@@ -4072,6 +4220,8 @@ async function startRunner() {
|
|
|
4072
4220
|
const httpServer = createServer2();
|
|
4073
4221
|
const terminalWss = attachTerminalServer();
|
|
4074
4222
|
const agentEventsWss = attachAgentEventsServer();
|
|
4223
|
+
const runnerRpcWss = attachRunnerRpcServer();
|
|
4224
|
+
registerProjectCommandRpcHandler(supabase, config);
|
|
4075
4225
|
attachFilesServer(httpServer, supabase, config);
|
|
4076
4226
|
httpServer.on("upgrade", (request, socket, head) => {
|
|
4077
4227
|
const pathname = (request.url ?? "").split("?")[0];
|
|
@@ -4079,6 +4229,8 @@ async function startRunner() {
|
|
|
4079
4229
|
terminalWss.handleUpgrade(request, socket, head, (ws) => terminalWss.emit("connection", ws, request));
|
|
4080
4230
|
} else if (pathname === "/agent-events") {
|
|
4081
4231
|
agentEventsWss.handleUpgrade(request, socket, head, (ws) => agentEventsWss.emit("connection", ws, request));
|
|
4232
|
+
} else if (pathname === "/runner-rpc") {
|
|
4233
|
+
runnerRpcWss.handleUpgrade(request, socket, head, (ws) => runnerRpcWss.emit("connection", ws, request));
|
|
4082
4234
|
} else {
|
|
4083
4235
|
socket.destroy();
|
|
4084
4236
|
}
|
package/dist/index.js
CHANGED
|
@@ -2415,7 +2415,7 @@ async function ensureBranchWatchers(supabase, config) {
|
|
|
2415
2415
|
// src/columnActions.ts
|
|
2416
2416
|
import { exec } from "node:child_process";
|
|
2417
2417
|
import { join as join12 } from "node:path";
|
|
2418
|
-
import { promisify as
|
|
2418
|
+
import { promisify as promisify5 } from "node:util";
|
|
2419
2419
|
|
|
2420
2420
|
// src/orchestrator.ts
|
|
2421
2421
|
import { randomUUID as randomUUID7 } from "node:crypto";
|
|
@@ -2621,10 +2621,177 @@ ${list}`;
|
|
|
2621
2621
|
}
|
|
2622
2622
|
|
|
2623
2623
|
// src/mcpContextNote.ts
|
|
2624
|
-
function buildInstructionsWithMcpContext(instructions, cardId, projectId) {
|
|
2625
|
-
|
|
2624
|
+
function buildInstructionsWithMcpContext(instructions, cardId, projectId, approvedCommands) {
|
|
2625
|
+
const base = `${instructions}
|
|
2626
2626
|
|
|
2627
2627
|
Agent Board MCP: this card's id is \`${cardId}\` and its project id is \`${projectId}\` \u2014 pass these directly to board tools (\`update_card\`, \`create_card\`, \`add_card_comment\`, etc.), no lookup needed. If this task turns out too large for one session, use \`create_card\` to split it into subtask cards (chain them with \`prerequisiteCardId\` when one depends on another) instead of trying to fit everything here. If part of the delivery needs a human to do something you can't do yourself (rotate a secret, approve something in a third-party dashboard, delete a resource manually, etc.), register each step with \`add_manual_actions\` instead of just mentioning it in your final message \u2014 that's the only place humans will reliably see it. Before finishing, post a short summary of what you did as a comment on this card via \`add_card_comment\` so the project's humans have visibility without opening this terminal.`;
|
|
2628
|
+
if (approvedCommands.length === 0) return base;
|
|
2629
|
+
const names = approvedCommands.map((command) => command.description ? `${command.name} (${command.description})` : command.name).join(", ");
|
|
2630
|
+
return `${base}
|
|
2631
|
+
|
|
2632
|
+
This project already has approved custom command templates you can invoke instead of running the equivalent shell command yourself or proposing a near-duplicate: ${names}. Call \`list_project_commands\` first to see each one's declared arguments, then \`run_project_command\` to invoke it.`;
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
// src/projectCommands.ts
|
|
2636
|
+
import { execFile as execFile4 } from "node:child_process";
|
|
2637
|
+
import { promisify as promisify4 } from "node:util";
|
|
2638
|
+
|
|
2639
|
+
// src/runnerRpcServer.ts
|
|
2640
|
+
import { parse as parse3 } from "node:url";
|
|
2641
|
+
import { WebSocketServer as WebSocketServer2 } from "ws";
|
|
2642
|
+
var handlers = /* @__PURE__ */ new Map();
|
|
2643
|
+
function registerRunnerRpcHandler(method, handler) {
|
|
2644
|
+
handlers.set(method, handler);
|
|
2645
|
+
}
|
|
2646
|
+
function isRpcRequestMessage(value) {
|
|
2647
|
+
if (!value || typeof value !== "object") return false;
|
|
2648
|
+
const candidate = value;
|
|
2649
|
+
return candidate.type === "request" && typeof candidate.id === "string" && typeof candidate.method === "string";
|
|
2650
|
+
}
|
|
2651
|
+
async function handleRequest2(socket, message) {
|
|
2652
|
+
const handler = handlers.get(message.method);
|
|
2653
|
+
let response;
|
|
2654
|
+
if (!handler) {
|
|
2655
|
+
response = { type: "response", id: message.id, ok: false, error: `Unknown runner RPC method: ${message.method}` };
|
|
2656
|
+
} else {
|
|
2657
|
+
try {
|
|
2658
|
+
const result = await handler(message.payload);
|
|
2659
|
+
response = { type: "response", id: message.id, ok: true, result };
|
|
2660
|
+
} catch (err) {
|
|
2661
|
+
response = { type: "response", id: message.id, ok: false, error: err instanceof Error ? err.message : "Internal runner error." };
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
if (socket.readyState === socket.OPEN) socket.send(JSON.stringify(response));
|
|
2665
|
+
}
|
|
2666
|
+
function attachRunnerRpcServer() {
|
|
2667
|
+
const wss = new WebSocketServer2({ noServer: true });
|
|
2668
|
+
wss.on("connection", (socket, request) => {
|
|
2669
|
+
const { query } = parse3(request.url ?? "", true);
|
|
2670
|
+
const entry = authenticateAgent(query);
|
|
2671
|
+
if (!entry) {
|
|
2672
|
+
logger.runner.warn("RUNNER-RPC", "Runner RPC WS rejected: invalid or expired agent ticket.");
|
|
2673
|
+
socket.close(4401, "Invalid or expired ticket");
|
|
2674
|
+
return;
|
|
2675
|
+
}
|
|
2676
|
+
socket.on("message", (raw) => {
|
|
2677
|
+
let parsed;
|
|
2678
|
+
try {
|
|
2679
|
+
parsed = JSON.parse(raw.toString());
|
|
2680
|
+
} catch {
|
|
2681
|
+
return;
|
|
2682
|
+
}
|
|
2683
|
+
if (!isRpcRequestMessage(parsed)) return;
|
|
2684
|
+
void handleRequest2(socket, parsed);
|
|
2685
|
+
});
|
|
2686
|
+
});
|
|
2687
|
+
logger.runner.info("RUNNER-RPC", "Runner RPC WebSocket handler attached.");
|
|
2688
|
+
return wss;
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
// src/vaultSecrets.ts
|
|
2692
|
+
async function resolveProjectSecretsEnv(supabase, projectId, repoId) {
|
|
2693
|
+
try {
|
|
2694
|
+
const { data, error } = await supabase.rpc("resolve_project_secrets", { p_project_id: projectId, p_repo_id: repoId });
|
|
2695
|
+
if (error) throw error;
|
|
2696
|
+
const rows = data ?? [];
|
|
2697
|
+
const env = {};
|
|
2698
|
+
for (const row of rows) env[row.name] = row.value;
|
|
2699
|
+
if (rows.length > 0) {
|
|
2700
|
+
logger.runner.info(
|
|
2701
|
+
"VAULT",
|
|
2702
|
+
`Resolved ${rows.length} secret(s) for project ${projectId}${repoId ? ` (repo ${repoId})` : ""}: ${rows.map((r) => r.name).join(", ")}.`
|
|
2703
|
+
);
|
|
2704
|
+
}
|
|
2705
|
+
return env;
|
|
2706
|
+
} catch (err) {
|
|
2707
|
+
logger.runner.error("VAULT", `Failed to resolve secrets for project ${projectId}${repoId ? ` (repo ${repoId})` : ""}:`, err);
|
|
2708
|
+
return {};
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
// src/projectCommands.ts
|
|
2713
|
+
function registerProjectCommandRpcHandler(supabase, config) {
|
|
2714
|
+
registerRunnerRpcHandler("run_project_command", (payload) => runProjectCommand(supabase, config, payload));
|
|
2715
|
+
}
|
|
2716
|
+
async function listApprovedProjectCommands(supabase, projectId) {
|
|
2717
|
+
try {
|
|
2718
|
+
const { data, error } = await supabase.rpc("resolve_project_commands", { p_project_id: projectId, p_repo_id: null });
|
|
2719
|
+
if (error) throw error;
|
|
2720
|
+
return (data ?? []).map((row) => ({ name: row.name, description: row.description }));
|
|
2721
|
+
} catch (err) {
|
|
2722
|
+
logger.runner.warn("PROJECT_COMMAND", `Failed to resolve approved commands for project ${projectId}:`, err);
|
|
2723
|
+
return [];
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
var execFileAsync4 = promisify4(execFile4);
|
|
2727
|
+
var OUTPUT_TAIL_CHARS = 2e4;
|
|
2728
|
+
var COMMAND_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
2729
|
+
var MAX_BUFFER = 10 * 1024 * 1024;
|
|
2730
|
+
var SECRET_PLACEHOLDER_RE = /\{\{secret:([A-Z][A-Z0-9_]*)\}\}/g;
|
|
2731
|
+
var SECRET_ENV_PLACEHOLDER_RE = /^\{\{secret_env:([A-Z][A-Z0-9_]*)\}\}$/;
|
|
2732
|
+
function isRunProjectCommandPayload(value) {
|
|
2733
|
+
if (!value || typeof value !== "object") return false;
|
|
2734
|
+
const candidate = value;
|
|
2735
|
+
return typeof candidate.projectId === "string" && Array.isArray(candidate.argv) && candidate.argv.length > 0 && candidate.argv.every((entry) => typeof entry === "string");
|
|
2736
|
+
}
|
|
2737
|
+
function tail(text) {
|
|
2738
|
+
return text.length > OUTPUT_TAIL_CHARS ? text.slice(text.length - OUTPUT_TAIL_CHARS) : text;
|
|
2739
|
+
}
|
|
2740
|
+
async function runProjectCommand(supabase, config, payload) {
|
|
2741
|
+
if (!isRunProjectCommandPayload(payload)) throw new Error("Malformed run_project_command payload.");
|
|
2742
|
+
const { projectId, argv } = payload;
|
|
2743
|
+
const registered = config.registeredProjects.find((project) => project.id === projectId);
|
|
2744
|
+
if (!registered) throw new Error("This runner does not have that project registered.");
|
|
2745
|
+
const secretsEnv = await resolveProjectSecretsEnv(supabase, projectId, null);
|
|
2746
|
+
const referencedSecretNames = /* @__PURE__ */ new Set();
|
|
2747
|
+
const resolvedArgv = [];
|
|
2748
|
+
for (const part of argv) {
|
|
2749
|
+
const envOnlyMatch = part.match(SECRET_ENV_PLACEHOLDER_RE);
|
|
2750
|
+
if (envOnlyMatch) {
|
|
2751
|
+
const name = envOnlyMatch[1];
|
|
2752
|
+
if (secretsEnv[name] === void 0) throw new Error(`Secret "${name}" referenced by this command is no longer configured for this project.`);
|
|
2753
|
+
referencedSecretNames.add(name);
|
|
2754
|
+
continue;
|
|
2755
|
+
}
|
|
2756
|
+
resolvedArgv.push(
|
|
2757
|
+
part.replace(SECRET_PLACEHOLDER_RE, (_match, name) => {
|
|
2758
|
+
const value = secretsEnv[name];
|
|
2759
|
+
if (value === void 0) throw new Error(`Secret "${name}" referenced by this command is no longer configured for this project.`);
|
|
2760
|
+
referencedSecretNames.add(name);
|
|
2761
|
+
return value;
|
|
2762
|
+
})
|
|
2763
|
+
);
|
|
2764
|
+
}
|
|
2765
|
+
const secretEnv = { ...process.env };
|
|
2766
|
+
for (const name of referencedSecretNames) secretEnv[name] = secretsEnv[name];
|
|
2767
|
+
function redact(text) {
|
|
2768
|
+
let redacted = text;
|
|
2769
|
+
for (const name of referencedSecretNames) {
|
|
2770
|
+
const value = secretsEnv[name];
|
|
2771
|
+
if (value) redacted = redacted.split(value).join("***");
|
|
2772
|
+
}
|
|
2773
|
+
return redacted;
|
|
2774
|
+
}
|
|
2775
|
+
try {
|
|
2776
|
+
const { stdout, stderr } = await execFileAsync4(resolvedArgv[0], resolvedArgv.slice(1), {
|
|
2777
|
+
cwd: registered.repoPath,
|
|
2778
|
+
env: secretEnv,
|
|
2779
|
+
timeout: COMMAND_TIMEOUT_MS,
|
|
2780
|
+
maxBuffer: MAX_BUFFER
|
|
2781
|
+
});
|
|
2782
|
+
logger.runner.info("PROJECT_COMMAND", `Ran "${argv[0]}" for project ${projectId} (exit 0).`);
|
|
2783
|
+
return { ok: true, exitCode: 0, stdout: tail(redact(stdout)), stderr: tail(redact(stderr)) };
|
|
2784
|
+
} catch (err) {
|
|
2785
|
+
const failure = err;
|
|
2786
|
+
const exitCode = typeof failure.code === "number" ? failure.code : null;
|
|
2787
|
+
logger.runner.warn("PROJECT_COMMAND", `"${argv[0]}" failed for project ${projectId} (exit ${exitCode ?? "?"}).`);
|
|
2788
|
+
return {
|
|
2789
|
+
ok: false,
|
|
2790
|
+
exitCode,
|
|
2791
|
+
stdout: tail(redact(failure.stdout ?? "")),
|
|
2792
|
+
stderr: tail(redact(failure.stderr ?? failure.message))
|
|
2793
|
+
};
|
|
2794
|
+
}
|
|
2628
2795
|
}
|
|
2629
2796
|
|
|
2630
2797
|
// src/prototypeGateNote.ts
|
|
@@ -2925,6 +3092,7 @@ async function spawnSessionPty(supabase, config, entry) {
|
|
|
2925
3092
|
});
|
|
2926
3093
|
} else {
|
|
2927
3094
|
const attachmentPaths = await downloadCardAttachments(supabase, cardId);
|
|
3095
|
+
const approvedCommands = await listApprovedProjectCommands(supabase, card.project_id);
|
|
2928
3096
|
const instructions = buildInstructionsWithMcpContext(
|
|
2929
3097
|
buildInstructionsWithPrototypeGate(
|
|
2930
3098
|
buildInstructionsWithFileReferences(
|
|
@@ -2934,7 +3102,8 @@ async function spawnSessionPty(supabase, config, entry) {
|
|
|
2934
3102
|
card.prototype_first
|
|
2935
3103
|
),
|
|
2936
3104
|
cardId,
|
|
2937
|
-
card.project_id
|
|
3105
|
+
card.project_id,
|
|
3106
|
+
approvedCommands
|
|
2938
3107
|
);
|
|
2939
3108
|
const spawned = spawnAgentPty(registered.repoPath, cardId, card.title, instructions, sessionId, ticket, card.plan_first);
|
|
2940
3109
|
pty = spawned.pty;
|
|
@@ -3133,32 +3302,11 @@ async function restartCardSession(supabase, config, card) {
|
|
|
3133
3302
|
await enqueueCard(supabase, config, card);
|
|
3134
3303
|
}
|
|
3135
3304
|
|
|
3136
|
-
// src/vaultSecrets.ts
|
|
3137
|
-
async function resolveProjectSecretsEnv(supabase, projectId, repoId) {
|
|
3138
|
-
try {
|
|
3139
|
-
const { data, error } = await supabase.rpc("resolve_project_secrets", { p_project_id: projectId, p_repo_id: repoId });
|
|
3140
|
-
if (error) throw error;
|
|
3141
|
-
const rows = data ?? [];
|
|
3142
|
-
const env = {};
|
|
3143
|
-
for (const row of rows) env[row.name] = row.value;
|
|
3144
|
-
if (rows.length > 0) {
|
|
3145
|
-
logger.runner.info(
|
|
3146
|
-
"VAULT",
|
|
3147
|
-
`Resolved ${rows.length} secret(s) for project ${projectId}${repoId ? ` (repo ${repoId})` : ""}: ${rows.map((r) => r.name).join(", ")}.`
|
|
3148
|
-
);
|
|
3149
|
-
}
|
|
3150
|
-
return env;
|
|
3151
|
-
} catch (err) {
|
|
3152
|
-
logger.runner.error("VAULT", `Failed to resolve secrets for project ${projectId}${repoId ? ` (repo ${repoId})` : ""}:`, err);
|
|
3153
|
-
return {};
|
|
3154
|
-
}
|
|
3155
|
-
}
|
|
3156
|
-
|
|
3157
3305
|
// src/columnActions.ts
|
|
3158
|
-
var execAsync =
|
|
3159
|
-
var
|
|
3160
|
-
function
|
|
3161
|
-
return text.length >
|
|
3306
|
+
var execAsync = promisify5(exec);
|
|
3307
|
+
var OUTPUT_TAIL_CHARS2 = 4e3;
|
|
3308
|
+
function tail2(text) {
|
|
3309
|
+
return text.length > OUTPUT_TAIL_CHARS2 ? text.slice(text.length - OUTPUT_TAIL_CHARS2) : text;
|
|
3162
3310
|
}
|
|
3163
3311
|
function resolveTargets(project, children, repoRoot, column, timing) {
|
|
3164
3312
|
if (children.length > 0) {
|
|
@@ -3172,11 +3320,11 @@ var EXEC_OPTIONS = { maxBuffer: 10 * 1024 * 1024 };
|
|
|
3172
3320
|
async function runCommand(target, extraEnv) {
|
|
3173
3321
|
try {
|
|
3174
3322
|
const { stdout, stderr } = await execAsync(target.command, { ...EXEC_OPTIONS, cwd: target.cwd, env: { ...process.env, ...extraEnv } });
|
|
3175
|
-
return { ok: true, output:
|
|
3323
|
+
return { ok: true, output: tail2(`${stdout}${stderr}`) };
|
|
3176
3324
|
} catch (err) {
|
|
3177
3325
|
const { stdout, stderr } = err;
|
|
3178
3326
|
const message = err instanceof Error ? err.message : String(err);
|
|
3179
|
-
return { ok: false, output:
|
|
3327
|
+
return { ok: false, output: tail2(`${stdout ?? ""}${stderr ?? ""}` || message) };
|
|
3180
3328
|
}
|
|
3181
3329
|
}
|
|
3182
3330
|
var inFlightColumns = /* @__PURE__ */ new Set();
|
|
@@ -3507,12 +3655,12 @@ function stopAll() {
|
|
|
3507
3655
|
}
|
|
3508
3656
|
|
|
3509
3657
|
// src/git.ts
|
|
3510
|
-
import { execFile as
|
|
3511
|
-
import { promisify as
|
|
3512
|
-
var
|
|
3658
|
+
import { execFile as execFile5 } from "node:child_process";
|
|
3659
|
+
import { promisify as promisify6 } from "node:util";
|
|
3660
|
+
var execFileAsync5 = promisify6(execFile5);
|
|
3513
3661
|
async function pushBranchToMain(repoPath) {
|
|
3514
3662
|
try {
|
|
3515
|
-
await
|
|
3663
|
+
await execFileAsync5("git", ["push", "origin", "HEAD:main"], { cwd: repoPath });
|
|
3516
3664
|
return { ok: true };
|
|
3517
3665
|
} catch (err) {
|
|
3518
3666
|
const stderr = err.stderr;
|
|
@@ -3772,14 +3920,14 @@ async function checkPendingDevServerRestarts(supabase, config) {
|
|
|
3772
3920
|
}
|
|
3773
3921
|
|
|
3774
3922
|
// src/terminalServer.ts
|
|
3775
|
-
import { parse as
|
|
3776
|
-
import { WebSocketServer as
|
|
3923
|
+
import { parse as parse4 } from "node:url";
|
|
3924
|
+
import { WebSocketServer as WebSocketServer3 } from "ws";
|
|
3777
3925
|
var FRAME_TYPE_RESIZE = 0;
|
|
3778
3926
|
var FRAME_TYPE_IMAGE = 1;
|
|
3779
3927
|
function attachTerminalServer() {
|
|
3780
|
-
const wss = new
|
|
3928
|
+
const wss = new WebSocketServer3({ noServer: true });
|
|
3781
3929
|
wss.on("connection", (socket, request) => {
|
|
3782
|
-
const { query } =
|
|
3930
|
+
const { query } = parse4(request.url ?? "", true);
|
|
3783
3931
|
const sessionId = typeof query.sessionId === "string" ? query.sessionId : null;
|
|
3784
3932
|
const ticket = typeof query.ticket === "string" ? query.ticket : null;
|
|
3785
3933
|
if (!sessionId || !ticket) {
|
|
@@ -3960,6 +4108,8 @@ async function startRunner() {
|
|
|
3960
4108
|
const httpServer = createServer();
|
|
3961
4109
|
const terminalWss = attachTerminalServer();
|
|
3962
4110
|
const agentEventsWss = attachAgentEventsServer();
|
|
4111
|
+
const runnerRpcWss = attachRunnerRpcServer();
|
|
4112
|
+
registerProjectCommandRpcHandler(supabase, config);
|
|
3963
4113
|
attachFilesServer(httpServer, supabase, config);
|
|
3964
4114
|
httpServer.on("upgrade", (request, socket, head) => {
|
|
3965
4115
|
const pathname = (request.url ?? "").split("?")[0];
|
|
@@ -3967,6 +4117,8 @@ async function startRunner() {
|
|
|
3967
4117
|
terminalWss.handleUpgrade(request, socket, head, (ws) => terminalWss.emit("connection", ws, request));
|
|
3968
4118
|
} else if (pathname === "/agent-events") {
|
|
3969
4119
|
agentEventsWss.handleUpgrade(request, socket, head, (ws) => agentEventsWss.emit("connection", ws, request));
|
|
4120
|
+
} else if (pathname === "/runner-rpc") {
|
|
4121
|
+
runnerRpcWss.handleUpgrade(request, socket, head, (ws) => runnerRpcWss.emit("connection", ws, request));
|
|
3970
4122
|
} else {
|
|
3971
4123
|
socket.destroy();
|
|
3972
4124
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-board-runner",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
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": {
|