Files
pikasTech-unidesk/scripts/src/agentrun.ts
T
2026-06-21 18:13:15 +00:00

7170 lines
348 KiB
TypeScript

import { chmodSync, copyFileSync, existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { spawnSync } from "node:child_process";
import { rootPath, type UniDeskConfig } from "./config";
import type { RenderedCliResult } from "./output";
import { applyLocalCaddyManagedSite } from "./pk01-caddy";
import { runSshCommandCapture, type SshCaptureResult } from "./ssh";
import { runRemoteSshCommandCapture } from "./remote";
import { startJob } from "./jobs";
import {
AGENTRUN_CONFIG_PATH,
agentRunLaneSummary,
agentRunPipelineRunName,
agentRunProviderCredentialRefs,
resolveAgentRunLaneTarget,
type AgentRunLaneSpec,
} from "./agentrun-lanes";
import {
agentRunImageArtifact,
placeholderAgentRunImage,
renderedFilesDigest,
renderedObjectsDigest,
renderAgentRunControlPlaneManifests,
renderAgentRunGitopsFiles,
type AgentRunArtifactService,
} from "./agentrun-manifests";
import { sha256Fingerprint } from "./platform-infra-ops-library";
export function agentRunHelp(): unknown {
return {
command: "agentrun get|describe|events|logs|result|ack|cancel|dispatch|create|apply|send|explain",
output: "human by default; use -o json|yaml or --raw for machine/debug output",
usage: [
"bun scripts/cli.ts agentrun get tasks --queue commander --limit 20",
"bun scripts/cli.ts agentrun describe run/<runId> --node D601 --lane v02",
"bun scripts/cli.ts agentrun get tasks -o wide",
"bun scripts/cli.ts agentrun get tasks -o json",
"bun scripts/cli.ts agentrun describe task/<taskId>",
"bun scripts/cli.ts agentrun events run/<runId> --after-seq 0 --limit 100",
"bun scripts/cli.ts agentrun logs session/<sessionId> --tail 100",
"bun scripts/cli.ts agentrun result run/<runId> --command <commandId>",
"bun scripts/cli.ts agentrun ack task/<taskId> --reader-id cli",
"bun scripts/cli.ts agentrun cancel task/<taskId> --reason <text> --dry-run",
"bun scripts/cli.ts agentrun dispatch task/<taskId>",
"bun scripts/cli.ts agentrun create task --aipod Artificer --prompt-stdin --idempotency-key <key>",
"bun scripts/cli.ts agentrun apply -f - --dry-run",
"bun scripts/cli.ts agentrun send session/<sessionId> --aipod Artificer --prompt-stdin",
"bun scripts/cli.ts agentrun explain task",
"bun scripts/cli.ts agentrun explain session-policy",
"bun scripts/cli.ts agentrun control-plane plan --node D601 --lane v02",
"bun scripts/cli.ts agentrun control-plane apply --node D601 --lane v02 --dry-run",
"bun scripts/cli.ts agentrun control-plane status --node D601 --lane v02",
"bun scripts/cli.ts agentrun control-plane secret-sync --node D601 --lane v02 --dry-run",
"bun scripts/cli.ts agentrun control-plane secret-sync --node D601 --lane v02 --confirm",
"bun scripts/cli.ts agentrun control-plane restart --node D601 --lane v02 --dry-run",
"bun scripts/cli.ts agentrun control-plane restart --node D601 --lane v02 --confirm",
"bun scripts/cli.ts agentrun control-plane trigger-current --node D601 --lane v02 --dry-run",
"bun scripts/cli.ts agentrun control-plane trigger-current --node D601 --lane v02 --confirm",
"bun scripts/cli.ts agentrun control-plane status",
"bun scripts/cli.ts agentrun control-plane status --full",
"bun scripts/cli.ts agentrun control-plane status --pipeline-run agentrun-vNN-ci-<short-sha>",
"bun scripts/cli.ts agentrun control-plane status --source-commit <full-sha>",
"bun scripts/cli.ts agentrun control-plane expose --dry-run",
"bun scripts/cli.ts agentrun control-plane expose --confirm",
"bun scripts/cli.ts agentrun control-plane trigger-current --dry-run",
"bun scripts/cli.ts agentrun control-plane trigger-current --confirm",
"bun scripts/cli.ts agentrun control-plane refresh --dry-run",
"bun scripts/cli.ts agentrun control-plane refresh --confirm",
"bun scripts/cli.ts agentrun control-plane cleanup-runners --node D601 --lane v02 --dry-run",
"bun scripts/cli.ts agentrun control-plane cleanup-runners --node D601 --lane v02 --confirm",
"bun scripts/cli.ts agentrun control-plane cleanup-runs --min-age-minutes 30 --limit 200 --dry-run",
"bun scripts/cli.ts agentrun control-plane cleanup-runs --min-age-minutes 30 --limit 200 --confirm",
"bun scripts/cli.ts agentrun control-plane cleanup-released-pvs --limit 200 --dry-run",
"bun scripts/cli.ts agentrun control-plane cleanup-released-pvs --limit 200 --confirm",
"bun scripts/cli.ts agentrun git-mirror status",
"bun scripts/cli.ts agentrun git-mirror status --full",
"bun scripts/cli.ts agentrun git-mirror sync --confirm",
"bun scripts/cli.ts agentrun git-mirror flush --confirm",
],
resources: ["task/qt", "run", "command/cmd", "runnerjob/rjob", "session/ses", "aipodspec/aps"],
description: "Operate AgentRun through Kubernetes-style resource verbs. Human output is compact by default; -o json|yaml returns the UniDesk render-only client schema, --raw exposes the REST envelope, and --node/--lane targets a YAML-declared runtime lane.",
legacyCompatibility: "queue/runs/commands/runner/sessions/aipod-specs remain as compatibility groups backed by direct HTTP; new commander work should use get/describe/events/logs/result/ack/cancel/dispatch/create/apply/send. sessions turn/steer are removed; use send only.",
};
}
export async function runAgentRunCommand(config: UniDeskConfig | null, args: string[]): Promise<Record<string, unknown> | RenderedCliResult> {
const route = normalizeAgentRunCommandArgs(args);
const { group, action, actionArgs } = route;
if (group === undefined || isHelpArg(group)) return renderAgentRunHelp(route.canonicalArgs);
if (isHelpArg(action) || actionArgs.some(isHelpArg)) return renderAgentRunHelp(route.canonicalArgs);
if (isResourceVerb(group)) return await runAgentRunResourceCommand(config, group, action, actionArgs, route.canonicalArgs);
if (isAgentRunRestCompatGroup(group)) {
return await runAgentRunRestCompatCommand(config, group, route.forwardArgs, route.canonicalArgs);
}
if (config === null) throw new Error("agentrun control-plane and git-mirror commands require UniDesk config");
if (group === "control-plane") {
if (action === "plan") return await controlPlanePlan(config, parseStatusOptions(actionArgs));
if (action === "apply") return await controlPlaneApply(config, parseLaneConfirmOptions(actionArgs));
if (action === "status") {
const options = parseStatusOptions(actionArgs);
const result = await status(config, options);
return options.full || options.raw ? result : renderAgentRunControlPlaneStatusSummary(result);
}
if (action === "secret-sync") return await secretSync(config, parseSecretSyncOptions(actionArgs));
if (action === "restart") return await restartYamlLane(config, parseLaneConfirmOptions(actionArgs));
if (action === "expose") return await exposeAgentRun(config, parseConfirmOptions(actionArgs));
if (action === "trigger-current") return await triggerCurrent(config, parseTriggerOptions(actionArgs));
if (action === "refresh") return await refresh(config, parseRefreshOptions(actionArgs));
if (action === "cleanup-runners") return await cleanupRunners(config, parseCleanupRunnersOptions(actionArgs));
if (action === "cleanup-runs") return await cleanupRuns(config, parseCleanupRunsOptions(actionArgs));
if (action === "cleanup-released-pvs") return await cleanupReleasedPvs(config, parseCleanupReleasedPvOptions(actionArgs));
}
if (group === "git-mirror") {
if (action === "status") return await gitMirrorStatus(config, parseGitMirrorStatusOptions(actionArgs));
if (action === "sync" || action === "flush") {
const options = parseGitMirrorOptions(actionArgs);
const { spec } = resolveAgentRunLaneTarget(options);
if (options.confirm && !options.wait) {
return startAsyncAgentRunJob(
`agentrun_${spec.lane}_git_mirror_${action}`,
["bun", "scripts/cli.ts", "agentrun", "git-mirror", action, "--node", spec.nodeId, "--lane", spec.lane, "--confirm", "--wait", "--timeout-seconds", String(options.timeoutSeconds)],
`Run AgentRun ${spec.version} git mirror ${action} on ${spec.nodeId}`,
);
}
return await runGitMirrorJob(config, action, options);
}
}
return unsupported(route.canonicalArgs);
}
function isAgentRunRestCompatGroup(group: string | undefined): group is AgentRunRestCompatGroup {
return group === "queue" || group === "sessions" || group === "aipod-specs" || group === "aipods" || group === "runs" || group === "commands" || group === "runner";
}
interface AgentRunCommandRoute {
canonicalArgs: string[];
group: string | undefined;
action: string | undefined;
actionArgs: string[];
forwardArgs: string[];
}
function normalizeAgentRunCommandArgs(args: string[]): AgentRunCommandRoute {
const canonicalArgs = args[0] === "v01" ? args.slice(1) : args;
const [group, action, ...actionArgs] = canonicalArgs;
return {
canonicalArgs,
group,
action,
actionArgs,
forwardArgs: canonicalArgs.slice(1),
};
}
function isHelpArg(value: string | undefined): boolean {
return value === "help" || value === "--help" || value === "-h";
}
function isResourceVerb(value: string | undefined): value is AgentRunResourceVerb {
return value === "get"
|| value === "describe"
|| value === "events"
|| value === "logs"
|| value === "result"
|| value === "ack"
|| value === "cancel"
|| value === "dispatch"
|| value === "create"
|| value === "apply"
|| value === "send"
|| value === "explain";
}
function renderAgentRunHelp(args: string[]): RenderedCliResult {
const text = agentRunHelpText(args);
return renderedCliResult(true, "agentrun help", text);
}
function agentRunHelpText(args: string[]): string {
const [verb, kind] = args.filter((arg) => !isHelpArg(arg));
if (verb === "get" && kind !== undefined) return agentRunGetKindHelp(kind);
if (verb === "get") {
return [
"Usage: bun scripts/cli.ts agentrun get <resource> [options]",
"",
"Resources: tasks|sessions|runs|commands|runnerjobs|aipodspecs",
"Output: table by default; use -o wide|name|json|yaml.",
"",
"Examples:",
" bun scripts/cli.ts agentrun get tasks --queue commander --limit 20",
" bun scripts/cli.ts agentrun get sessions --limit 20",
" bun scripts/cli.ts agentrun get tasks -o json",
].join("\n");
}
if (verb === "describe") {
return [
"Usage: bun scripts/cli.ts agentrun describe <kind/name> [--node <node> --lane <lane>] [--full] [-o json|yaml] [--raw]",
"",
"Kinds: task|run|command|runnerjob|session|aipodspec",
"Examples:",
" bun scripts/cli.ts agentrun describe task/qt_...",
" bun scripts/cli.ts agentrun describe run/<runId> --node D601 --lane v02",
" bun scripts/cli.ts agentrun describe command/cmd_... --run <runId>",
" bun scripts/cli.ts agentrun describe session/<sessionId> --full",
].join("\n");
}
if (verb === "events") {
return "Usage: bun scripts/cli.ts agentrun events run/<runId> [--after-seq N] [--limit 100] [-o json|yaml] [--raw]";
}
if (verb === "logs") {
return "Usage: bun scripts/cli.ts agentrun logs session/<sessionId> [--tail 100|--after-seq N] [--limit 100] [--full-text] [-o json|yaml] [--raw]";
}
if (verb === "result") {
return "Usage: bun scripts/cli.ts agentrun result run/<runId> --command <commandId> | agentrun result command/<commandId> --run <runId>";
}
if (verb === "ack") {
return "Usage: bun scripts/cli.ts agentrun ack task/<taskId>|session/<sessionId> [--reader-id cli]";
}
if (verb === "cancel") {
return "Usage: bun scripts/cli.ts agentrun cancel task/<taskId>|session/<sessionId>|run/<runId>|command/<commandId> --reason <text> [--run <runId>] [--dry-run]";
}
if (verb === "dispatch") {
return "Usage: bun scripts/cli.ts agentrun dispatch task/<taskId>";
}
if (verb === "create") {
return "Usage: bun scripts/cli.ts agentrun create task --aipod Artificer --prompt-stdin [--idempotency-key <key>] [--dry-run]";
}
if (verb === "apply") {
return "Usage: bun scripts/cli.ts agentrun apply -f task.yaml|json|- [--dry-run]\nTask manifests use kind: Task and spec: <AgentRun task payload>.";
}
if (verb === "send") {
return "Usage: bun scripts/cli.ts agentrun send session/<sessionId> --aipod Artificer --prompt-stdin\nThe server decides whether this becomes an internal steer or a new turn from session state.";
}
if (verb === "explain") return agentRunExplain(kind ?? "task");
if (verb === "control-plane") {
return [
"Usage: bun scripts/cli.ts agentrun control-plane <action> [options]",
"",
"Actions: plan, apply, status, secret-sync, expose, trigger-current, refresh, cleanup-runners, cleanup-runs, cleanup-released-pvs",
"Examples:",
" bun scripts/cli.ts agentrun control-plane plan --node D601 --lane v02",
" bun scripts/cli.ts agentrun control-plane apply --node D601 --lane v02 --dry-run",
" bun scripts/cli.ts agentrun control-plane status --node D601 --lane v02",
" bun scripts/cli.ts agentrun control-plane secret-sync --node D601 --lane v02 --dry-run",
" bun scripts/cli.ts agentrun control-plane trigger-current --node D601 --lane v02 --dry-run",
" bun scripts/cli.ts agentrun control-plane status",
" bun scripts/cli.ts agentrun control-plane status --pipeline-run agentrun-vNN-ci-<short-sha>",
" bun scripts/cli.ts agentrun control-plane expose --dry-run",
" bun scripts/cli.ts agentrun control-plane trigger-current --dry-run",
" bun scripts/cli.ts agentrun control-plane cleanup-runners --node D601 --lane v02 --dry-run",
" bun scripts/cli.ts agentrun control-plane cleanup-runs --min-age-minutes 30 --limit 200 --dry-run",
].join("\n");
}
if (verb === "git-mirror") {
return [
"Usage: bun scripts/cli.ts agentrun git-mirror <status|sync|flush> [--full|--raw|--confirm]",
"",
"Confirmed sync/flush returns an async job unless --wait is set.",
].join("\n");
}
if (verb !== undefined && isAgentRunRestCompatGroup(verb)) {
return [
`Compatibility group: agentrun ${verb} ...`,
"",
"Use resource primitives for daily commander work:",
" bun scripts/cli.ts agentrun get tasks --queue commander --limit 20",
" bun scripts/cli.ts agentrun describe task/<taskId>",
" bun scripts/cli.ts agentrun describe run/<runId> --node D601 --lane v02",
" bun scripts/cli.ts agentrun events run/<runId> --after-seq 0 --limit 100",
" bun scripts/cli.ts agentrun logs session/<sessionId> --tail 100",
"",
"Use --raw on a resource command when you need the direct AgentRun REST envelope.",
].join("\n");
}
return [
"Usage: bun scripts/cli.ts agentrun <verb> <resource> [options]",
"",
"Verbs: get, describe, events, logs, result, ack, cancel, dispatch, create, apply, send, explain",
"Resources: task/qt, run, command/cmd, runnerjob/rjob, session/ses, aipodspec/aps",
"",
"Common:",
" bun scripts/cli.ts agentrun get tasks --queue commander --limit 20",
" bun scripts/cli.ts agentrun describe task/<taskId>",
" bun scripts/cli.ts agentrun describe run/<runId> --node D601 --lane v02",
" bun scripts/cli.ts agentrun events run/<runId> --after-seq 0 --limit 100",
" bun scripts/cli.ts agentrun logs session/<sessionId> --tail 100",
" bun scripts/cli.ts agentrun create task --aipod Artificer --prompt-stdin",
"",
"Machine/debug output:",
" -o json|yaml emits a stable UniDesk resource object without the global JSON envelope.",
" --raw emits the direct AgentRun REST envelope.",
" --node/--lane targets a YAML-declared AgentRun runtime lane; default remains the configured public manager.",
].join("\n");
}
function agentRunGetKindHelp(kindRaw: string): string {
const kind = parseResourceKind(kindRaw);
if (kind === "task") return "Usage: bun scripts/cli.ts agentrun get tasks [--queue commander] [--state running,pending,completed,failed] [--unread] [--limit 20] [-o wide|name|json|yaml]";
if (kind === "session") return "Usage: bun scripts/cli.ts agentrun get sessions [--limit 20] [-o wide|name|json|yaml]";
if (kind === "run") return "Usage: bun scripts/cli.ts agentrun get runs --task <taskId> [--limit 20] [-o wide|name|json|yaml]";
if (kind === "command") return "Usage: bun scripts/cli.ts agentrun get commands --run <runId> [--command <commandId>] [-o wide|name|json|yaml]";
if (kind === "runnerjob") return "Usage: bun scripts/cli.ts agentrun get runnerjobs --run <runId> --command <commandId> [-o wide|name|json|yaml]";
if (kind === "aipodspec") return "Usage: bun scripts/cli.ts agentrun get aipodspecs [-o wide|name|json|yaml]";
return "Unknown resource. Supported: tasks, sessions, runs, commands, runnerjobs, aipodspecs.";
}
async function runAgentRunResourceCommand(config: UniDeskConfig | null, verb: AgentRunResourceVerb, action: string | undefined, actionArgs: string[], canonicalArgs: string[]): Promise<RenderedCliResult> {
if (isHelpArg(action) || actionArgs.some(isHelpArg)) return renderAgentRunHelp(canonicalArgs);
if (verb === "explain") return renderedCliResult(true, "agentrun explain", agentRunExplain(action ?? "task"));
const resourceArgs = action === undefined ? actionArgs : [action, ...actionArgs];
const options = parseResourceOptions(resourceArgs);
const bridgeActionArgs = stripAgentRunResourceWrapperArgs(actionArgs);
const command = `agentrun ${canonicalArgs.join(" ")}`.trim();
try {
return await withAgentRunRestTarget(resolveAgentRunRestTarget(config, options), async () => {
if (verb === "get") return await resourceGet(config, command, action, bridgeActionArgs, options);
if (verb === "describe") return await resourceDescribe(config, command, action, bridgeActionArgs, options);
if (verb === "events") return await resourceEvents(config, command, action, bridgeActionArgs, options);
if (verb === "logs") return await resourceLogs(config, command, action, bridgeActionArgs, options);
if (verb === "result") return await resourceResult(config, command, action, bridgeActionArgs, options);
if (verb === "ack") return await resourceAck(config, command, action, bridgeActionArgs, options);
if (verb === "cancel") return await resourceCancel(config, command, action, bridgeActionArgs, options);
if (verb === "dispatch") return await resourceDispatch(config, command, action, bridgeActionArgs, options);
if (verb === "create") return await resourceCreate(config, command, action, bridgeActionArgs, options);
if (verb === "apply") return await resourceApply(config, command, bridgeActionArgs, options);
if (verb === "send") return await resourceSessionPromptCommand(config, command, action, bridgeActionArgs, options);
return renderedCliResult(false, command, `Unsupported AgentRun resource command. Try: bun scripts/cli.ts agentrun --help`);
});
} catch (error) {
if (error instanceof AgentRunRestError) return renderAgentRunRestError(command, error, options);
return renderedCliResult(false, command, `Error: ${error instanceof Error ? error.message : String(error)}`);
}
return renderedCliResult(false, command, `Unsupported AgentRun resource command. Try: bun scripts/cli.ts agentrun --help`);
}
export function stripAgentRunResourceWrapperArgs(args: string[]): string[] {
const result: string[] = [];
for (let index = 0; index < args.length; index += 1) {
const arg = args[index] ?? "";
if (arg === "-o" || arg === "--output") {
index += 1;
continue;
}
if (arg === "--node" || arg === "--lane") {
index += 1;
continue;
}
if (arg.startsWith("--output=") || arg.startsWith("-o=")) continue;
if (arg.startsWith("--node=") || arg.startsWith("--lane=")) continue;
if (arg === "--full" || arg === "--raw" || arg === "--debug" || arg === "--full-text") continue;
result.push(arg);
}
return result;
}
function parseResourceOptions(args: string[]): AgentRunResourceOptions {
const options: AgentRunResourceOptions = {
output: "human",
full: false,
raw: false,
debug: false,
limit: 20,
queue: null,
state: null,
unread: false,
readerId: "cli",
taskId: null,
runId: null,
commandId: null,
sessionId: null,
afterSeq: null,
tail: null,
fullText: false,
reason: null,
dryRun: false,
file: null,
aipod: null,
idempotencyKey: null,
promptStdin: false,
node: null,
lane: null,
passthroughArgs: [],
};
const valueFlags = new Set(["-o", "--output", "--limit", "--queue", "--state", "--reader-id", "--task", "--task-id", "--run", "--run-id", "--command", "--command-id", "--session", "--session-id", "--after-seq", "--tail", "--reason", "-f", "--file", "--filename", "--aipod", "--idempotency-key", "--node", "--lane"]);
const booleanFlags = new Set(["--full", "--raw", "--debug", "--unread", "--dry-run", "--full-text", "--prompt-stdin", "--stdin"]);
for (let index = 0; index < args.length; index += 1) {
const arg = args[index] ?? "";
if (!arg.startsWith("-")) continue;
if (arg.startsWith("--") && arg.includes("=")) {
const [flag, ...rest] = arg.split("=");
applyResourceOption(options, flag, rest.join("="));
continue;
}
if (booleanFlags.has(arg)) {
applyResourceOption(options, arg, null);
continue;
}
if (valueFlags.has(arg)) {
const value = args[index + 1];
if (value === undefined || value.startsWith("--")) throw new Error(`${arg} requires a value`);
applyResourceOption(options, arg, value);
index += 1;
continue;
}
}
return options;
}
function applyResourceOption(options: AgentRunResourceOptions, flag: string, value: string | null): void {
if (flag === "-o" || flag === "--output") {
if (value !== "human" && value !== "wide" && value !== "name" && value !== "json" && value !== "yaml") throw new Error(`${flag} must be one of human,wide,name,json,yaml`);
options.output = value;
return;
}
if (flag === "--full") options.full = true;
else if (flag === "--raw") options.raw = true;
else if (flag === "--debug") options.debug = true;
else if (flag === "--unread") options.unread = true;
else if (flag === "--dry-run") options.dryRun = true;
else if (flag === "--full-text") options.fullText = true;
else if (flag === "--prompt-stdin" || flag === "--stdin") options.promptStdin = true;
else if (flag === "--limit") options.limit = parseNonNegativeInt(value, "--limit", 20, 500);
else if (flag === "--queue") options.queue = requiredValue(value, flag);
else if (flag === "--state") options.state = requiredValue(value, flag);
else if (flag === "--reader-id") options.readerId = requiredValue(value, flag);
else if (flag === "--task" || flag === "--task-id") options.taskId = requiredValue(value, flag);
else if (flag === "--run" || flag === "--run-id") options.runId = requiredValue(value, flag);
else if (flag === "--command" || flag === "--command-id") options.commandId = requiredValue(value, flag);
else if (flag === "--session" || flag === "--session-id") options.sessionId = requiredValue(value, flag);
else if (flag === "--after-seq") options.afterSeq = parseNonNegativeInt(value, "--after-seq", 0, Number.MAX_SAFE_INTEGER);
else if (flag === "--tail") options.tail = parseNonNegativeInt(value, "--tail", 100, 1000);
else if (flag === "--reason") options.reason = requiredValue(value, flag);
else if (flag === "-f" || flag === "--file" || flag === "--filename") options.file = requiredValue(value, flag);
else if (flag === "--aipod") options.aipod = requiredValue(value, flag);
else if (flag === "--idempotency-key") options.idempotencyKey = requiredValue(value, flag);
else if (flag === "--node") options.node = requiredValue(value, flag);
else if (flag === "--lane") options.lane = requiredValue(value, flag);
}
function parseNonNegativeInt(raw: string | null, flag: string, defaultValue: number, maxValue: number): number {
if (raw === null || raw.length === 0) return defaultValue;
const value = Number(raw);
if (!Number.isInteger(value) || value < 0) throw new Error(`${flag} must be a non-negative integer`);
return Math.min(value, maxValue);
}
function requiredValue(value: string | null, flag: string): string {
if (value === null || value.length === 0) throw new Error(`${flag} requires a value`);
return value;
}
async function resourceGet(config: UniDeskConfig | null, command: string, action: string | undefined, args: string[], options: AgentRunResourceOptions): Promise<RenderedCliResult> {
const kind = parseResourceKind(action);
if (kind === null) throw new Error("get requires a resource: tasks|sessions|runs|commands|runnerjobs|aipodspecs");
let result: Record<string, unknown>;
if (kind === "task") {
const taskListArgs = options.unread
? ["commander", "--reader-id", options.readerId, "--limit", String(options.limit)]
: [
"list",
"--state",
taskListState(options),
"--limit",
String(options.queue === null ? options.limit : Math.max(options.limit, 100)),
...(options.queue === null ? [] : ["--queue", options.queue]),
];
result = await runAgentRunRestCommand(config, "queue", taskListArgs);
return renderResourceResult(command, result, options, "Task", normalizeTaskItems(innerData(result), options).slice(0, options.limit));
}
if (kind === "session") {
result = await runAgentRunRestCommand(config, "sessions", ["ps", "--limit", String(options.limit)]);
return renderResourceResult(command, result, options, "Session", normalizeSessionItems(innerData(result)).slice(0, options.limit));
}
if (kind === "aipodspec") {
result = await runAgentRunRestCommand(config, "aipod-specs", ["list"]);
return renderResourceResult(command, result, options, "AipodSpec", normalizeAipodSpecItems(innerData(result)).slice(0, options.limit));
}
if (kind === "run" && options.taskId !== null) {
result = await runAgentRunRestCommand(config, "queue", ["show", options.taskId]);
const task = record(innerData(result));
return renderResourceResult(command, result, options, "Run", normalizeAttemptResources(task, "run"));
}
if (kind === "command" && options.runId !== null && options.commandId !== null) {
result = await runAgentRunRestCommand(config, "commands", ["show", options.commandId, "--run-id", options.runId]);
return renderResourceResult(command, result, options, "Command", normalizeSingleCommand(innerData(result)));
}
if (kind === "runnerjob" && options.runId !== null && options.commandId !== null) {
result = await runAgentRunRestCommand(config, "runner", ["jobs", "--run-id", options.runId, "--command-id", options.commandId]);
return renderResourceResult(command, result, options, "RunnerJob", normalizeRunnerJobItems(innerData(result)));
}
throw new Error(`get ${kind}s requires more context; use --task, --run/--command, or describe <kind>/<id>`);
}
async function resourceDescribe(config: UniDeskConfig | null, command: string, action: string | undefined, args: string[], options: AgentRunResourceOptions): Promise<RenderedCliResult> {
const ref = parseResourceRef(action, args);
if (ref.kind === "task") {
const result = await runAgentRunRestCommand(config, "queue", ["show", ref.name, ...(options.full ? ["--full"] : [])]);
const data = record(innerData(result));
const task = unwrapTaskDetail(data);
if (options.raw) return renderMachine(command, result, "json", result.ok !== false);
if (options.output === "json" || options.output === "yaml") return renderMachine(command, { kind: ref.kind, name: ref.name, resource: task }, options.output, result.ok !== false);
return renderedCliResult(result.ok !== false, command, renderTaskDescription(task, options));
}
if (ref.kind === "run") {
const result = await runAgentRunRestCommand(config, "runs", ["show", ref.name, ...(options.full ? ["--full"] : [])]);
return renderDescribe(command, result, options, ref, renderGenericDescription(ref, innerData(result)));
}
if (ref.kind === "command") {
const runId = options.runId ?? requiredContext("command describe", "--run <runId>");
const result = await runAgentRunRestCommand(config, "commands", ["show", ref.name, "--run-id", runId, ...(options.full ? ["--full"] : [])]);
return renderDescribe(command, result, options, ref, renderGenericDescription(ref, innerData(result)));
}
if (ref.kind === "runnerjob") {
const runId = options.runId ?? requiredContext("runnerjob describe", "--run <runId>");
const result = await runAgentRunRestCommand(config, "runner", ["job-status", ref.name, "--run-id", runId]);
return renderDescribe(command, result, options, ref, renderGenericDescription(ref, innerData(result)));
}
if (ref.kind === "session") {
const result = await runAgentRunRestCommand(config, "sessions", ["show", ref.name, ...(options.full ? ["--full"] : [])]);
return renderDescribe(command, result, options, ref, renderGenericDescription(ref, innerData(result)));
}
if (ref.kind === "aipodspec") {
const result = await runAgentRunRestCommand(config, "aipod-specs", ["show", ref.name, ...(options.full ? ["--full"] : [])]);
return renderDescribe(command, result, options, ref, renderGenericDescription(ref, innerData(result)));
}
throw new Error(`unsupported describe kind: ${ref.kind}`);
}
async function resourceEvents(config: UniDeskConfig | null, command: string, action: string | undefined, args: string[], options: AgentRunResourceOptions): Promise<RenderedCliResult> {
const ref = parseResourceRef(action, args, "run");
const runId = ref.kind === "run" ? ref.name : options.runId ?? requiredContext("events", "--run <runId>");
const eventArgs = ["events", runId, "--after-seq", String(options.afterSeq ?? 0), "--limit", String(options.limit), "--tail-summary"];
const result = await runAgentRunRestCommand(config, "runs", eventArgs);
return renderEventLike(command, result, options, "Event", normalizeEventItems(innerData(result)), runId);
}
async function resourceLogs(config: UniDeskConfig | null, command: string, action: string | undefined, args: string[], options: AgentRunResourceOptions): Promise<RenderedCliResult> {
const ref = parseResourceRef(action, args, "session");
if (ref.kind !== "session") throw new Error("logs currently requires session/<sessionId>");
const effectiveLimit = options.tail ?? options.limit;
const logArgs = ["output", ref.name, "--limit", String(effectiveLimit)];
if (options.afterSeq !== null) logArgs.push("--after-seq", String(options.afterSeq));
if (options.fullText) logArgs.push("--full-text");
let result: Record<string, unknown>;
if (options.afterSeq === null && options.tail !== null) {
result = await resourceLogsTailResult(config, ref.name, effectiveLimit, options.fullText);
} else {
result = await runAgentRunRestCommand(config, "sessions", logArgs);
}
return renderEventLike(command, result, { ...options, limit: effectiveLimit }, "Log", normalizeLogItems(innerData(result)), ref.name);
}
async function resourceLogsTailResult(config: UniDeskConfig | null, sessionId: string, limit: number, fullText: boolean): Promise<Record<string, unknown>> {
const session = record(innerData(await runAgentRunRestCommand(config, "sessions", ["show", sessionId])));
const lastSeq = nonNegativeIntegerOrNull(session.lastEventSeq) ?? 0;
let window = Math.max(limit * 4, 100);
let result: Record<string, unknown> | null = null;
let data: Record<string, unknown> = {};
for (let attempt = 0; attempt < 6; attempt += 1) {
const afterSeq = Math.max(0, lastSeq - window);
const queryLimit = Math.max(limit, Math.min(1000, window));
const args = ["output", sessionId, "--limit", String(queryLimit), "--after-seq", String(afterSeq)];
if (fullText) args.push("--full-text");
result = await runAgentRunRestCommand(config, "sessions", args);
data = record(innerData(result));
const items = arrayRecords(data.items);
if (items.length >= limit || afterSeq === 0) break;
window *= 2;
}
if (result === null) return await runAgentRunRestCommand(config, "sessions", ["output", sessionId, "--limit", String(limit), "--after-seq", "0"]);
const items = arrayRecords(data.items);
const tailedItems = items.slice(-limit);
const lastReturnedSeq = tailedItems.length > 0 ? nonNegativeIntegerOrNull(tailedItems[tailedItems.length - 1]?.seq) : null;
return {
...result,
data: {
...data,
items: tailedItems,
count: tailedItems.length,
cursor: lastReturnedSeq === null ? data.cursor : String(lastReturnedSeq),
clientTail: {
requested: limit,
sourceLastEventSeq: lastSeq,
scanned: items.length,
valuesPrinted: false,
},
},
};
}
async function resourceResult(config: UniDeskConfig | null, command: string, action: string | undefined, args: string[], options: AgentRunResourceOptions): Promise<RenderedCliResult> {
const ref = parseResourceRef(action, args);
if (ref.kind === "run") {
const commandId = options.commandId ?? requiredContext("run result", "--command <commandId>");
const result = await runAgentRunRestCommand(config, "runs", ["result", ref.name, "--command-id", commandId]);
return renderResultSummary(command, result, options, ref);
}
if (ref.kind === "command") {
const runId = options.runId ?? requiredContext("command result", "--run <runId>");
const result = await runAgentRunRestCommand(config, "commands", ["result", ref.name, "--run-id", runId]);
return renderResultSummary(command, result, options, ref);
}
throw new Error("result supports run/<runId> or command/<commandId>");
}
async function resourceAck(config: UniDeskConfig | null, command: string, action: string | undefined, args: string[], options: AgentRunResourceOptions): Promise<RenderedCliResult> {
const ref = parseResourceRef(action, args, "task");
const result = ref.kind === "task"
? await runAgentRunRestCommand(config, "queue", ["read", ref.name, "--reader-id", options.readerId])
: ref.kind === "session"
? await runAgentRunRestCommand(config, "sessions", ["read", ref.name, "--reader-id", options.readerId])
: null;
if (result === null) throw new Error("ack supports task/<taskId> or session/<sessionId>");
return renderMutationSummary(command, result, options, `Acknowledged ${ref.kind}/${shortId(ref.name)}`);
}
async function resourceCancel(config: UniDeskConfig | null, command: string, action: string | undefined, args: string[], options: AgentRunResourceOptions): Promise<RenderedCliResult> {
const ref = parseResourceRef(action, args, "task");
const cancelArgs = ["cancel", ref.name];
if (options.reason !== null) cancelArgs.push("--reason", options.reason);
if (ref.kind === "command") cancelArgs.push("--run-id", options.runId ?? requiredContext("command cancel", "--run <runId>"));
if (options.dryRun) {
const result = agentRunResourceCancelDryRunPlan(ref, options, rerunWithoutDryRun(command));
return renderMutationSummary(command, result, options, `Planned cancel ${ref.kind}/${shortId(ref.name)}`, [rerunWithoutDryRun(command)]);
}
const result = ref.kind === "task"
? await runAgentRunRestCommand(config, "queue", cancelArgs)
: ref.kind === "session"
? await runAgentRunRestCommand(config, "sessions", cancelArgs)
: ref.kind === "run"
? await runAgentRunRestCommand(config, "runs", cancelArgs)
: ref.kind === "command"
? await runAgentRunRestCommand(config, "commands", cancelArgs)
: null;
if (result === null) throw new Error("cancel supports task/<taskId>, session/<sessionId>, run/<runId>, or command/<commandId>");
return renderMutationSummary(command, result, options, `${options.dryRun ? "Planned cancel" : "Cancel requested"} ${ref.kind}/${shortId(ref.name)}`, options.dryRun ? [rerunWithoutDryRun(command)] : undefined);
}
function agentRunResourceCancelDryRunPlan(ref: AgentRunResourceRef, options: AgentRunResourceOptions, confirmCommand: string): Record<string, unknown> {
const body: Record<string, unknown> = {};
if (options.reason !== null) body.reason = options.reason;
if (ref.kind === "task") return agentRunDryRunPlan("task-cancel", `/api/v1/queue/tasks/${encodeURIComponent(ref.name)}/cancel`, body, confirmCommand);
if (ref.kind === "session") return agentRunDryRunPlan("session-cancel", `/api/v1/sessions/${encodeURIComponent(ref.name)}/control`, { action: "cancel", ...body }, confirmCommand);
if (ref.kind === "run") return agentRunDryRunPlan("run-cancel", `/api/v1/runs/${encodeURIComponent(ref.name)}/cancel`, body, confirmCommand);
if (ref.kind === "command") {
const runId = options.runId ?? requiredContext("command cancel", "--run <runId>");
return agentRunDryRunPlan("command-cancel", `/api/v1/commands/${encodeURIComponent(ref.name)}/cancel`, body, confirmCommand, "POST", {
commandRef: { runId, commandId: ref.name, valuesPrinted: false },
});
}
throw new Error("cancel supports task/<taskId>, session/<sessionId>, run/<runId>, or command/<commandId>");
}
async function resourceDispatch(config: UniDeskConfig | null, command: string, action: string | undefined, args: string[], options: AgentRunResourceOptions): Promise<RenderedCliResult> {
const ref = parseResourceRef(action, args, "task");
if (ref.kind !== "task") throw new Error("dispatch supports task/<taskId>");
const result = await runAgentRunRestCommand(config, "queue", ["dispatch", ref.name, ...stripLeadingResource(args, ref.name)]);
return renderMutationSummary(command, result, options, `Task dispatch submitted ${shortId(ref.name)}`, [
`bun scripts/cli.ts agentrun describe task/${ref.name}`,
]);
}
async function resourceCreate(config: UniDeskConfig | null, command: string, action: string | undefined, args: string[], options: AgentRunResourceOptions): Promise<RenderedCliResult> {
const kind = parseResourceKind(action);
if (kind !== "task") throw new Error("create currently supports: create task");
const submitArgs = ["submit", ...stripLeadingResource(args, "task")];
const result = await runAgentRunRestCommand(config, "queue", submitArgs);
return renderMutationSummary(command, result, options, "Task create submitted", options.dryRun ? [rerunWithoutDryRun(command)] : undefined);
}
async function resourceApply(config: UniDeskConfig | null, command: string, args: string[], options: AgentRunResourceOptions): Promise<RenderedCliResult> {
void config;
const file = options.file ?? requiredContext("apply", "-f <file>|-");
const raw = file === "-" ? readFileSync(0, "utf8") : readFileSync(file, "utf8");
const parsed = parseTaskManifest(raw, file);
const result = options.dryRun
? agentRunDryRunPlan("queue-submit", "/api/v1/queue/tasks", parsed, `bun scripts/cli.ts ${command.replace(/\s+--dry-run\b/gu, "").trim()}`)
: await agentRunRestRequest("agentrun queue submit", "POST", "/api/v1/queue/tasks", parsed);
return renderMutationSummary(command, result, options, `${options.dryRun ? "Dry-run applied" : "Applied"} task manifest`, options.dryRun ? [rerunWithoutDryRun(command)] : undefined);
}
async function resourceSessionPromptCommand(config: UniDeskConfig | null, command: string, action: string | undefined, args: string[], options: AgentRunResourceOptions): Promise<RenderedCliResult> {
const ref = parseResourceRef(action, args, "session");
if (ref.kind !== "session") throw new Error("send requires session/<sessionId>");
const sessionArgs = ["send", ref.name, ...stripLeadingResource(args, ref.name)];
const result = await runAgentRunRestCommand(config, "sessions", sessionArgs);
return renderMutationSummary(command, result, options, options.dryRun ? "Session send plan" : "Session send submitted");
}
function renderedCliResult(ok: boolean, command: string, renderedText: string, contentType: RenderedCliResult["contentType"] = "text/plain"): RenderedCliResult {
return { ok, command, renderedText, contentType };
}
function renderResourceResult(command: string, raw: Record<string, unknown>, options: AgentRunResourceOptions, kindLabel: string, items: Record<string, unknown>[]): RenderedCliResult {
if (options.raw) return renderMachine(command, raw, "json", raw.ok !== false);
const payload = { kind: `${kindLabel}List`, count: items.length, items };
if (options.output === "json" || options.output === "yaml") return renderMachine(command, payload, options.output);
if (options.output === "name") return renderedCliResult(raw.ok !== false, command, items.map((item) => `${String(item.kind ?? kindLabel).toLowerCase()}/${String(item.name ?? "")}`).join("\n"));
if (items.length === 0) return renderedCliResult(raw.ok !== false, command, `No ${kindLabel.toLowerCase()} resources found.`);
return renderedCliResult(raw.ok !== false, command, renderResourceTable(items, options.output === "wide"));
}
function renderDescribe(command: string, raw: Record<string, unknown>, options: AgentRunResourceOptions, ref: AgentRunResourceRef, text: string): RenderedCliResult {
if (options.raw) return renderMachine(command, raw, "json", raw.ok !== false);
if (options.output === "json" || options.output === "yaml") return renderMachine(command, { kind: ref.kind, name: ref.name, resource: innerData(raw) }, options.output, raw.ok !== false);
return renderedCliResult(raw.ok !== false, command, text);
}
function renderEventLike(command: string, raw: Record<string, unknown>, options: AgentRunResourceOptions, kind: string, items: Record<string, unknown>[], sourceName: string): RenderedCliResult {
if (options.raw) return renderMachine(command, raw, "json", raw.ok !== false);
const data = record(innerData(raw));
const payload = { kind: `${kind}List`, source: sourceName, nextAfterSeq: data.nextAfterSeq ?? null, count: items.length, items };
if (options.output === "json" || options.output === "yaml") return renderMachine(command, payload, options.output, raw.ok !== false);
const lines = [
`${kind}s for ${sourceName}`,
renderTable(["SEQ", "TYPE", "STATUS", "COMMAND", "SUMMARY"], items.map((item) => [
String(item.seq ?? "-"),
String(item.type ?? "-"),
String(item.status ?? item.phase ?? "-"),
shortId(String(item.commandId ?? "-")),
truncateOneLine(String(item.summary ?? item.text ?? "") || "-", 96),
])),
];
if (data.nextAfterSeq !== undefined && data.nextAfterSeq !== null) lines.push(`Next: bun scripts/cli.ts ${nextPagedResourceCommand(command, String(data.nextAfterSeq), options.limit)}`);
return renderedCliResult(raw.ok !== false, command, lines.join("\n"));
}
function renderResultSummary(command: string, raw: Record<string, unknown>, options: AgentRunResourceOptions, ref: AgentRunResourceRef): RenderedCliResult {
if (options.raw) return renderMachine(command, raw, "json", raw.ok !== false);
const data = record(innerData(raw));
if (options.output === "json" || options.output === "yaml") return renderMachine(command, { kind: "Result", ref, result: data }, options.output, raw.ok !== false);
const executionOk = typeof data.executionOk === "boolean"
? data.executionOk
: typeof data.ok === "boolean"
? data.ok
: raw.ok !== false;
const lines = [
`Result: ${ref.kind}/${shortId(ref.name)}`,
`State: ${displayValue(data.state ?? data.status ?? data.terminalStatus ?? "-")}`,
`OK: ${String(executionOk)}`,
];
const providerTerminalFailure = typeof data.providerTerminalFailure === "boolean" ? data.providerTerminalFailure : null;
const authority = stringOrNull(data.finalResponseAuthority);
const fallback = typeof data.finalResponseFallback === "boolean" ? data.finalResponseFallback : null;
const needsContinuation = typeof data.needsContinuation === "boolean" ? data.needsContinuation : null;
if (providerTerminalFailure !== null) lines.push(`ProviderTerminalFailure: ${String(providerTerminalFailure)}`);
if (authority !== null) lines.push(`Authority: ${authority}`);
if (fallback !== null) lines.push(`Fallback: ${String(fallback)}`);
if (needsContinuation !== null) lines.push(`NeedsContinuation: ${String(needsContinuation)}`);
const finalRecord = record(data.finalResponse);
const final = stringOrNull(data.finalResponse)
?? stringOrNull(finalRecord.content)
?? stringOrNull(finalRecord.text)
?? stringOrNull(finalRecord.message)
?? stringOrNull(data.output)
?? stringOrNull(data.summary)
?? stringOrNull(data.result);
if (final !== null) lines.push("", truncateMultiline(final, options.fullText ? 8000 : 1600));
else {
const failure = renderFailureLines(data);
lines.push("", ...(failure.length > 0 ? failure : ["No final response is available yet."]));
}
return renderedCliResult(raw.ok !== false, command, lines.join("\n"));
}
function renderMutationSummary(command: string, raw: Record<string, unknown>, options: AgentRunResourceOptions, headline: string, overrideNextLines?: string[]): RenderedCliResult {
if (options.raw) return renderMachine(command, raw, "json", raw.ok !== false);
if (options.output === "json" || options.output === "yaml") return renderMachine(command, raw, options.output, raw.ok !== false);
const data = record(innerData(raw));
const id = stringOrNull(data.id) ?? stringOrNull(data.taskId) ?? stringOrNull(data.sessionId);
const lines = [
headline,
`OK: ${String(raw.ok !== false)}`,
];
if (id !== null) lines.push(`Name: ${id}`);
const decision = stringOrNull(data.decision);
const internalCommandType = stringOrNull(data.internalCommandType);
if (data.dryRun !== undefined) lines.push(`DryRun: ${String(data.dryRun)}`);
if (data.mutation !== undefined) lines.push(`Mutation: ${String(data.mutation)}`);
if (decision !== null) lines.push(`Decision: ${decision}`);
if (internalCommandType !== null) lines.push(`InternalCommandType: ${internalCommandType}`);
const next = record(raw.next ?? data.next);
const nextLines = (overrideNextLines ?? Object.values(next).map(String)).filter((line) => line.length > 0).slice(0, 5);
if (nextLines.length > 0) lines.push("", "Next:", ...nextLines.map((line) => ` ${line}`));
return renderedCliResult(raw.ok !== false, command, lines.join("\n"));
}
function rerunWithoutDryRun(command: string): string {
return `bun scripts/cli.ts ${command.replace(/\s+--dry-run\b/gu, "").trim()}`;
}
function renderMachine(command: string, value: unknown, mode: "json" | "yaml", ok = true): RenderedCliResult {
const text = mode === "json" ? `${JSON.stringify(value, null, 2)}\n` : `${Bun.YAML.stringify(value)}\n`;
return renderedCliResult(ok, command, text, mode === "json" ? "application/json" : "application/yaml");
}
function parseResourceKind(raw: string | undefined): AgentRunResourceKind | null {
if (raw === undefined) return null;
const value = raw.toLowerCase().replace(/s$/u, "");
if (value === "task" || value === "qt" || value === "queue" || value === "queuetask") return "task";
if (value === "run") return "run";
if (value === "command" || value === "cmd") return "command";
if (value === "runnerjob" || value === "runner-job" || value === "rjob" || value === "job") return "runnerjob";
if (value === "session" || value === "ses") return "session";
if (value === "aipodspec" || value === "aipod-spec" || value === "aipod-specs" || value === "aps") return "aipodspec";
return null;
}
function parseResourceRef(action: string | undefined, args: string[], defaultKind: AgentRunResourceKind | null = null): AgentRunResourceRef {
const raw = action ?? "";
if (raw.includes("/")) {
const [kindRaw, ...nameParts] = raw.split("/");
const kind = parseResourceKind(kindRaw);
const name = nameParts.join("/");
if (kind === null || name.length === 0) throw new Error(`invalid resource reference: ${raw}`);
return { kind, name };
}
const kind = parseResourceKind(raw);
if (kind !== null) {
const name = args.find((arg) => !arg.startsWith("-") && arg !== raw);
if (name === undefined) throw new Error(`${kind} reference requires a name`);
return { kind, name };
}
if (defaultKind !== null && raw.length > 0) return { kind: defaultKind, name: raw };
throw new Error("resource reference required, for example task/<id> or session/<id>");
}
function stripLeadingResource(args: string[], resourceName: string): string[] {
let skipped = false;
const result: string[] = [];
for (const arg of args) {
if (!skipped && (arg === resourceName || arg.endsWith(`/${resourceName}`) || parseResourceKind(arg) !== null)) {
skipped = true;
continue;
}
result.push(arg);
}
return result;
}
function requiredContext(context: string, flag: string): never {
throw new Error(`${context} requires ${flag}`);
}
function innerData(raw: Record<string, unknown>): unknown {
const data = raw.data;
if (typeof data === "object" && data !== null && "data" in data && Object.keys(raw).length <= 3) return record(data).data;
return data ?? raw;
}
function normalizeTaskItems(data: unknown, options: AgentRunResourceOptions): Record<string, unknown>[] {
const items = arrayRecords(record(data).items ?? data);
return items
.filter((item) => options.queue === null || String(item.queue ?? "") === options.queue)
.filter((item) => options.state === null || options.state.split(",").includes(String(item.state ?? "")))
.filter((item) => !options.unread || item.unread === true || item.attention === true)
.map((item) => {
const attempt = record(item.latestAttempt);
return {
kind: "task",
name: item.id,
state: item.state,
queue: item.queue,
lane: item.lane,
profile: item.backendProfile,
run: attempt.runId,
command: attempt.commandId,
runnerjob: attempt.runnerJobId,
session: attempt.sessionId ?? stringOrNull(record(item.sessionRef).sessionId),
age: relativeAge(stringOrNull(item.updatedAt) ?? stringOrNull(item.createdAt)),
title: item.title,
updatedAt: item.updatedAt,
attention: item.unread === true || item.attention === true ? "unread" : "",
};
});
}
function taskListState(options: AgentRunResourceOptions): string {
const requested = options.state?.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
return requested?.[0] ?? "running";
}
function normalizeSessionItems(data: unknown): Record<string, unknown>[] {
const items = arrayRecords(record(data).items ?? data);
return items.map((item) => ({
kind: "session",
name: item.id ?? item.sessionId ?? item.name,
state: item.state ?? item.status ?? item.phase ?? "-",
queue: item.queue ?? "",
lane: item.lane ?? "",
profile: item.backendProfile ?? item.profile ?? "",
run: item.runId ?? "",
command: item.commandId ?? "",
runnerjob: item.runnerJobId ?? "",
session: item.id ?? item.sessionId ?? item.name,
age: relativeAge(stringOrNull(item.updatedAt) ?? stringOrNull(item.createdAt)),
title: item.title ?? "",
}));
}
function normalizeAipodSpecItems(data: unknown): Record<string, unknown>[] {
const items = arrayRecords(record(data).items ?? data);
return items.map((item) => ({
kind: "aipodspec",
name: item.name ?? item.id ?? item.metadata?.toString(),
state: item.state ?? "available",
queue: "",
lane: item.lane ?? "",
profile: item.backendProfile ?? item.profile ?? "",
title: item.description ?? item.title ?? "",
}));
}
function normalizeAttemptResources(task: Record<string, unknown>, kind: "run" | "command" | "runnerjob"): Record<string, unknown>[] {
const attempt = record(task.latestAttempt);
const key = kind === "run" ? "runId" : kind === "command" ? "commandId" : "runnerJobId";
const name = stringOrNull(attempt[key]);
if (name === null) return [];
return [{
kind,
name,
state: attempt.state ?? task.state,
queue: task.queue,
lane: task.lane,
run: attempt.runId,
command: attempt.commandId,
runnerjob: attempt.runnerJobId,
session: attempt.sessionId,
title: task.title,
}];
}
function normalizeSingleCommand(data: unknown): Record<string, unknown>[] {
const item = record(data);
return [{
kind: "command",
name: item.id ?? item.commandId ?? "",
state: item.state ?? item.status ?? "",
run: item.runId ?? "",
command: item.id ?? item.commandId ?? "",
session: item.sessionId ?? "",
title: item.summary ?? item.commandType ?? "",
}];
}
function normalizeRunnerJobItems(data: unknown): Record<string, unknown>[] {
const items = arrayRecords(record(data).items ?? data);
return items.map((item) => ({
kind: "runnerjob",
name: item.id ?? item.runnerJobId ?? item.name ?? item.jobName,
state: item.phase ?? item.state ?? item.status ?? "",
run: item.runId ?? "",
command: item.commandId ?? "",
runnerjob: item.id ?? item.runnerJobId ?? item.name ?? item.jobName,
session: item.sessionId ?? "",
title: item.image ?? item.namespace ?? "",
}));
}
function normalizeEventItems(data: unknown): Record<string, unknown>[] {
return arrayRecords(record(data).items ?? data).map((item) => {
const payload = record(item.payload);
return {
seq: item.seq,
type: item.type,
status: agentRunEventStatus(item, payload),
phase: item.phase ?? payload.phase,
commandId: agentRunEventCommandId(item, payload),
summary: agentRunEventSummary(item, payload),
};
});
}
function normalizeLogItems(data: unknown): Record<string, unknown>[] {
return arrayRecords(record(data).items ?? data).map((item) => {
const payload = record(item.payload);
return {
seq: item.seq,
type: item.type ?? item.role ?? "output",
status: agentRunEventStatus(item, payload),
commandId: agentRunEventCommandId(item, payload),
summary: agentRunEventSummary(item, payload),
};
});
}
function agentRunEventCommandId(item: Record<string, unknown>, payload: Record<string, unknown>): string {
return stringOrNull(item.commandId)
?? stringOrNull(payload.commandId)
?? stringOrNull(record(payload.summary).commandId)
?? "-";
}
function agentRunEventStatus(item: Record<string, unknown>, payload: Record<string, unknown>): string {
return stringOrNull(item.status)
?? stringOrNull(item.phase)
?? stringOrNull(payload.status)
?? stringOrNull(payload.phase)
?? stringOrNull(payload.failureKind)
?? (item.type === "error" ? "error" : "");
}
function agentRunEventSummary(item: Record<string, unknown>, payload: Record<string, unknown>): string {
const type = stringOrNull(item.type);
const parts: string[] = [];
const push = (value: unknown): void => {
const text = eventText(value);
if (text !== null && !parts.includes(text)) parts.push(text);
};
if (type === "error") {
push(payload.failureKind);
const error = record(payload.error);
push(error.message);
push(error.additionalDetails);
if (payload.willRetry === true) push("willRetry=true");
}
if (type === "backend_status") {
push(payload.phase);
push(formatCountField("tools", record(payload.tools).count));
push(formatCountField("bundles", record(payload.bundles).count));
push(formatCountField("promptRefs", record(payload.promptRefs).count));
push(payload.jobName);
push(payload.namespace);
}
if (type === "tool_call") {
push(payload.summary);
push(payload.toolName);
push(payload.command);
const exitCode = payload.exitCode;
if (exitCode !== undefined && exitCode !== null) push(`exit=${String(exitCode)}`);
push(payload.outputSummary);
}
if (type === "command_output") {
push(commandOutputSummary(payload));
}
if (type === "assistant_message") {
push(payload.summary);
push(payload.text);
}
push(item.summary);
push(item.outputSummary);
push(item.text);
if (type !== "command_output") {
push(payload.summary);
push(payload.outputSummary);
push(payload.text);
}
push(payload.message);
push(payload.reason);
return parts.join("; ");
}
function commandOutputSummary(payload: Record<string, unknown>): string | null {
const raw = commandOutputText(payload);
if (raw === null) return null;
const structured = parseJsonRecordFromText(raw);
if (structured !== null) {
const summary = summarizeStructuredCliOutput(structured);
if (summary !== null) return summary;
}
return summarizePartialJsonCommandOutput(raw) ?? summarizeTextCommandOutput(raw);
}
function commandOutputText(payload: Record<string, unknown>): string | null {
const summary = record(payload.summary);
const summaryText = stringOrNull(summary.text);
const payloadText = stringOrNull(payload.text);
if (summary.textTruncated === true && payloadText !== null) return payloadText;
return summaryText
?? payloadText
?? stringOrNull(payload.outputSummary)
?? eventText(payload.summary);
}
function parseJsonRecordFromText(raw: string): Record<string, unknown> | null {
const trimmed = raw.trim();
const direct = tryParseJsonRecord(trimmed);
if (direct !== null) return direct;
const firstBrace = trimmed.indexOf("{");
const lastBrace = trimmed.lastIndexOf("}");
if (firstBrace >= 0 && lastBrace > firstBrace) {
const sliced = tryParseJsonRecord(trimmed.slice(firstBrace, lastBrace + 1));
if (sliced !== null) return sliced;
}
const line = trimmed
.split(/\r?\n/u)
.map((item) => item.trim())
.reverse()
.find((item) => item.startsWith("{") && item.endsWith("}"));
return line === undefined ? null : tryParseJsonRecord(line);
}
function tryParseJsonRecord(raw: string): Record<string, unknown> | null {
try {
const parsed = JSON.parse(raw) as unknown;
return isRecord(parsed) ? parsed : null;
} catch {
return null;
}
}
function summarizeStructuredCliOutput(value: Record<string, unknown>): string | null {
const parts: string[] = [];
const action = firstPathText(value, ["action", "operation", "command", "kind"]);
const status = firstPathText(value, ["status", "state", "phase", "body.status"]);
const failure = firstPathText(value, ["failureKind", "error.failureKind", "body.failureKind"]);
const message = firstPathText(value, ["error.message", "error.additionalDetails", "message", "reason", "body.message", "body.error", "result.message"]);
const primary = [
action,
status,
labeledSummaryValue("http", firstPathText(value, ["httpStatus", "request.httpStatus"])),
].filter((item): item is string => item !== null);
if (primary.length > 0) pushSummaryPart(parts, primary.join(" "));
pushLabeledSummaryPart(parts, "failure", failure);
if (failure !== null || isFailureLikeStatus(status)) pushLabeledSummaryPart(parts, "message", message);
pushLabeledSummaryPart(parts, "trace", firstPathText(value, ["traceId", "lastTraceId", "trace.traceId", "trace.id", "body.traceId", "body.trace.id", "providerTrace.traceId", "runnerTrace.traceId"]), true);
pushLabeledSummaryPart(parts, "session", firstPathText(value, ["sessionId", "session.sessionId", "body.sessionId", "workspace.selectedAgentSessionId", "workspace.selectedConversation.sessionId"]), true);
pushLabeledSummaryPart(parts, "conversation", firstPathText(value, ["conversationId", "session.conversationId", "body.conversationId", "workspace.selectedConversationId", "workspace.selectedConversation.conversationId"]), true);
pushLabeledSummaryPart(parts, "thread", firstPathText(value, ["threadId", "session.threadId", "body.threadId", "workspace.selectedConversation.threadId"]), true);
pushLabeledSummaryPart(parts, "provider", firstPathText(value, ["providerProfile", "backendProfile", "profile", "session.providerProfile", "workspace.providerProfile"]));
pushLabeledSummaryPart(parts, "route", commandOutputRouteSummary(value));
pushLabeledSummaryPart(parts, "pipeline", firstPathText(value, ["pipelineRun", "pipelineRunName", "pipelineRun.name", "pipeline.runName", "pipeline.name"]), true);
pushLabeledSummaryPart(parts, "run", firstPathText(value, ["runId", "agentRun.runId"]), true);
pushLabeledSummaryPart(parts, "cmd", firstPathText(value, ["commandId", "agentRun.commandId", "providerTrace.commandId", "runnerTrace.commandId"]), true);
pushLabeledSummaryPart(parts, "job", firstPathText(value, ["jobName", "agentRun.jobName", "runnerJobId"]), true);
pushLabeledSummaryPart(parts, "lane", firstPathText(value, ["runtimeEndpoint.lane", "lane"]));
pushLabeledSummaryPart(parts, "ns", firstPathText(value, ["runtimeEndpoint.namespace", "namespace"]));
pushLabeledSummaryPart(parts, "actor", firstPathText(value, ["actor.username", "body.actor.username", "actor.displayName", "body.actor.displayName"]));
pushLabeledSummaryPart(parts, "auth", firstPathText(value, ["authMethod", "auth.authMethod", "body.authMethod"]));
pushLabeledSummaryPart(parts, "reply", firstPathText(value, ["reply.content", "result.reply.content", "body.reply.content", "answer", "content", "body.content"]));
if (failure === null && !isFailureLikeStatus(status)) pushLabeledSummaryPart(parts, "message", message);
const itemsCount = arrayRecords(value.items).length;
if (itemsCount > 0) pushSummaryPart(parts, `items=${itemsCount}`);
const eventsCount = arrayRecords(value.events).length;
if (eventsCount > 0) pushSummaryPart(parts, `events=${eventsCount}`);
if (value.sessionUsable === false) pushSummaryPart(parts, "sessionUsable=false");
if (record(value.summary).textTruncated === true || value.textTruncated === true || value.outputTruncated === true) pushSummaryPart(parts, "truncated=true");
return parts.length > 0 ? parts.slice(0, 10).join("; ") : null;
}
function commandOutputRouteSummary(value: Record<string, unknown>): string | null {
const route = record(value.route);
const request = record(value.request);
const method = stringOrNull(route.method) ?? stringOrNull(request.method);
const path = stringOrNull(route.path) ?? stringOrNull(request.path);
if (method !== null && path !== null) return `${method} ${path}`;
return stringOrNull(request.url) ?? stringOrNull(value.baseUrl);
}
function firstPathText(value: Record<string, unknown>, paths: string[]): string | null {
for (const path of paths) {
const text = summaryScalar(pathValue(value, path));
if (text !== null) return text;
}
return null;
}
function pathValue(value: Record<string, unknown>, path: string): unknown {
let current: unknown = value;
for (const key of path.split(".")) {
if (!isRecord(current)) return undefined;
current = current[key];
}
return current;
}
function summaryScalar(value: unknown): string | null {
if (typeof value === "string") {
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
}
if (typeof value === "number" && Number.isFinite(value)) return String(value);
if (typeof value === "boolean") return String(value);
return null;
}
function isFailureLikeStatus(value: string | null): boolean {
return value !== null && /(?:fail|error|timeout|cancel|interrupt|denied|rejected)/iu.test(value);
}
function pushLabeledSummaryPart(parts: string[], label: string, value: string | null, shorten = false): void {
const text = labeledSummaryValue(label, value, shorten);
if (text !== null) pushSummaryPart(parts, text);
}
function labeledSummaryValue(label: string, value: string | null, shorten = false): string | null {
if (value === null) return null;
const rendered = shorten ? shortId(value) : truncateOneLine(value, 80);
return `${label}=${rendered}`;
}
function pushSummaryPart(parts: string[], value: string): void {
const text = truncateOneLine(value, 160);
if (text.length > 0 && !parts.includes(text)) parts.push(text);
}
function summarizeTextCommandOutput(raw: string): string | null {
const lines = raw
.split(/\r?\n/u)
.map((line) => line.trim())
.filter((line) => line.length > 0 && !isLowSignalCommandOutputLine(line));
const signal = lines.find((line) => /(?:trace|session|conversation|thread|status|failure|error|route|http|pipeline|provider|commit|succeeded|failed|created)/iu.test(line))
?? lines[0]
?? null;
if (signal === null) return null;
return truncateOneLine(cleanCommandOutputLine(signal), 220);
}
function summarizePartialJsonCommandOutput(raw: string): string | null {
const fields = new Map<string, string>();
for (const line of raw.split(/\r?\n/u)) {
const field = jsonLineField(line);
if (field === null || isLowSignalJsonField(field.key)) continue;
if (!fields.has(field.key) && field.value.length > 0) fields.set(field.key, field.value);
}
const parts: string[] = [];
const action = fields.get("action") ?? fields.get("operation") ?? fields.get("command") ?? fields.get("kind") ?? null;
const status = fields.get("status") ?? fields.get("state") ?? fields.get("phase") ?? null;
const http = fields.get("httpStatus") ?? null;
const failure = fields.get("failureKind") ?? null;
const message = fields.get("message") ?? fields.get("reason") ?? fields.get("error") ?? null;
const primary = [action, status, labeledSummaryValue("http", http)].filter((item): item is string => item !== null);
if (primary.length > 0) pushSummaryPart(parts, primary.join(" "));
pushLabeledSummaryPart(parts, "failure", failure);
if (failure !== null || isFailureLikeStatus(status)) pushLabeledSummaryPart(parts, "message", message);
pushLabeledSummaryPart(parts, "trace", fields.get("traceId") ?? fields.get("lastTraceId") ?? null, true);
pushLabeledSummaryPart(parts, "session", fields.get("sessionId") ?? fields.get("selectedAgentSessionId") ?? null, true);
pushLabeledSummaryPart(parts, "conversation", fields.get("conversationId") ?? fields.get("selectedConversationId") ?? null, true);
pushLabeledSummaryPart(parts, "thread", fields.get("threadId") ?? null, true);
pushLabeledSummaryPart(parts, "provider", fields.get("providerProfile") ?? fields.get("backendProfile") ?? fields.get("profile") ?? null);
pushLabeledSummaryPart(parts, "route", partialRouteSummary(fields));
pushLabeledSummaryPart(parts, "pipeline", fields.get("pipelineRun") ?? fields.get("pipelineRunName") ?? null, true);
pushLabeledSummaryPart(parts, "run", fields.get("runId") ?? null, true);
pushLabeledSummaryPart(parts, "cmd", fields.get("commandId") ?? null, true);
pushLabeledSummaryPart(parts, "job", fields.get("jobName") ?? fields.get("runnerJobId") ?? null, true);
pushLabeledSummaryPart(parts, "lane", fields.get("lane") ?? null);
pushLabeledSummaryPart(parts, "ns", fields.get("namespace") ?? null);
pushLabeledSummaryPart(parts, "actor", fields.get("username") ?? fields.get("displayName") ?? null);
pushLabeledSummaryPart(parts, "auth", fields.get("authMethod") ?? null);
if (failure === null && !isFailureLikeStatus(status)) pushLabeledSummaryPart(parts, "message", message);
return parts.length > 0 ? parts.slice(0, 10).join("; ") : null;
}
function partialRouteSummary(fields: Map<string, string>): string | null {
const method = fields.get("method") ?? null;
const path = fields.get("path") ?? null;
if (method !== null && path !== null) return `${method} ${path}`;
return fields.get("url") ?? fields.get("baseUrl") ?? null;
}
function jsonLineField(line: string): { key: string; value: string } | null {
const match = line.trim().replace(/,$/u, "").match(/^"([^"]+)":\s*(.+)$/u);
if (match === null) return null;
return { key: match[1], value: cleanJsonScalar(match[2]) };
}
function cleanJsonScalar(raw: string): string {
const trimmed = raw.trim();
if (trimmed === "null" || trimmed === "{" || trimmed === "[" || trimmed === "}" || trimmed === "]") return "";
if (trimmed.startsWith('"') && trimmed.endsWith('"')) {
try {
const parsed = JSON.parse(trimmed) as unknown;
return typeof parsed === "string" ? parsed.trim() : String(parsed);
} catch {
return trimmed.slice(1, -1).trim();
}
}
return trimmed;
}
function cleanCommandOutputLine(line: string): string {
const trimmed = line.replace(/,$/u, "").trim();
const jsonField = trimmed.match(/^"([^"]+)":\s*(.+)$/u);
if (jsonField === null) return trimmed;
const key = jsonField[1];
const value = jsonField[2].replace(/^"|"$/gu, "").trim();
return `${key}=${value}`;
}
function isLowSignalCommandOutputLine(line: string): boolean {
const trimmed = line.replace(/,$/u, "").trim();
if (trimmed === "{" || trimmed === "}" || trimmed === "}," || trimmed === "];" || trimmed === "[") return true;
const field = jsonLineField(trimmed);
return field !== null && isLowSignalJsonField(field.key);
}
function isLowSignalJsonField(key: string): boolean {
return key === "generatedAt" || key === "cli" || key === "version" || key === "valuesRedacted" || key === "secretMaterialStored";
}
function eventText(value: unknown): string | null {
const direct = stringOrNull(value);
if (direct !== null && direct.trim().length > 0) return direct.trim();
const nested = record(value);
return stringOrNull(nested.text)
?? stringOrNull(nested.message)
?? stringOrNull(nested.reason)
?? stringOrNull(nested.summary);
}
function formatCountField(label: string, value: unknown): string | null {
if (typeof value === "number" && Number.isFinite(value)) return `${label}=${value}`;
if (typeof value === "string" && value.length > 0) return `${label}=${value}`;
return null;
}
function renderResourceTable(items: Record<string, unknown>[], wide: boolean): string {
const headers = wide
? ["NAME", "STATE", "QUEUE", "LANE", "PROFILE", "RUN", "CMD", "RJOB", "SESSION", "AGE", "TITLE"]
: ["NAME", "STATE", "QUEUE", "LANE", "RUN", "CMD", "RJOB", "SESSION", "AGE", "ATTENTION"];
const rows = items.map((item) => {
if (wide) {
return [
resourceName(item),
displayValue(item.state),
displayValue(item.queue),
displayValue(item.lane),
displayValue(item.profile),
shortId(stringOrDash(item.run)),
shortId(stringOrDash(item.command)),
shortId(stringOrDash(item.runnerjob)),
shortId(stringOrDash(item.session)),
displayValue(item.age),
truncateOneLine(displayValue(item.title), 48),
];
}
return [
resourceName(item),
displayValue(item.state),
displayValue(item.queue),
displayValue(item.lane),
shortId(stringOrDash(item.run)),
shortId(stringOrDash(item.command)),
shortId(stringOrDash(item.runnerjob)),
shortId(stringOrDash(item.session)),
displayValue(item.age),
displayValue(item.attention),
];
});
return renderTable(headers, rows);
}
function renderTaskDescription(task: Record<string, unknown>, options: AgentRunResourceOptions): string {
const attempt = record(task.latestAttempt);
const taskId = displayValue(task.id);
const runId = stringOrNull(attempt.runId);
const commandId = stringOrNull(attempt.commandId);
const runnerJobId = stringOrNull(attempt.runnerJobId);
const sessionId = stringOrNull(attempt.sessionId) ?? stringOrNull(record(task.sessionRef).sessionId);
const lines = [
`Name: task/${taskId}`,
`State: ${displayValue(task.state)} Attention: ${displayValue(task.unread === true ? "unread" : "")}`,
`Queue: ${displayValue(task.queue)}`,
`Lane: ${displayValue(task.lane)} Profile: ${displayValue(task.backendProfile)} Provider: ${displayValue(task.providerId)}`,
`Title: ${displayValue(task.title)}`,
"",
"Latest Attempt:",
` Attempt: ${displayValue(attempt.attemptId)} State: ${displayValue(attempt.state)}`,
` Run: ${runId === null ? "-" : `run/${runId}`}`,
` Command: ${commandId === null ? "-" : `command/${commandId}`}`,
` Runner: ${runnerJobId === null ? "-" : `runnerjob/${runnerJobId}`}`,
` Session: ${sessionId === null ? "-" : `session/${sessionId}`}`,
"",
"Conditions:",
` Terminal: ${displayValue(task.state)}`,
` Failure: ${displayValue(task.failureKind ?? task.degradedReason ?? "none")}`,
" Values: redacted",
"",
"Next:",
];
const next = [
runId === null ? null : `bun scripts/cli.ts agentrun events run/${runId} --after-seq 0 --limit 100`,
sessionId === null ? null : `bun scripts/cli.ts agentrun logs session/${sessionId} --tail 100`,
runId === null || commandId === null ? null : `bun scripts/cli.ts agentrun result run/${runId} --command ${commandId}`,
`bun scripts/cli.ts agentrun ack task/${taskId}`,
options.full ? null : `bun scripts/cli.ts agentrun describe task/${taskId} --full`,
].filter((item): item is string => item !== null).slice(0, 5);
lines.push(...next.map((item) => ` ${item}`));
return lines.join("\n");
}
function unwrapTaskDetail(data: Record<string, unknown>): Record<string, unknown> {
const task = record(data.task);
if (Object.keys(task).length > 0) return task;
return data;
}
function renderGenericDescription(ref: AgentRunResourceRef, data: unknown): string {
const value = record(data);
const lines = [
`Name: ${ref.kind}/${ref.name}`,
`State: ${displayValue(value.state ?? value.status ?? value.phase ?? value.executionState ?? value.terminalStatus ?? "-")}`,
];
const identity = renderResourceIdentityLines(ref, value);
if (identity.length > 0) lines.push("", ...identity);
const supervisor = renderSupervisorLines(ref, value);
if (supervisor.length > 0) lines.push("", "Supervisor:", ...supervisor.map((line) => ` ${line}`));
const failure = renderFailureLines(value);
if (failure.length > 0) lines.push("", ...failure);
const next = renderResourceNextLines(ref, value);
if (next.length > 0) lines.push("", "Next:", ...next.map((line) => ` ${line}`));
if (identity.length === 0 && failure.length === 0 && next.length === 0) {
lines.push(
"",
"Summary:",
JSON.stringify(pickCompact(value, ["id", "name", "runId", "commandId", "runnerJobId", "sessionId", "state", "status", "phase", "reason", "failureKind", "valuesPrinted"]), null, 2),
);
}
return lines.join("\n");
}
function renderResourceIdentityLines(ref: AgentRunResourceRef, value: Record<string, unknown>): string[] {
const supervisor = record(value.supervisor);
const fields: [string, unknown][] = [
["Run", firstKnownString(value.runId, value.activeRunId, value.lastRunId, supervisor.runId, supervisor.activeRunId, supervisor.lastRunId)],
["Command", firstKnownString(value.commandId, value.activeCommandId, value.lastCommandId, supervisor.commandId, supervisor.activeCommandId, supervisor.lastCommandId)],
["RunnerJob", value.runnerJobId ?? (ref.kind === "runnerjob" ? value.id : null)],
["Session", firstKnownString(value.sessionId, supervisor.sessionId)],
["Namespace", value.namespace],
["Job", value.jobName],
["Image", value.image],
["LogPath", value.logPath],
["Started", value.startedAt],
["Finished", value.finishedAt],
];
if (ref.kind !== "runnerjob") {
return fields
.filter(([label]) => label === "Run" || label === "Command" || label === "RunnerJob" || label === "Session")
.map(([label, item]) => formatResourceField(label, item))
.filter((line): line is string => line !== null);
}
return fields.map(([label, item]) => formatResourceField(label, item)).filter((line): line is string => line !== null);
}
function firstKnownString(...values: unknown[]): string | null {
for (const value of values) {
const text = stringOrNull(value);
if (text !== null && text.length > 0) return text;
}
return null;
}
function formatResourceField(label: string, value: unknown): string | null {
const text = stringOrNull(value);
if (text === null || text.length === 0) return null;
return `${label}: ${text}`;
}
function renderSupervisorLines(ref: AgentRunResourceRef, value: Record<string, unknown>): string[] {
if (ref.kind !== "session" && ref.kind !== "task") return [];
const supervisor = record(value.supervisor);
const liveness = record(value.liveness);
const source = Object.keys(supervisor).length > 0 ? supervisor : liveness;
if (Object.keys(source).length === 0) return [];
const timeoutBudget = record(source.timeoutBudget);
const terminalClassification = record(source.terminalClassification);
const runId = firstKnownString(source.activeRunId, source.runId, source.lastRunId, value.activeRunId, value.runId, value.lastRunId);
const commandId = firstKnownString(source.activeCommandId, source.commandId, source.lastCommandId, value.activeCommandId, value.commandId, value.lastCommandId);
const lines: string[] = [];
const executionState = firstKnownString(source.executionState, value.executionState);
if (executionState !== null) lines.push(`Execution: ${executionState}`);
if (typeof source.active === "boolean") lines.push(`Active: ${String(source.active)}`);
if (runId !== null) lines.push(`Run: run/${runId}`);
if (commandId !== null) lines.push(`Command: command/${commandId}`);
const phase = firstKnownString(source.phase, liveness.phase);
const lastEventAgeMs = typeof source.lastEventAgeMs === "number" ? source.lastEventAgeMs : typeof liveness.lastEventAgeMs === "number" ? liveness.lastEventAgeMs : null;
if (phase !== null || lastEventAgeMs !== null) lines.push(`Liveness: ${displayValue(phase)}${lastEventAgeMs === null ? "" : ` ageMs=${String(lastEventAgeMs)}`}`);
const timeoutKind = firstKnownString(timeoutBudget.timeoutKind);
const timeoutState = firstKnownString(timeoutBudget.state);
const remainingMs = typeof timeoutBudget.remainingMs === "number" ? timeoutBudget.remainingMs : null;
if (timeoutKind !== null || timeoutState !== null || remainingMs !== null) lines.push(`Timeout: ${displayValue(timeoutKind)} / ${displayValue(timeoutState)}${remainingMs === null ? "" : ` remainingMs=${String(remainingMs)}`}`);
const category = firstKnownString(terminalClassification.category);
const reason = firstKnownString(terminalClassification.reason);
if (category !== null || reason !== null) lines.push(`Classification: ${displayValue(category)}${reason === null ? "" : `; ${reason}`}`);
return lines;
}
function renderFailureLines(value: Record<string, unknown>): string[] {
const terminalClassification = record(value.terminalClassification);
const nestedTerminalClassification = Object.keys(terminalClassification).length > 0
? terminalClassification
: record(record(value.liveness).terminalClassification);
const failureKind = stringOrNull(value.failureKind)
?? stringOrNull(nestedTerminalClassification.failureKind)
?? stringOrNull(value.degradedReason);
const failureMessage = stringOrNull(value.failureMessage)
?? stringOrNull(nestedTerminalClassification.failureMessage)
?? stringOrNull(value.message)
?? stringOrNull(value.reason);
const category = stringOrNull(nestedTerminalClassification.category);
const timeoutKind = stringOrNull(nestedTerminalClassification.timeoutKind);
const timeoutState = stringOrNull(nestedTerminalClassification.timeoutState);
const providerInterruption = stringOrNull(nestedTerminalClassification.providerInterruption);
const lastActivity = stringOrNull(nestedTerminalClassification.lastActivityKind);
const lastActivitySeq = nestedTerminalClassification.lastActivitySeq;
const lines: string[] = [];
if (failureKind !== null) lines.push(`Failure: ${failureKind}`);
if (failureMessage !== null) lines.push(`Message: ${failureMessage}`);
if (category !== null) lines.push(`Category: ${category}`);
if (timeoutKind !== null || timeoutState !== null) lines.push(`Timeout: ${displayValue(timeoutKind)} / ${displayValue(timeoutState)}`);
if (providerInterruption !== null) lines.push(`Provider: ${providerInterruption}`);
if (lastActivity !== null) lines.push(`LastActivity: ${lastActivity}${lastActivitySeq === undefined ? "" : ` seq=${String(lastActivitySeq)}`}`);
return lines;
}
function renderResourceNextLines(ref: AgentRunResourceRef, value: Record<string, unknown>): string[] {
const supervisor = record(value.supervisor);
const runId = firstKnownString(value.runId, value.activeRunId, value.lastRunId, supervisor.runId, supervisor.activeRunId, supervisor.lastRunId);
const commandId = firstKnownString(value.commandId, value.activeCommandId, value.lastCommandId, supervisor.commandId, supervisor.activeCommandId, supervisor.lastCommandId);
const runnerJobId = stringOrNull(value.runnerJobId) ?? (ref.kind === "runnerjob" ? ref.name : null);
const sessionId = firstKnownString(value.sessionId, supervisor.sessionId);
const lines = [
runId === null ? null : `bun scripts/cli.ts agentrun events run/${runId} --after-seq 0 --limit 100`,
sessionId === null ? null : `bun scripts/cli.ts agentrun logs session/${sessionId} --tail 100`,
runId === null || commandId === null ? null : `bun scripts/cli.ts agentrun result run/${runId} --command ${commandId}`,
runId === null || commandId === null ? null : `bun scripts/cli.ts agentrun describe command/${commandId} --run ${runId} --full`,
runId === null || runnerJobId === null ? null : `bun scripts/cli.ts agentrun describe runnerjob/${runnerJobId} --run ${runId} --full`,
ref.kind !== "session" || sessionId === null ? null : `bun scripts/cli.ts agentrun send session/${sessionId} --prompt-stdin`,
];
const unique: string[] = [];
for (const line of lines) {
if (line !== null && !unique.includes(line)) unique.push(line);
}
return unique.slice(0, 5);
}
function parseTaskManifest(raw: string, source: string): Record<string, unknown> {
const parsed = source.endsWith(".json") ? JSON.parse(raw) as unknown : Bun.YAML.parse(raw) as unknown;
const input = record(parsed);
const kind = stringOrNull(input.kind);
if (kind !== null && kind.toLowerCase() !== "task") throw new Error("apply currently supports kind: Task only");
const spec = record(input.spec);
const payload = Object.keys(spec).length > 0 ? spec : input;
if (!isRecord(payload.payload) && !isRecord(payload.executionPolicy) && stringOrNull(payload.title) === null) {
throw new Error("task manifest must contain spec with an AgentRun task payload");
}
return payload;
}
function nextPagedResourceCommand(command: string, nextAfterSeq: string, limit: number): string {
const parts = command
.replace(/\s+--after-seq\s+\S+/gu, "")
.replace(/\s+--tail(?:\s+\S+)?/gu, "")
.replace(/\s+--limit\s+\S+/gu, "")
.trim();
return `${parts} --after-seq ${nextAfterSeq} --limit ${limit}`;
}
function agentRunExplain(kindRaw: string): string {
if (kindRaw === "session-policy" || kindRaw === "provider-policy") return renderAgentRunSessionPolicyExplanation();
const kind = parseResourceKind(kindRaw);
if (kind === "task") {
return [
"KIND: task",
"FIELDS:",
" metadata.name: optional client name/idempotency hint",
" spec.tenantId, spec.projectId, spec.queue, spec.title",
" spec.payload.prompt or spec.payload.<structured fields>",
" spec.executionPolicy, spec.resourceBundleRef, spec.workspaceRef",
"",
"Apply example:",
" kind: Task",
" spec:",
" tenantId: unidesk",
" projectId: pikasTech/unidesk",
" queue: commander",
" title: Example",
" payload:",
" prompt: Do the work",
].join("\n");
}
return `KIND: ${kindRaw}\nUse get/describe with -o json to inspect the current stable UniDesk resource schema.`;
}
function arrayRecords(value: unknown): Record<string, unknown>[] {
if (Array.isArray(value)) return value.map(record).filter((item) => Object.keys(item).length > 0);
if (typeof value === "object" && value !== null && Array.isArray((value as { items?: unknown }).items)) return arrayRecords((value as { items: unknown }).items);
return [];
}
function renderTable(headers: string[], rows: string[][]): string {
const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => (row[index] ?? "").length)));
const line = (values: string[]): string => values.map((value, index) => value.padEnd(widths[index] ?? value.length)).join(" ").trimEnd();
return [line(headers), ...rows.map(line)].join("\n");
}
function resourceName(item: Record<string, unknown>): string {
const kind = String(item.kind ?? "resource");
return `${kind}/${shortId(displayValue(item.name))}`;
}
function shortId(value: string): string {
if (value === "-" || value.length <= 18) return value;
const known = ["qt_", "run_", "cmd_", "rjob_"];
const prefix = known.find((item) => value.startsWith(item));
if (prefix !== undefined) return `${prefix}${value.slice(prefix.length, prefix.length + 8)}...`;
if (/^\w+-\d+/u.test(value)) return value.length > 28 ? `${value.slice(0, 25)}...` : value;
return `${value.slice(0, 14)}...`;
}
function displayValue(value: unknown): string {
if (value === undefined || value === null || value === "") return "-";
return String(value);
}
function stringOrDash(value: unknown): string {
return displayValue(value);
}
function truncateOneLine(value: string, maxChars: number): string {
const oneLine = value.replace(/\s+/gu, " ").trim();
return oneLine.length <= maxChars ? oneLine : `${oneLine.slice(0, Math.max(0, maxChars - 3))}...`;
}
function truncateMultiline(value: string, maxChars: number): string {
return value.length <= maxChars ? value : `${value.slice(0, maxChars)}\n...[truncated ${value.length - maxChars} chars; rerun with --full-text or --full]`;
}
function relativeAge(iso: string | null): string {
if (iso === null) return "-";
const ms = Date.now() - Date.parse(iso);
if (!Number.isFinite(ms) || ms < 0) return "-";
const minutes = Math.floor(ms / 60000);
if (minutes < 60) return `${minutes}m`;
const hours = Math.floor(minutes / 60);
if (hours < 48) return `${hours}h`;
return `${Math.floor(hours / 24)}d`;
}
function pickCompact(value: Record<string, unknown>, keys: string[]): Record<string, unknown> {
const picked: Record<string, unknown> = {};
for (const key of keys) {
if (value[key] !== undefined) picked[key] = value[key];
}
return picked;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
interface TriggerOptions {
confirm: boolean;
dryRun: boolean;
node: string | null;
lane: string | null;
wait: boolean;
}
interface ConfirmOptions {
confirm: boolean;
dryRun: boolean;
}
interface SecretSyncOptions extends ConfirmOptions {
node: string | null;
lane: string | null;
}
interface LaneConfirmOptions extends ConfirmOptions {
node: string | null;
lane: string | null;
}
interface RefreshOptions extends ConfirmOptions {
node: string | null;
lane: string | null;
}
interface GitMirrorStatusOptions extends DisclosureOptions {
node: string | null;
lane: string | null;
}
interface GitMirrorOptions extends ConfirmOptions {
node: string | null;
lane: string | null;
timeoutSeconds: number;
wait: boolean;
}
interface CleanupRunnersOptions extends ConfirmOptions {
node: string | null;
lane: string | null;
timeoutSeconds: number;
forceActive: boolean;
}
interface CleanupRunsOptions extends ConfirmOptions {
node: string | null;
lane: string | null;
minAgeMinutes: number;
limit: number;
timeoutSeconds: number;
}
interface CleanupReleasedPvOptions extends ConfirmOptions {
node: string | null;
lane: string | null;
limit: number;
timeoutSeconds: number;
}
interface DisclosureOptions {
full: boolean;
raw: boolean;
}
interface StatusOptions extends DisclosureOptions {
node: string | null;
lane: string | null;
sourceCommit: string | null;
pipelineRun: string | null;
targetMode: "latest-source-head" | "source-commit" | "pipeline-run";
}
interface TimedValue<T> {
value: T;
elapsedMs: number;
}
function parseDisclosureOptions(args: string[]): DisclosureOptions {
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === "--node" || arg === "--lane") {
const value = args[index + 1];
if (value === undefined || value.startsWith("--")) throw new Error(`${arg} requires a value`);
index += 1;
continue;
}
if (arg !== "--full" && arg !== "--raw") throw new Error(`unsupported status option: ${arg}`);
}
const raw = args.includes("--raw");
return { full: raw || args.includes("--full"), raw };
}
function parseGitMirrorStatusOptions(args: string[]): GitMirrorStatusOptions {
validateOptions(args, new Set(["--full", "--raw"]), new Set(["--node", "--lane"]));
const raw = args.includes("--raw");
return {
full: raw || args.includes("--full"),
raw,
node: optionValue(args, "--node") ?? null,
lane: optionValue(args, "--lane") ?? null,
};
}
function parseStatusOptions(args: string[]): StatusOptions {
let node: string | null = null;
let lane: string | null = null;
let sourceCommit: string | null = null;
let pipelineRun: string | null = null;
let full = false;
let raw = false;
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === "--full") {
full = true;
continue;
}
if (arg === "--raw") {
raw = true;
full = true;
continue;
}
if (arg === "--node") {
const value = args[index + 1];
if (value === undefined || value.startsWith("--")) throw new Error("--node requires a value");
node = value;
index += 1;
continue;
}
if (arg === "--lane") {
const value = args[index + 1];
if (value === undefined || value.startsWith("--")) throw new Error("--lane requires a value");
lane = value;
index += 1;
continue;
}
if (arg === "--source-commit") {
const value = args[index + 1];
if (value === undefined || value.startsWith("--")) throw new Error("--source-commit requires a value");
if (!isGitSha(value)) throw new Error("--source-commit must be a full 40-character git SHA");
sourceCommit = value;
index += 1;
continue;
}
if (arg === "--pipeline-run") {
const value = args[index + 1];
if (value === undefined || value.startsWith("--")) throw new Error("--pipeline-run requires a value");
if (!isAgentRunPipelineRunName(value)) throw new Error("--pipeline-run must be an agentrun-vNN-ci-<12+ hex> PipelineRun name");
pipelineRun = value;
index += 1;
continue;
}
throw new Error(`unsupported status option: ${arg}`);
}
if (sourceCommit !== null && pipelineRun !== null) throw new Error("control-plane status accepts only one of --source-commit or --pipeline-run");
return {
full,
raw,
node,
lane,
sourceCommit,
pipelineRun,
targetMode: pipelineRun !== null ? "pipeline-run" : sourceCommit !== null ? "source-commit" : "latest-source-head",
};
}
function parseTriggerOptions(args: string[]): TriggerOptions {
const base = parseConfirmOptions(args);
let node: string | null = null;
let lane: string | null = null;
let wait = false;
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === "--confirm" || arg === "--dry-run") continue;
if (arg === "--wait") {
wait = true;
continue;
}
if (arg === "--node") {
const value = args[index + 1];
if (value === undefined || value.startsWith("--")) throw new Error("--node requires a value");
node = value;
index += 1;
continue;
}
if (arg === "--lane") {
const value = args[index + 1];
if (value === undefined || value.startsWith("--")) throw new Error("--lane requires a value");
lane = value;
index += 1;
continue;
}
throw new Error(`unsupported trigger-current option: ${arg}`);
}
return { ...base, node, lane, wait };
}
function parseSecretSyncOptions(args: string[]): SecretSyncOptions {
const base = parseConfirmOptions(args);
let node: string | null = null;
let lane: string | null = null;
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === "--confirm" || arg === "--dry-run") continue;
if (arg === "--node") {
const value = args[index + 1];
if (value === undefined || value.startsWith("--")) throw new Error("--node requires a value");
node = value;
index += 1;
continue;
}
if (arg === "--lane") {
const value = args[index + 1];
if (value === undefined || value.startsWith("--")) throw new Error("--lane requires a value");
lane = value;
index += 1;
continue;
}
throw new Error(`unsupported secret-sync option: ${arg}`);
}
return { ...base, node, lane };
}
function parseLaneConfirmOptions(args: string[]): LaneConfirmOptions {
const base = parseConfirmOptions(args);
let node: string | null = null;
let lane: string | null = null;
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === "--confirm" || arg === "--dry-run") continue;
if (arg === "--node") {
const value = args[index + 1];
if (value === undefined || value.startsWith("--")) throw new Error("--node requires a value");
node = value;
index += 1;
continue;
}
if (arg === "--lane") {
const value = args[index + 1];
if (value === undefined || value.startsWith("--")) throw new Error("--lane requires a value");
lane = value;
index += 1;
continue;
}
throw new Error(`unsupported control-plane option: ${arg}`);
}
if (node === null && lane === null) throw new Error("control-plane apply requires --node and --lane");
return { ...base, node, lane };
}
function parseRefreshOptions(args: string[]): RefreshOptions {
const base = parseConfirmOptions(args);
let node: string | null = null;
let lane: string | null = null;
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === "--confirm" || arg === "--dry-run") continue;
if (arg === "--node") {
const value = args[index + 1];
if (value === undefined || value.startsWith("--")) throw new Error("--node requires a value");
node = value;
index += 1;
continue;
}
if (arg === "--lane") {
const value = args[index + 1];
if (value === undefined || value.startsWith("--")) throw new Error("--lane requires a value");
lane = value;
index += 1;
continue;
}
throw new Error(`unsupported refresh option: ${arg}`);
}
return { ...base, node, lane };
}
function parseConfirmOptions(args: string[]): ConfirmOptions {
if (args.includes("--confirm") && args.includes("--dry-run")) throw new Error("accepts only one of --confirm or --dry-run");
return {
confirm: args.includes("--confirm"),
dryRun: args.includes("--dry-run") || !args.includes("--confirm"),
};
}
function parseGitMirrorOptions(args: string[]): GitMirrorOptions {
validateOptions(args, new Set(["--confirm", "--dry-run", "--wait"]), new Set(["--timeout-seconds", "--node", "--lane"]));
const base = parseConfirmOptions(args);
const timeoutIndex = args.indexOf("--timeout-seconds");
const timeoutSeconds = timeoutIndex >= 0 ? Number(args[timeoutIndex + 1]) : 300;
if (!Number.isFinite(timeoutSeconds) || timeoutSeconds < 30) throw new Error("--timeout-seconds must be a number >= 30");
return {
...base,
node: optionValue(args, "--node") ?? null,
lane: optionValue(args, "--lane") ?? null,
timeoutSeconds,
wait: args.includes("--wait"),
};
}
function parseCleanupRunnersOptions(args: string[]): CleanupRunnersOptions {
validateOptions(args, new Set(["--confirm", "--dry-run", "--force-active"]), new Set(["--timeout-seconds", "--node", "--lane"]));
const base = parseConfirmOptions(args);
return {
...base,
node: optionValue(args, "--node") ?? null,
lane: optionValue(args, "--lane") ?? null,
timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", 180, 600),
forceActive: args.includes("--force-active"),
};
}
function parseCleanupRunsOptions(args: string[]): CleanupRunsOptions {
validateOptions(args, new Set(["--confirm", "--dry-run"]), new Set(["--min-age-minutes", "--limit", "--timeout-seconds", "--node", "--lane"]));
const base = parseConfirmOptions(args);
return {
...base,
node: optionValue(args, "--node") ?? null,
lane: optionValue(args, "--lane") ?? null,
minAgeMinutes: positiveIntegerOption(args, "--min-age-minutes", 60, 10080),
limit: positiveIntegerOption(args, "--limit", 20, 500),
timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", 180, 600),
};
}
function parseCleanupReleasedPvOptions(args: string[]): CleanupReleasedPvOptions {
validateOptions(args, new Set(["--confirm", "--dry-run"]), new Set(["--limit", "--timeout-seconds", "--node", "--lane"]));
const base = parseConfirmOptions(args);
return {
...base,
node: optionValue(args, "--node") ?? null,
lane: optionValue(args, "--lane") ?? null,
limit: positiveIntegerOption(args, "--limit", 20, 500),
timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", 120, 600),
};
}
function validateOptions(args: string[], booleanOptions: Set<string>, valueOptions: Set<string>): void {
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (booleanOptions.has(arg)) continue;
if (valueOptions.has(arg)) {
const value = args[index + 1];
if (value === undefined || value.startsWith("--")) throw new Error(`${arg} requires a value`);
index += 1;
continue;
}
throw new Error(`unsupported option: ${arg}`);
}
}
function optionValue(args: string[], name: string): string | undefined {
const index = args.indexOf(name);
if (index === -1) return undefined;
const value = args[index + 1];
if (value === undefined || value.startsWith("--")) throw new Error(`${name} requires a value`);
return value;
}
function positiveIntegerOption(args: string[], name: string, defaultValue: number, maxValue: number): number {
const raw = optionValue(args, name);
if (raw === undefined) return defaultValue;
const value = Number(raw);
if (!Number.isInteger(value) || value < 0) throw new Error(`${name} must be a non-negative integer`);
return Math.min(value, maxValue);
}
async function controlPlanePlan(_config: UniDeskConfig, options: StatusOptions): Promise<Record<string, unknown>> {
const target = resolveAgentRunLaneTarget(options);
const spec = target.spec;
return {
ok: true,
command: "agentrun control-plane plan",
mode: "yaml-declared-node-lane",
configPath: target.configPath,
target: agentRunLaneSummary(spec),
plannedChecks: [
"source-branch-exists",
"source-worktree-exists-and-clean",
"git-mirror-services-ready",
"ci-namespace-pipeline-serviceaccount",
"argo-application-alignment",
"runtime-namespace-manager-service",
"database-secretref-present",
"local-postgres-absent-when-external",
],
deploymentBoundary: {
mutation: false,
note: "plan/status are read-only. Long writes must use controlled AgentRun/Platform DB commands and this YAML target.",
},
next: {
status: `bun scripts/cli.ts agentrun control-plane status --node ${spec.nodeId} --lane ${spec.lane}`,
postgresStatus: spec.database.configRef ? `bun scripts/cli.ts platform-db postgres status --config ${spec.database.configRef}` : null,
postgresApply: spec.database.configRef ? `bun scripts/cli.ts platform-db postgres apply --config ${spec.database.configRef} --confirm` : null,
controlPlaneApply: `bun scripts/cli.ts agentrun control-plane apply --node ${spec.nodeId} --lane ${spec.lane} --dry-run`,
},
valuesPrinted: false,
};
}
async function controlPlaneApply(config: UniDeskConfig, options: LaneConfirmOptions): Promise<Record<string, unknown>> {
const { configPath, spec } = resolveAgentRunLaneTarget(options);
const objects = renderAgentRunControlPlaneManifests(spec);
const manifestYaml = `${objects.map((object) => Bun.YAML.stringify(object).trim()).join("\n---\n")}\n`;
const objectRefs = objects.map((object) => manifestObjectRef(object));
const plan = {
node: spec.nodeId,
kubeRoute: spec.nodeKubeRoute,
lane: spec.lane,
version: spec.version,
objectCount: objects.length,
objects: objectRefs,
manifestBytes: Buffer.byteLength(manifestYaml, "utf8"),
manifestDigest: renderedObjectsDigest(objects),
fieldManager: "unidesk-agentrun-control-plane",
valuesPrinted: false,
};
if (options.dryRun || !options.confirm) {
return {
ok: true,
command: "agentrun control-plane apply",
mode: "dry-run",
mutation: false,
configPath,
target: agentRunLaneSummary(spec),
plan,
next: {
confirm: `bun scripts/cli.ts agentrun control-plane apply --node ${spec.nodeId} --lane ${spec.lane} --confirm`,
status: `bun scripts/cli.ts agentrun control-plane status --node ${spec.nodeId} --lane ${spec.lane}`,
},
valuesPrinted: false,
};
}
const applied = await capture(config, spec.nodeKubeRoute, ["sh", "--", applyYamlScript(manifestYaml, "unidesk-agentrun-control-plane", false)]);
const payload = captureJsonPayload(applied);
return {
ok: applied.exitCode === 0 && payload.ok !== false,
command: "agentrun control-plane apply",
mode: "confirmed-apply",
mutation: true,
configPath,
target: agentRunLaneSummary(spec),
plan,
result: payload,
capture: compactCapture(applied, { full: applied.exitCode !== 0, stdoutTailChars: 5000, stderrTailChars: 5000 }),
next: {
secretSync: `bun scripts/cli.ts agentrun control-plane secret-sync --node ${spec.nodeId} --lane ${spec.lane} --confirm`,
status: `bun scripts/cli.ts agentrun control-plane status --node ${spec.nodeId} --lane ${spec.lane}`,
},
valuesPrinted: false,
};
}
async function status(config: UniDeskConfig, options: StatusOptions): Promise<Record<string, unknown>> {
return await statusYamlLane(config, options, resolveAgentRunLaneTarget(options));
}
async function statusYamlLane(config: UniDeskConfig, options: StatusOptions, target: { configPath: string; spec: AgentRunLaneSpec }): Promise<Record<string, unknown>> {
const spec = target.spec;
const sourceProbe = await timedStatusStage("source", () => capture(config, `${spec.nodeRoute}:${spec.source.workspace}`, ["sh", "--", yamlLaneSourceStatusScript(spec)]));
const sourcePayload = captureJsonPayload(sourceProbe.value);
const branchTipCommit = stringOrNull(sourcePayload.remoteBranchCommit) ?? stringOrNull(sourcePayload.localHead);
const initialSourceCommit = options.sourceCommit
?? stringOrNull(sourcePayload.remoteBranchCommit)
?? stringOrNull(sourcePayload.localHead);
const pipelineRunName = options.pipelineRun ?? (initialSourceCommit ? agentRunPipelineRunName(spec, initialSourceCommit) : null);
const [runtimeProbe, mirrorProbe] = await Promise.all([
timedStatusStage("runtime", () => capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneRuntimeStatusScript(spec, pipelineRunName)])),
timedStatusStage("git-mirror", () => capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneGitMirrorStatusScript(spec)])),
]);
const runtimePayload = captureJsonPayload(runtimeProbe.value);
const mirrorPayload = captureJsonPayload(mirrorProbe.value);
const pipeline = record(runtimePayload.pipeline);
const pipelineRunStatus = record(runtimePayload.pipelineRun);
const argo = record(runtimePayload.argo);
const manager = record(runtimePayload.manager);
const database = record(runtimePayload.database);
const secrets = record(runtimePayload.secrets);
const localPostgres = record(runtimePayload.localPostgres);
const expectedGitopsRevision = stringOrNull(mirrorPayload.gitopsCommit);
const mirrorSourceCommit = stringOrNull(mirrorPayload.sourceCommit);
const pipelineRunSourceCommit = stringOrNull(pipelineRunStatus.sourceCommit);
const sourceCommit = options.sourceCommit
?? (options.pipelineRun !== null ? pipelineRunSourceCommit ?? initialSourceCommit : initialSourceCommit);
const managerSourceMatchesExpected = Boolean(sourceCommit && manager.sourceCommit === sourceCommit);
const argoSyncedToGitops = Boolean(expectedGitopsRevision && argo.revision === expectedGitopsRevision);
const pipelineSucceeded = pipelineRunStatus.status === "True";
const sourceWorktreeDetached = sourcePayload.branch === "HEAD";
const targetMode = options.pipelineRun !== null ? "pipeline-run" : options.sourceCommit !== null ? "source-commit" : "latest-source-head";
const sourceBranchAdvanced = targetMode !== "latest-source-head"
&& sourceCommit !== null
&& branchTipCommit !== null
&& sourceCommit !== branchTipCommit;
const branchDrift = {
targetMode,
sourceBranch: spec.source.branch,
currentBranchTipCommit: branchTipCommit,
targetSourceCommit: sourceCommit,
pipelineRunSourceCommit,
sourceBranchAdvanced,
targetSupersededByCurrentBranch: sourceBranchAdvanced,
closeoutHint: sourceBranchAdvanced
? "target PipelineRun/source commit is not the current branch tip; verify the branch tip contains the fix, then trigger-current again for latest-tip closeout"
: null,
valuesPrinted: false,
};
const warnings = [
...(sourceWorktreeDetached ? ["source-worktree-detached"] : []),
...(sourceBranchAdvanced ? ["source-branch-advanced-after-target"] : []),
];
const blockers = [
...(sourcePayload.workspaceExists === true ? [] : ["source-worktree-missing"]),
...(sourcePayload.remoteBranchExists === true ? [] : ["source-branch-missing"]),
...(sourcePayload.workspaceClean === true || sourcePayload.workspaceExists !== true ? [] : ["source-worktree-dirty"]),
...(mirrorPayload.readReady === true ? [] : ["git-mirror-read-not-ready"]),
...(mirrorPayload.writeReady === true ? [] : ["git-mirror-write-not-ready"]),
...(mirrorPayload.cachePvcExists === true ? [] : ["git-mirror-cache-pvc-missing"]),
...(runtimePayload.ciNamespaceExists === true ? [] : ["ci-namespace-missing"]),
...(pipeline.exists === true ? [] : ["pipeline-missing"]),
...(pipelineRunName !== null && pipelineRunStatus.exists !== true ? ["pipelinerun-missing"] : []),
...(pipelineRunStatus.exists !== true || pipelineRunStatus.status === undefined || pipelineRunStatus.status === null || pipelineRunStatus.status === "True" ? [] : ["pipelinerun-not-succeeded"]),
...(runtimePayload.serviceAccountExists === true ? [] : ["ci-serviceaccount-missing"]),
...(argo.exists === true ? [] : ["argo-application-missing"]),
...(mirrorPayload.readReady === true && expectedGitopsRevision === null ? ["gitops-revision-unresolved"] : []),
...(argo.exists !== true || expectedGitopsRevision === null || argoSyncedToGitops ? [] : ["argo-revision-stale"]),
...(runtimePayload.runtimeNamespaceExists === true ? [] : ["runtime-namespace-missing"]),
...(manager.deploymentExists === true ? [] : ["manager-deployment-missing"]),
...(manager.deploymentExists !== true || sourceCommit === null || managerSourceMatchesExpected ? [] : ["manager-source-stale"]),
...(manager.serviceExists === true ? [] : ["manager-service-missing"]),
...(spec.database.mode === "external-postgres" && database.secretPresent !== true ? ["database-secret-missing"] : []),
...(secrets.ready === true ? [] : ["lane-secret-missing"]),
...(spec.database.localPostgresExpectedAbsent && localPostgres.absent !== true ? ["local-postgres-present"] : []),
];
const ciBlockerNames = new Set(["ci-namespace-missing", "pipeline-missing", "pipelinerun-missing", "pipelinerun-not-succeeded", "ci-serviceaccount-missing"]);
const ciBlockers = blockers.filter((blocker) => ciBlockerNames.has(blocker));
const runtimeBlockers = blockers.filter((blocker) => !ciBlockerNames.has(blocker));
const aligned = blockers.length === 0 && pipelineSucceeded && argoSyncedToGitops && managerSourceMatchesExpected;
const runtimeAligned = runtimeBlockers.length === 0 && argoSyncedToGitops && managerSourceMatchesExpected;
const ciEvidenceMissing = pipelineRunName !== null && pipelineRunStatus.exists !== true;
const mirrorAlreadySynced = mirrorPayload.readReady === true
&& mirrorPayload.writeReady === true
&& sourceCommit !== null
&& mirrorSourceCommit === sourceCommit
&& expectedGitopsRevision !== null;
const runtimeAlignment = {
pipelineSucceeded,
argoRevision: stringOrNull(argo.revision),
argoSyncedToGitops,
managerSourceCommit: stringOrNull(manager.sourceCommit),
managerSourceMatchesExpected,
runtimeAligned,
};
const statusFullCommand = agentRunControlPlaneStatusCommand(spec, options, true);
const triggerCurrentCommand = `bun scripts/cli.ts agentrun control-plane trigger-current --node ${spec.nodeId} --lane ${spec.lane} --confirm`;
const nextAction = aligned
? { code: "none", summary: "lane aligned; no action required", command: null }
: ciEvidenceMissing && runtimeAligned && mirrorAlreadySynced
? { code: "ci-evidence-missing", summary: "runtime and git mirror are aligned, but the expected PipelineRun evidence is missing; rerun trigger-current to recreate CI evidence", command: triggerCurrentCommand }
: sourceBranchAdvanced
? { code: "target-superseded", summary: "target commit is no longer the branch tip; trigger-current against latest source before closeout", command: triggerCurrentCommand }
: ciBlockers.length > 0
? { code: "ci-blocked", summary: `CI evidence is blocked: ${ciBlockers.join(", ")}`, command: statusFullCommand }
: runtimeBlockers.length > 0
? { code: "runtime-blocked", summary: `runtime alignment is blocked: ${runtimeBlockers.join(", ")}`, command: statusFullCommand }
: { code: "inspect-full-status", summary: "alignment is inconclusive; inspect full status details", command: statusFullCommand };
const compactSourceStatus = {
workspaceExists: sourcePayload.workspaceExists ?? false,
workspaceClean: sourcePayload.workspaceClean ?? null,
branch: sourcePayload.branch ?? null,
remoteBranchExists: sourcePayload.remoteBranchExists ?? false,
remoteBranchCommit: sourcePayload.remoteBranchCommit ?? null,
workspaceDetached: sourceWorktreeDetached,
};
const compactGitMirrorStatus = {
readReady: mirrorPayload.readReady ?? false,
writeReady: mirrorPayload.writeReady ?? false,
cachePvcExists: mirrorPayload.cachePvcExists ?? false,
sourceCommit: mirrorSourceCommit,
gitopsCommit: expectedGitopsRevision,
alreadySynced: mirrorAlreadySynced,
};
const compactCiStatus = {
namespaceExists: runtimePayload.ciNamespaceExists ?? false,
serviceAccountExists: runtimePayload.serviceAccountExists ?? false,
pipelineExists: pipeline.exists ?? false,
pipelineRun: {
name: pipelineRunName,
exists: pipelineRunStatus.exists ?? false,
status: pipelineRunStatus.status ?? null,
reason: pipelineRunStatus.reason ?? null,
sourceCommit: pipelineRunSourceCommit,
},
evidenceMissing: ciEvidenceMissing,
blockers: ciBlockers,
};
const compactArgoStatus = {
exists: argo.exists ?? false,
application: argo.application ?? null,
revision: stringOrNull(argo.revision),
syncStatus: argo.syncStatus ?? null,
healthStatus: argo.healthStatus ?? null,
syncedToGitops: argoSyncedToGitops,
};
const compactRuntimeStatus = {
namespaceExists: runtimePayload.runtimeNamespaceExists ?? false,
manager: {
deploymentExists: manager.deploymentExists ?? false,
serviceExists: manager.serviceExists ?? false,
sourceCommit: stringOrNull(manager.sourceCommit),
sourceMatchesExpected: managerSourceMatchesExpected,
},
databaseSecretPresent: database.secretPresent ?? null,
secretsReady: secrets.ready ?? null,
localPostgresAbsent: localPostgres.absent ?? null,
blockers: runtimeBlockers,
};
const detailedSummary = {
aligned,
runtimeAligned,
ciEvidenceMissing,
mirrorAlreadySynced,
blockers,
warnings,
sourceCommit,
expectedPipelineRun: pipelineRunName,
expectedGitopsRevision,
runtimeAlignment,
branchDrift,
source: {
workspaceExists: sourcePayload.workspaceExists ?? false,
workspaceClean: sourcePayload.workspaceClean ?? null,
branch: sourcePayload.branch ?? null,
workspaceDetached: sourceWorktreeDetached,
localHead: sourcePayload.localHead ?? null,
remoteBranchExists: sourcePayload.remoteBranchExists ?? false,
remoteBranchCommit: sourcePayload.remoteBranchCommit ?? null,
},
gitMirror: {
readReady: mirrorPayload.readReady ?? false,
writeReady: mirrorPayload.writeReady ?? false,
cachePvcExists: mirrorPayload.cachePvcExists ?? false,
repositoryCount: Array.isArray(mirrorPayload.repositories) ? mirrorPayload.repositories.length : null,
sourceCommit: mirrorSourceCommit,
gitopsCommit: expectedGitopsRevision,
alreadySynced: mirrorAlreadySynced,
},
ci: {
namespaceExists: runtimePayload.ciNamespaceExists ?? false,
serviceAccountExists: runtimePayload.serviceAccountExists ?? false,
pipeline,
pipelineRun: pipelineRunStatus,
evidenceMissing: ciEvidenceMissing,
blockers: ciBlockers,
},
argo,
runtime: {
namespaceExists: runtimePayload.runtimeNamespaceExists ?? false,
manager: {
deploymentExists: manager.deploymentExists ?? false,
serviceExists: manager.serviceExists ?? false,
deployment: manager.deployment ?? null,
service: manager.service ?? null,
image: manager.image ?? null,
sourceCommit: stringOrNull(manager.sourceCommit),
sourceMatchesExpected: managerSourceMatchesExpected,
},
database,
secrets: compactLaneSecretsStatus(secrets),
localPostgres,
},
nextAction,
};
const commanderSummary = {
aligned,
runtimeAligned,
ciEvidenceMissing,
mirrorAlreadySynced,
blockers,
warnings,
sourceCommit,
expectedPipelineRun: pipelineRunName,
expectedGitopsRevision,
runtimeAlignment,
branchDrift,
source: compactSourceStatus,
gitMirror: compactGitMirrorStatus,
ci: compactCiStatus,
argo: compactArgoStatus,
runtime: compactRuntimeStatus,
nextAction,
};
const result: Record<string, unknown> = {
ok: sourceProbe.value.exitCode === 0 && runtimeProbe.value.exitCode === 0 && mirrorProbe.value.exitCode === 0 && blockers.length === 0,
command: "agentrun control-plane status",
mode: "yaml-declared-node-lane",
configPath: target.configPath,
target: options.full || options.raw ? agentRunLaneSummary(spec) : compactAgentRunLaneStatusTarget(spec),
summary: options.full || options.raw ? detailedSummary : commanderSummary,
alignment: {
aligned,
runtimeAligned,
ciEvidenceMissing,
mirrorAlreadySynced,
blockers,
warnings,
sourceCommit,
expectedPipelineRun: pipelineRunName,
expectedGitopsRevision,
runtimeAlignment,
branchDrift,
},
timings: {
sourceMs: sourceProbe.elapsedMs,
runtimeMs: runtimeProbe.elapsedMs,
gitMirrorMs: mirrorProbe.elapsedMs,
totalMs: sourceProbe.elapsedMs + Math.max(runtimeProbe.elapsedMs, mirrorProbe.elapsedMs),
},
disclosure: {
output: options.full || options.raw ? "full" : "compact-summary",
full: options.full,
raw: options.raw,
omittedWhenCompact: options.full || options.raw ? [] : ["full target YAML summary", "detailed source/runtime/gitMirror objects", "capture stdout/stderr tails for successful probes"],
fullCommand: statusFullCommand,
},
next: {
action: nextAction,
plan: `bun scripts/cli.ts agentrun control-plane plan --node ${spec.nodeId} --lane ${spec.lane}`,
postgresStatus: spec.database.configRef ? `bun scripts/cli.ts platform-db postgres status --config ${spec.database.configRef}` : null,
postgresApply: spec.database.configRef ? `bun scripts/cli.ts platform-db postgres apply --config ${spec.database.configRef} --confirm` : null,
secretSync: `bun scripts/cli.ts agentrun control-plane secret-sync --node ${spec.nodeId} --lane ${spec.lane} --confirm`,
restart: `bun scripts/cli.ts agentrun control-plane restart --node ${spec.nodeId} --lane ${spec.lane} --confirm`,
statusFull: statusFullCommand,
triggerCurrent: ciEvidenceMissing || sourceBranchAdvanced ? triggerCurrentCommand : null,
triggerLatest: sourceBranchAdvanced ? `bun scripts/cli.ts agentrun control-plane trigger-current --node ${spec.nodeId} --lane ${spec.lane} --confirm` : null,
},
valuesPrinted: false,
};
if (options.full || options.raw || sourceProbe.value.exitCode !== 0 || runtimeProbe.value.exitCode !== 0 || mirrorProbe.value.exitCode !== 0) {
result.captures = {
source: compactCapture(sourceProbe.value, { full: options.full || options.raw || sourceProbe.value.exitCode !== 0 }),
runtime: compactCapture(runtimeProbe.value, { full: options.full || options.raw || runtimeProbe.value.exitCode !== 0 }),
gitMirror: compactCapture(mirrorProbe.value, { full: options.full || options.raw || mirrorProbe.value.exitCode !== 0 }),
};
}
if (options.full || options.raw) {
result.source = sourcePayload;
result.runtime = runtimePayload;
result.gitMirror = mirrorPayload;
}
return result;
}
function agentRunControlPlaneStatusCommand(spec: AgentRunLaneSpec, options: Pick<StatusOptions, "sourceCommit" | "pipelineRun">, full: boolean): string {
return [
"bun scripts/cli.ts agentrun control-plane status",
`--node ${spec.nodeId}`,
`--lane ${spec.lane}`,
options.pipelineRun === null ? null : `--pipeline-run ${options.pipelineRun}`,
options.sourceCommit === null ? null : `--source-commit ${options.sourceCommit}`,
full ? "--full" : null,
].filter(Boolean).join(" ");
}
function renderAgentRunControlPlaneStatusSummary(result: Record<string, unknown>): RenderedCliResult {
const summary = record(result.summary);
const target = record(result.target);
const node = record(target.node);
const runtimeTarget = record(target.runtime);
const ciTarget = record(target.ci);
const source = record(summary.source);
const gitMirror = record(summary.gitMirror);
const ci = record(summary.ci);
const ciRun = record(ci.pipelineRun);
const argo = record(summary.argo);
const runtime = record(summary.runtime);
const runtimeManager = record(runtime.manager);
const nextAction = record(summary.nextAction);
const next = record(result.next);
const disclosure = record(result.disclosure);
const timings = record(result.timings);
const blockers = Array.isArray(summary.blockers) ? summary.blockers.map(String) : [];
const warnings = Array.isArray(summary.warnings) ? summary.warnings.map(String) : [];
const lines = [
"AGENTRUN CONTROL-PLANE STATUS",
renderTable(
["NODE", "LANE", "NAMESPACE", "SOURCE", "PIPELINE", "ALIGNED", "RUNTIME", "BLOCKERS"],
[[
displayValue(node.id ?? target.node ?? "-"),
displayValue(target.lane ?? "-"),
displayValue(runtimeTarget.namespace ?? "-"),
shortSha(summary.sourceCommit),
displayValue(summary.expectedPipelineRun ?? "-"),
yesNo(summary.aligned),
yesNo(summary.runtimeAligned),
blockers.length === 0 ? "-" : blockers.join(","),
]],
),
"",
renderTable(
["COMPONENT", "STATUS", "DETAIL"],
[
["source", yesNo(source.workspaceClean), `branch=${displayValue(source.branch)} commit=${shortSha(source.remoteBranchCommit)} detached=${yesNo(source.workspaceDetached)}`],
["git-mirror", yesNo(gitMirror.alreadySynced), `read=${yesNo(gitMirror.readReady)} write=${yesNo(gitMirror.writeReady)} gitops=${shortSha(gitMirror.gitopsCommit)}`],
["ci", yesNo(ciRun.status === "True"), `run=${displayValue(ciRun.name)} status=${displayValue(ciRun.status)} reason=${displayValue(ciRun.reason)} evidenceMissing=${yesNo(ci.evidenceMissing)}`],
["argo", yesNo(argo.syncedToGitops), `sync=${displayValue(argo.syncStatus)} health=${displayValue(argo.healthStatus)} revision=${shortSha(argo.revision)}`],
["runtime", yesNo(runtimeManager.sourceMatchesExpected), `manager=${shortSha(runtimeManager.sourceCommit)} secrets=${yesNo(runtime.secretsReady)} dbSecret=${yesNo(runtime.databaseSecretPresent)} localPgAbsent=${yesNo(runtime.localPostgresAbsent)}`],
],
),
"",
warnings.length === 0 ? "WARNINGS\n-" : ["WARNINGS", ...warnings.map((warning) => `- ${warning}`)].join("\n"),
"",
"NEXT",
` action: ${displayValue(nextAction.summary ?? "-")}`,
nextAction.command ? ` run: ${displayValue(nextAction.command)}` : null,
` full: ${displayValue(next.statusFull ?? disclosure.fullCommand ?? "bun scripts/cli.ts agentrun control-plane status --full")}`,
"",
`TIMINGS source=${displayValue(timings.sourceMs)}ms runtime=${displayValue(timings.runtimeMs)}ms gitMirror=${displayValue(timings.gitMirrorMs)}ms total=${displayValue(timings.totalMs)}ms`,
].filter((line): line is string => line !== null);
return renderedCliResult(result.ok !== false, "agentrun control-plane status", `${lines.join("\n")}\n`);
}
function yesNo(value: unknown): string {
if (value === true) return "yes";
if (value === false) return "no";
if (value === null || value === undefined) return "-";
return displayValue(value);
}
function shortSha(value: unknown): string {
const text = displayValue(value);
if (/^[0-9a-f]{40}$/u.test(text)) return text.slice(0, 12);
return text;
}
function compactLaneSecretsStatus(secrets: Record<string, unknown>): Record<string, unknown> {
const items = Array.isArray(secrets.items) ? secrets.items.filter((item): item is Record<string, unknown> => typeof item === "object" && item !== null && !Array.isArray(item)) : [];
const missing = items
.filter((item) => item.present !== true || item.keyPresent !== true)
.map((item) => ({
namespace: item.namespace ?? null,
name: item.name ?? null,
key: item.key ?? null,
present: item.present ?? false,
keyPresent: item.keyPresent ?? false,
}));
return {
ready: secrets.ready ?? false,
count: secrets.count ?? items.length,
missingCount: missing.length,
missing,
valuesPrinted: false,
};
}
function compactAgentRunLaneStatusTarget(spec: AgentRunLaneSpec): Record<string, unknown> {
return {
node: {
id: spec.nodeId,
route: spec.nodeRoute,
kubeRoute: spec.nodeKubeRoute,
},
lane: spec.lane,
version: spec.version,
source: {
repository: spec.source.repository,
branch: spec.source.branch,
},
runtime: {
namespace: spec.runtime.namespace,
managerDeployment: spec.runtime.managerDeployment,
managerService: spec.runtime.managerService,
},
ci: {
namespace: spec.ci.namespace,
pipeline: spec.ci.pipeline,
pipelineRunPrefix: spec.ci.pipelineRunPrefix,
},
gitops: {
branch: spec.gitops.branch,
argoApplication: spec.gitops.argoApplication,
},
gitMirror: {
namespace: spec.gitMirror.namespace,
readService: spec.gitMirror.readService,
writeService: spec.gitMirror.writeService,
repositoryCount: spec.gitMirror.repositories.length,
},
database: {
mode: spec.database.mode,
provider: spec.database.provider,
configRef: spec.database.configRef,
localPostgresExpectedAbsent: spec.database.localPostgresExpectedAbsent,
valuesPrinted: false,
},
secretCount: spec.secrets.length,
valuesPrinted: false,
};
}
async function restartYamlLane(config: UniDeskConfig, options: LaneConfirmOptions): Promise<Record<string, unknown>> {
const { configPath, spec } = resolveAgentRunLaneTarget(options);
const plan = {
node: spec.nodeId,
kubeRoute: spec.nodeKubeRoute,
lane: spec.lane,
namespace: spec.runtime.namespace,
deployment: spec.runtime.managerDeployment,
annotation: "kubectl.kubernetes.io/restartedAt",
reason: "reload-lane-secrets-or-runtime-env",
valuesPrinted: false,
};
if (options.dryRun || !options.confirm) {
return {
ok: true,
command: "agentrun control-plane restart",
mode: "dry-run",
mutation: false,
configPath,
target: agentRunLaneSummary(spec),
plan,
next: {
confirm: `bun scripts/cli.ts agentrun control-plane restart --node ${spec.nodeId} --lane ${spec.lane} --confirm`,
status: `bun scripts/cli.ts agentrun control-plane status --node ${spec.nodeId} --lane ${spec.lane}`,
},
valuesPrinted: false,
};
}
const result = await capture(config, spec.nodeKubeRoute, ["sh", "--", restartYamlLaneScript(spec)]);
const payload = captureJsonPayload(result);
return {
ok: result.exitCode === 0 && payload.ok !== false,
command: "agentrun control-plane restart",
mode: "confirmed-rollout-restart",
mutation: true,
configPath,
target: agentRunLaneSummary(spec),
plan,
result: payload,
capture: compactCapture(result, { full: result.exitCode !== 0, stdoutTailChars: 4000, stderrTailChars: 4000 }),
next: {
status: `bun scripts/cli.ts agentrun control-plane status --node ${spec.nodeId} --lane ${spec.lane}`,
},
valuesPrinted: false,
};
}
async function secretSync(config: UniDeskConfig, options: SecretSyncOptions): Promise<Record<string, unknown>> {
const { configPath, spec } = resolveAgentRunLaneTarget(options);
const sources = collectLaneSecretSources(spec);
if (sources.length === 0) {
return {
ok: false,
command: "agentrun control-plane secret-sync",
mode: "yaml-declared-node-lane",
configPath,
target: agentRunLaneSummary(spec),
degradedReason: "lane-secret-sources-not-declared",
valuesPrinted: false,
};
}
const values = sources.map((source) => ({ spec: source, value: readSecretSourceValue(spec, source) }));
const plan = values.map(({ spec: item, value }) => ({
id: item.id,
namespace: item.targetRef.namespace,
secret: item.targetRef.name,
key: item.targetRef.key,
sourceRef: item.sourceRef,
sourceMode: item.sourceMode,
sourceKey: item.sourceKey,
sourcePath: value.redactedPath,
fingerprint: value.fingerprint,
valueBytes: value.valueBytes,
valuesPrinted: false,
}));
if (options.dryRun || !options.confirm) {
return {
ok: true,
command: "agentrun control-plane secret-sync",
mode: "dry-run",
mutation: false,
configPath,
target: agentRunLaneSummary(spec),
plan: { secretCount: plan.length, items: plan, valuesPrinted: false },
next: {
confirm: `bun scripts/cli.ts agentrun control-plane secret-sync --node ${spec.nodeId} --lane ${spec.lane} --confirm`,
status: `bun scripts/cli.ts agentrun control-plane status --node ${spec.nodeId} --lane ${spec.lane}`,
},
valuesPrinted: false,
};
}
const result = await capture(config, spec.nodeKubeRoute, ["sh", "--", secretSyncScript(spec, values.map(({ spec: item, value }) => ({ targetRef: item.targetRef, value: value.value })))]);
const payload = captureJsonPayload(result);
return {
ok: result.exitCode === 0 && payload.ok !== false,
command: "agentrun control-plane secret-sync",
mode: "confirmed-sync",
mutation: true,
configPath,
target: agentRunLaneSummary(spec),
plan: { secretCount: plan.length, items: plan, valuesPrinted: false },
result: payload,
capture: compactCapture(result, { full: result.exitCode !== 0, stdoutTailChars: 3000, stderrTailChars: 3000 }),
next: {
status: `bun scripts/cli.ts agentrun control-plane status --node ${spec.nodeId} --lane ${spec.lane}`,
},
valuesPrinted: false,
};
}
async function exposeAgentRun(_config: UniDeskConfig, options: ConfirmOptions): Promise<Record<string, unknown>> {
const clientConfig = readAgentRunClientConfig();
const exposure = clientConfig.publicExposure;
if (!exposure?.enabled) {
return {
ok: true,
command: "agentrun control-plane expose",
action: "disabled-by-yaml",
publicExposure: exposureSummary(exposure),
valuesPrinted: false,
};
}
if (options.dryRun) {
return {
ok: true,
command: "agentrun control-plane expose",
dryRun: true,
mutation: false,
publicExposure: exposureSummary(exposure),
plan: {
masterFrps: {
action: "ensure-allow-port",
configPath: exposure.masterFrps.configPath,
containerName: exposure.masterFrps.containerName,
remotePort: exposure.remotePort,
},
masterCaddy: {
action: "ensure-https-vhost",
domain: exposure.masterCaddy.domain,
configPath: exposure.masterCaddy.configPath,
serviceName: exposure.masterCaddy.serviceName,
upstreamBaseUrl: exposure.masterCaddy.upstreamBaseUrl,
responseHeaderTimeoutSeconds: exposure.masterCaddy.responseHeaderTimeoutSeconds,
},
},
next: { confirm: "bun scripts/cli.ts agentrun control-plane expose --confirm" },
valuesPrinted: false,
};
}
const masterFrps = applyAgentRunMasterFrpsAllowPort(exposure);
const masterCaddy = applyAgentRunMasterCaddySite(exposure);
const ok = masterFrps.ok === true && masterCaddy.ok === true;
return {
ok,
command: "agentrun control-plane expose",
mutation: true,
publicExposure: exposureSummary(exposure),
masterFrps,
masterCaddy,
validation: {
publicProbe: `curl -fsS ${exposure.publicBaseUrl.replace(/\/+$/u, "")}/health/readiness`,
directClient: "bun scripts/cli.ts agentrun get tasks --queue commander -o json",
valuesPrinted: false,
},
valuesPrinted: false,
};
}
function exposureSummary(exposure: AgentRunPublicExposure | null | undefined): Record<string, unknown> {
if (!exposure) return { enabled: false, valuesPrinted: false };
return {
enabled: exposure.enabled,
proxyName: exposure.proxyName,
remotePort: exposure.remotePort,
publicBaseUrl: exposure.publicBaseUrl,
masterBaseUrl: exposure.masterBaseUrl,
frps: {
configPath: exposure.masterFrps.configPath,
containerName: exposure.masterFrps.containerName,
},
caddy: {
enabled: exposure.masterCaddy.enabled,
domain: exposure.masterCaddy.domain,
configPath: exposure.masterCaddy.configPath,
serviceName: exposure.masterCaddy.serviceName,
upstreamBaseUrl: exposure.masterCaddy.upstreamBaseUrl,
responseHeaderTimeoutSeconds: exposure.masterCaddy.responseHeaderTimeoutSeconds,
},
valuesPrinted: false,
};
}
function applyAgentRunMasterFrpsAllowPort(exposure: AgentRunPublicExposure): Record<string, unknown> {
const pathValue = exposure.masterFrps.configPath;
const port = exposure.remotePort;
const container = exposure.masterFrps.containerName;
if (!existsSync(pathValue)) return { ok: false, error: "master-frps-config-missing", path: pathValue, valuesPrinted: false };
const before = readFileSync(pathValue, "utf8");
const alreadyAllowed = frpsAllowPortExists(before, port);
let backupPath: string | null = null;
let action = "kept-existing";
if (!alreadyAllowed) {
const stamp = new Date().toISOString().replace(/[-:]/gu, "").replace(/\..*$/u, "Z");
backupPath = `${pathValue}.bak-agentrun-${stamp}`;
copyFileSync(pathValue, backupPath);
writeFileSync(pathValue, `${before.replace(/\s*$/u, "")}\n\n[[allowPorts]]\nstart = ${port}\nend = ${port}\n`, "utf8");
chmodSync(pathValue, statSync(backupPath).mode & 0o777);
action = "added-allow-port";
}
const restart = alreadyAllowed ? null : spawnSync("docker", ["restart", container], { encoding: "utf8" });
const inspect = spawnSync("docker", ["inspect", "-f", "{{.State.Running}}", container], { encoding: "utf8" });
const ok = alreadyAllowed || (restart?.status === 0 && inspect.status === 0 && String(inspect.stdout).trim() === "true");
return {
ok,
action,
path: pathValue,
backupPath,
remotePort: port,
containerName: container,
restart: restart === null ? null : { exitCode: restart.status, stdoutTail: String(restart.stdout).slice(-1000), stderrTail: String(restart.stderr).slice(-1000) },
inspect: { exitCode: inspect.status, running: String(inspect.stdout).trim() === "true", stderrTail: String(inspect.stderr).slice(-1000) },
valuesPrinted: false,
};
}
function applyAgentRunMasterCaddySite(exposure: AgentRunPublicExposure): Record<string, unknown> {
const caddy = exposure.masterCaddy;
if (!caddy.enabled) return { ok: true, action: "disabled-by-yaml", valuesPrinted: false };
const pathValue = caddy.configPath;
if (!existsSync(pathValue)) return { ok: false, error: "master-caddy-config-missing", path: pathValue, valuesPrinted: false };
const desiredBlock = renderAgentRunCaddySiteBlock(caddy.domain, caddy.upstreamBaseUrl, caddy.responseHeaderTimeoutSeconds);
const applied = applyLocalCaddyManagedSite({
serviceId: "agentrun",
markerName: "agentrun",
configPath: pathValue,
serviceName: caddy.serviceName,
siteBlock: desiredBlock,
legacySiteDomain: caddy.domain,
});
return {
...applied,
domain: caddy.domain,
upstreamBaseUrl: caddy.upstreamBaseUrl,
responseHeaderTimeoutSeconds: caddy.responseHeaderTimeoutSeconds,
valuesPrinted: false,
};
}
function renderAgentRunCaddySiteBlock(domain: string, upstreamBaseUrl: string, responseHeaderTimeoutSeconds: number): string {
const upstream = new URL(upstreamBaseUrl);
const upstreamHost = `${upstream.hostname}${upstream.port ? `:${upstream.port}` : ""}`;
return `${domain} {
encode zstd gzip
reverse_proxy ${upstreamHost} {
header_up Host {host}
header_up X-Real-IP {remote_host}
transport http {
dial_timeout 5s
response_header_timeout ${responseHeaderTimeoutSeconds}s
}
}
}`;
}
function frpsAllowPortExists(toml: string, port: number): boolean {
const sections = toml.split(/(?=\[\[allowPorts\]\])/u);
return sections.some((section) => {
if (!section.includes("[[allowPorts]]")) return false;
const start = section.match(/^\s*start\s*=\s*(\d+)\s*$/mu);
const end = section.match(/^\s*end\s*=\s*(\d+)\s*$/mu);
return Number(start?.[1]) === port && Number(end?.[1]) === port;
});
}
async function triggerCurrent(config: UniDeskConfig, options: TriggerOptions): Promise<Record<string, unknown>> {
return await triggerCurrentYamlLane(config, options, resolveAgentRunLaneTarget(options));
}
async function triggerCurrentYamlLane(config: UniDeskConfig, options: TriggerOptions, target: { configPath: string; spec: AgentRunLaneSpec }): Promise<Record<string, unknown>> {
const spec = target.spec;
const probe = await capture(config, spec.nodeRoute, ["sh", "--", yamlLaneSourceBootstrapProbeScript(spec)]);
const source = captureJsonPayload(probe);
const sourceCommit = stringOrNull(source.sourceCommit);
const remoteBranchExists = source.remoteBranchExists === true;
const pipelineRun = sourceCommit !== null && isGitSha(sourceCommit) ? agentRunPipelineRunName(spec, sourceCommit) : null;
const placeholderImage = sourceCommit === null ? null : placeholderAgentRunImage(spec, sourceCommit);
const renderedFiles = placeholderImage === null ? [] : renderAgentRunGitopsFiles(spec, { sourceCommit, image: placeholderImage });
const plan = {
node: spec.nodeId,
lane: spec.lane,
version: spec.version,
source: {
workspace: spec.source.workspace,
remote: spec.source.remote,
branch: spec.source.branch,
bootstrapFromBranch: spec.source.bootstrapFromBranch,
bootstrapTimeoutSeconds: spec.source.bootstrapTimeoutSeconds,
bootstrapPollSeconds: spec.source.bootstrapPollSeconds,
remoteBranchExists,
sourceCommit,
},
deploymentFormat: spec.deployment.format,
deploymentTruth: "config/agentrun.yaml",
removedServiceDeployJson: true,
pipelineRun,
imageBuild: {
repository: `${spec.ci.registryPrefix}/${spec.deployment.manager.imageBuild.repository}`,
containerfile: spec.deployment.manager.imageBuild.containerfile,
buildArgNames: Object.keys(spec.deployment.manager.imageBuild.buildArgs).sort(),
timeoutSeconds: spec.deployment.manager.imageBuild.timeoutSeconds,
pollSeconds: spec.deployment.manager.imageBuild.pollSeconds,
proxyConfigured: spec.deployment.manager.imageBuild.httpProxy !== null || spec.deployment.manager.imageBuild.httpsProxy !== null,
},
gitops: {
branch: spec.gitops.branch,
path: spec.gitops.path,
renderedFileCount: renderedFiles.length,
renderedFilesDigest: renderedFiles.length === 0 ? null : renderedFilesDigest(renderedFiles),
},
valuesPrinted: false,
};
if (options.dryRun || !options.confirm) {
return {
ok: true,
command: "agentrun control-plane trigger-current",
mode: "dry-run",
mutation: false,
configPath: target.configPath,
target: agentRunLaneSummary(spec),
plan,
sourceProbe: compactCapture(probe, { full: probe.exitCode !== 0, stdoutTailChars: 3000, stderrTailChars: 3000 }),
next: {
confirm: `bun scripts/cli.ts agentrun control-plane trigger-current --node ${spec.nodeId} --lane ${spec.lane} --confirm`,
status: `bun scripts/cli.ts agentrun control-plane status --node ${spec.nodeId} --lane ${spec.lane}`,
},
valuesPrinted: false,
};
}
if (options.wait) {
return await triggerCurrentYamlLaneConfirmed(config, spec, target.configPath, true);
}
return startAsyncAgentRunJob(
`agentrun_${spec.lane}_trigger_current`,
["bun", "scripts/cli.ts", "agentrun", "control-plane", "trigger-current", "--node", spec.nodeId, "--lane", spec.lane, "--confirm", "--wait"],
`Run AgentRun ${spec.version} YAML-only trigger-current on ${spec.nodeId}`,
);
}
async function triggerCurrentYamlLaneConfirmed(config: UniDeskConfig, spec: AgentRunLaneSpec, configPath: string, waited: boolean): Promise<Record<string, unknown>> {
const result = await triggerCurrentYamlLaneConfirmedSteps(config, spec, configPath, waited);
const restore = await capture(config, spec.nodeRoute, ["sh", "--", yamlLaneSourceRestoreScript(spec)]);
const restorePayload = captureJsonPayload(restore);
const restoreOk = restore.exitCode === 0 && restorePayload.ok === true;
const existingWarnings = Array.isArray(result.warnings) ? result.warnings.filter((item): item is string => typeof item === "string") : [];
return {
...result,
warnings: restoreOk ? existingWarnings : Array.from(new Set([...existingWarnings, "source-worktree-restore-failed"])),
sourceWorkspaceRestore: restorePayload,
sourceWorkspaceRestoreCapture: compactCapture(restore, { full: !restoreOk, stdoutTailChars: 3000, stderrTailChars: 3000 }),
valuesPrinted: false,
};
}
async function triggerCurrentYamlLaneConfirmedSteps(config: UniDeskConfig, spec: AgentRunLaneSpec, configPath: string, waited: boolean): Promise<Record<string, unknown>> {
progressEvent("agentrun.yaml-lane.source-bootstrap.progress", {
node: spec.nodeId,
lane: spec.lane,
status: "submitting",
});
const bootstrapSubmit = await capture(config, spec.nodeRoute, ["sh", "--", yamlLaneSourceBootstrapSubmitScript(spec)]);
const bootstrapSubmitPayload = captureJsonPayload(bootstrapSubmit);
if (bootstrapSubmit.exitCode !== 0 || bootstrapSubmitPayload.ok === false) {
return {
ok: false,
command: "agentrun control-plane trigger-current",
mode: waited ? "confirmed-waited" : "confirmed-trigger",
configPath,
target: agentRunLaneSummary(spec),
phase: "source-bootstrap-submit",
degradedReason: "yaml-lane-source-bootstrap-submit-failed",
result: bootstrapSubmitPayload,
capture: compactCapture(bootstrapSubmit, { full: true, stdoutTailChars: 5000, stderrTailChars: 5000 }),
valuesPrinted: false,
};
}
const bootstrap = await waitForYamlLaneSourceBootstrap(config, spec, stringOrNull(bootstrapSubmitPayload.jobId));
const bootstrapPayload = bootstrap.payload;
const sourceCommit = stringOrNull(bootstrapPayload.sourceCommit);
if (bootstrap.ok !== true || sourceCommit === null || !isGitSha(sourceCommit)) {
return {
ok: false,
command: "agentrun control-plane trigger-current",
mode: waited ? "confirmed-waited" : "confirmed-trigger",
configPath,
target: agentRunLaneSummary(spec),
phase: "source-bootstrap",
degradedReason: "yaml-lane-source-bootstrap-failed",
result: bootstrapPayload,
bootstrapStatus: bootstrap,
valuesPrinted: false,
};
}
const buildSubmit = await capture(config, spec.nodeRoute, ["sh", "--", yamlLaneBuildImageSubmitScript(spec, sourceCommit)]);
const buildSubmitPayload = captureJsonPayload(buildSubmit);
if (buildSubmit.exitCode !== 0 || buildSubmitPayload.ok === false) {
return {
ok: false,
command: "agentrun control-plane trigger-current",
mode: waited ? "confirmed-waited" : "confirmed-trigger",
configPath,
target: agentRunLaneSummary(spec),
phase: "image-build-submit",
sourceCommit,
degradedReason: "yaml-lane-image-build-submit-failed",
sourceBootstrap: bootstrapPayload,
result: buildSubmitPayload,
capture: compactCapture(buildSubmit, { full: true, stdoutTailChars: 5000, stderrTailChars: 5000 }),
valuesPrinted: false,
};
}
const build = await waitForYamlLaneBuildImage(config, spec, sourceCommit, stringOrNull(buildSubmitPayload.jobId));
const buildPayload = build.payload;
const digest = stringOrNull(buildPayload.digest);
const envIdentity = stringOrNull(buildPayload.envIdentity);
if (build.ok !== true || digest === null || envIdentity === null) {
return {
ok: false,
command: "agentrun control-plane trigger-current",
mode: waited ? "confirmed-waited" : "confirmed-trigger",
configPath,
target: agentRunLaneSummary(spec),
phase: "image-build",
sourceCommit,
degradedReason: "yaml-lane-image-build-failed",
sourceBootstrap: bootstrapPayload,
buildSubmit: buildSubmitPayload,
result: buildPayload,
buildStatus: build,
valuesPrinted: false,
};
}
const image = agentRunImageArtifact(spec, { sourceCommit, envIdentity, digest, status: stringOrNull(buildPayload.status) ?? "built" });
const renderedFiles = renderAgentRunGitopsFiles(spec, { sourceCommit, image });
progressEvent("agentrun.yaml-lane.gitops-publish.progress", {
node: spec.nodeId,
lane: spec.lane,
sourceCommit,
status: "submitting",
});
const gitopsSubmit = await capture(config, spec.nodeRoute, ["sh", "--", yamlLaneGitopsPublishSubmitScript(spec, renderedFiles)]);
const gitopsSubmitPayload = captureJsonPayload(gitopsSubmit);
if (gitopsSubmit.exitCode !== 0 || gitopsSubmitPayload.ok === false) {
return {
ok: false,
command: "agentrun control-plane trigger-current",
mode: waited ? "confirmed-waited" : "confirmed-trigger",
configPath,
target: agentRunLaneSummary(spec),
phase: "gitops-publish-submit",
sourceCommit,
image,
degradedReason: "yaml-lane-gitops-publish-submit-failed",
sourceBootstrap: bootstrapPayload,
imageBuild: buildPayload,
result: gitopsSubmitPayload,
capture: compactCapture(gitopsSubmit, { full: true, stdoutTailChars: 5000, stderrTailChars: 5000 }),
valuesPrinted: false,
};
}
const gitops = await waitForYamlLaneGitopsPublish(config, spec, sourceCommit, stringOrNull(gitopsSubmitPayload.jobId));
const gitopsPayload = gitops.payload;
if (gitops.ok !== true || gitopsPayload.ok === false) {
return {
ok: false,
command: "agentrun control-plane trigger-current",
mode: waited ? "confirmed-waited" : "confirmed-trigger",
configPath,
target: agentRunLaneSummary(spec),
phase: "gitops-publish",
sourceCommit,
image,
degradedReason: "yaml-lane-gitops-publish-failed",
sourceBootstrap: bootstrapPayload,
imageBuild: buildPayload,
gitopsPublishSubmit: gitopsSubmitPayload,
result: gitopsPayload,
gitopsPublishStatus: gitops,
valuesPrinted: false,
};
}
const mirror = await runYamlLaneGitMirrorSyncJob(config, spec);
if (mirror.ok !== true) {
return {
ok: false,
command: "agentrun control-plane trigger-current",
mode: waited ? "confirmed-waited" : "confirmed-trigger",
configPath,
target: agentRunLaneSummary(spec),
phase: "git-mirror-sync",
sourceCommit,
image,
gitops: gitopsPayload,
degradedReason: "yaml-lane-git-mirror-sync-failed",
result: mirror,
valuesPrinted: false,
};
}
const pipelineRun = agentRunPipelineRunName(spec, sourceCommit);
const created = await capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLanePipelineRunCreateScript(spec, sourceCommit, pipelineRun)]);
const createPayload = captureJsonPayload(created);
return {
ok: created.exitCode === 0 && createPayload.ok !== false,
command: "agentrun control-plane trigger-current",
mode: waited ? "confirmed-waited" : "confirmed-trigger",
mutation: true,
configPath,
target: agentRunLaneSummary(spec),
sourceCommit,
pipelineRun,
image,
sourceBootstrap: bootstrapPayload,
imageBuildSubmit: buildSubmitPayload,
imageBuild: buildPayload,
gitopsPublishSubmit: gitopsSubmitPayload,
renderedFiles: {
count: renderedFiles.length,
digest: renderedFilesDigest(renderedFiles),
},
gitops: gitopsPayload,
gitMirror: mirror,
created: createPayload,
capture: compactCapture(created, { full: created.exitCode !== 0, stdoutTailChars: 4000, stderrTailChars: 4000 }),
next: {
status: `bun scripts/cli.ts agentrun control-plane status --node ${spec.nodeId} --lane ${spec.lane}`,
statusByPipelineRun: `bun scripts/cli.ts agentrun control-plane status --node ${spec.nodeId} --lane ${spec.lane} --pipeline-run ${pipelineRun} --full`,
},
valuesPrinted: false,
};
}
async function refresh(config: UniDeskConfig, options: RefreshOptions): Promise<Record<string, unknown>> {
return await refreshYamlLane(config, options);
}
async function refreshYamlLane(config: UniDeskConfig, options: RefreshOptions): Promise<Record<string, unknown>> {
const { configPath, spec } = resolveAgentRunLaneTarget(options);
const plan = {
node: spec.nodeId,
lane: spec.lane,
version: spec.version,
argoNamespace: spec.gitops.argoNamespace,
argoApplication: spec.gitops.argoApplication,
gitopsBranch: spec.gitops.branch,
mutation: "argocd-hard-refresh",
valuesPrinted: false,
};
if (options.dryRun || !options.confirm) {
return {
ok: true,
command: "agentrun control-plane refresh",
mode: "dry-run",
mutation: false,
configPath,
target: agentRunLaneSummary(spec),
plan,
next: {
confirm: `bun scripts/cli.ts agentrun control-plane refresh --node ${spec.nodeId} --lane ${spec.lane} --confirm`,
status: `bun scripts/cli.ts agentrun control-plane status --node ${spec.nodeId} --lane ${spec.lane}`,
},
valuesPrinted: false,
};
}
const refreshed = await capture(config, spec.nodeKubeRoute, ["sh", "--", refreshYamlLaneScript(spec)]);
const payload = captureJsonPayload(refreshed);
return {
ok: refreshed.exitCode === 0 && payload.ok !== false,
command: "agentrun control-plane refresh",
mode: "confirmed-yaml-lane",
mutation: true,
configPath,
target: agentRunLaneSummary(spec),
plan,
result: payload,
refreshed: compactCapture(refreshed, { full: refreshed.exitCode !== 0, stdoutTailChars: 3000, stderrTailChars: 3000 }),
next: {
status: `bun scripts/cli.ts agentrun control-plane status --node ${spec.nodeId} --lane ${spec.lane}`,
},
valuesPrinted: false,
};
}
async function cleanupRunners(config: UniDeskConfig, options: CleanupRunnersOptions): Promise<Record<string, unknown>> {
const { configPath, spec } = resolveAgentRunLaneTarget(options);
const result = await capture(config, spec.nodeKubeRoute, ["sh", "--", cleanupRunnersScript(options, spec)]);
const payload = captureJsonPayload(result);
const ok = result.exitCode === 0 && payload.ok !== false;
const base = {
...payload,
ok,
command: "agentrun control-plane cleanup-runners",
configPath,
target: agentRunLaneSummary(spec),
mode: options.dryRun || !options.confirm ? "dry-run" : "confirmed-cleanup",
namespace: spec.runtime.namespace,
retention: spec.deployment.runner.retention,
probe: compactCapture(result, { full: result.exitCode !== 0, stdoutTailChars: 3000, stderrTailChars: 3000 }),
};
if (options.dryRun || !options.confirm) {
return {
...base,
dryRun: true,
mutation: false,
next: {
confirm: `bun scripts/cli.ts agentrun control-plane cleanup-runners --node ${spec.nodeId} --lane ${spec.lane}${options.forceActive ? " --force-active" : ""} --confirm`,
},
};
}
return {
...base,
dryRun: false,
mutation: true,
followUp: {
dryRun: `bun scripts/cli.ts agentrun control-plane cleanup-runners --node ${spec.nodeId} --lane ${spec.lane}${options.forceActive ? " --force-active" : ""} --dry-run`,
status: `bun scripts/cli.ts agentrun control-plane status --node ${spec.nodeId} --lane ${spec.lane}`,
},
};
}
async function cleanupRuns(config: UniDeskConfig, options: CleanupRunsOptions): Promise<Record<string, unknown>> {
const { configPath, spec } = resolveAgentRunLaneTarget(options);
const result = await capture(config, spec.nodeKubeRoute, ["sh", "--", cleanupRunsScript(options, spec.ci.namespace, spec.ci.pipelineRunPrefix)]);
const payload = captureJsonPayload(result);
const ok = result.exitCode === 0 && payload.ok !== false;
const base = {
...payload,
ok,
command: "agentrun control-plane cleanup-runs",
configPath,
target: agentRunLaneSummary(spec),
mode: options.dryRun || !options.confirm ? "dry-run" : "confirmed-cleanup",
namespace: spec.ci.namespace,
minAgeMinutes: options.minAgeMinutes,
limit: options.limit,
probe: compactCapture(result, { full: result.exitCode !== 0, stdoutTailChars: 3000, stderrTailChars: 3000 }),
};
if (options.dryRun || !options.confirm) {
return {
...base,
dryRun: true,
mutation: false,
next: {
confirm: `bun scripts/cli.ts agentrun control-plane cleanup-runs --node ${spec.nodeId} --lane ${spec.lane} --min-age-minutes ${options.minAgeMinutes} --limit ${options.limit} --confirm`,
},
};
}
return {
...base,
dryRun: false,
mutation: true,
followUp: {
status: `bun scripts/cli.ts agentrun control-plane status --node ${spec.nodeId} --lane ${spec.lane}`,
releasedPvs: `bun scripts/cli.ts agentrun control-plane cleanup-released-pvs --node ${spec.nodeId} --lane ${spec.lane} --limit ${options.limit} --dry-run`,
diskPressure: `trans ${spec.nodeKubeRoute} kubectl get node -o jsonpath='{range .items[*]}{.metadata.name}{\"\\t\"}{range .status.conditions[*]}{.type}{\"=\"}{.status}{\" \"}{.reason}{\";\"}{end}{\"\\n\"}{end}'`,
},
};
}
async function cleanupReleasedPvs(config: UniDeskConfig, options: CleanupReleasedPvOptions): Promise<Record<string, unknown>> {
const { configPath, spec } = resolveAgentRunLaneTarget(options);
const result = await capture(config, spec.nodeKubeRoute, ["sh", "--", cleanupReleasedPvsScript(options, spec.ci.namespace)]);
const payload = captureJsonPayload(result);
const ok = result.exitCode === 0 && payload.ok !== false;
const base = {
...payload,
ok,
command: "agentrun control-plane cleanup-released-pvs",
configPath,
target: agentRunLaneSummary(spec),
mode: options.dryRun || !options.confirm ? "dry-run" : "confirmed-cleanup",
namespace: spec.ci.namespace,
limit: options.limit,
probe: compactCapture(result, { full: result.exitCode !== 0, stdoutTailChars: 3000, stderrTailChars: 3000 }),
};
if (options.dryRun || !options.confirm) {
return {
...base,
dryRun: true,
mutation: false,
next: {
confirm: `bun scripts/cli.ts agentrun control-plane cleanup-released-pvs --node ${spec.nodeId} --lane ${spec.lane} --limit ${options.limit} --confirm`,
},
};
}
return {
...base,
dryRun: false,
mutation: true,
followUp: {
cleanupRuns: `bun scripts/cli.ts agentrun control-plane cleanup-runs --node ${spec.nodeId} --lane ${spec.lane} --min-age-minutes 30 --limit ${options.limit} --dry-run`,
diskPressure: `trans ${spec.nodeKubeRoute} kubectl get node -o jsonpath='{range .items[*]}{.metadata.name}{\"\\t\"}{range .status.conditions[*]}{.type}{\"=\"}{.status}{\" \"}{.reason}{\";\"}{end}{\"\\n\"}{end}'`,
},
};
}
function cleanupRunnersScript(options: CleanupRunnersOptions, spec: AgentRunLaneSpec): string {
const retention = spec.deployment.runner.retention;
const matchLabelsB64 = Buffer.from(JSON.stringify(retention.selectors.matchLabels), "utf8").toString("base64");
const jobNamePrefixesB64 = Buffer.from(JSON.stringify(retention.selectors.jobNamePrefixes), "utf8").toString("base64");
return [
"set -eu",
`namespace=${shQuote(spec.runtime.namespace)}`,
`manager_deployment=${shQuote(spec.runtime.managerDeployment)}`,
`max_runners=${String(retention.maxRunners)}`,
`cleanup_order=${shQuote(retention.cleanupOrder)}`,
`active_heartbeat_max_age_ms=${String(retention.activeHeartbeatMaxAgeMs)}`,
`age_based_cleanup_enabled=${retention.ageBasedCleanup.enabled ? "true" : "false"}`,
`age_based_max_age_hours=${retention.ageBasedCleanup.maxAgeHours === null ? "" : String(retention.ageBasedCleanup.maxAgeHours)}`,
`timeout_seconds=${String(options.timeoutSeconds)}`,
`force_active=${options.forceActive ? "true" : "false"}`,
`match_labels_json_b64=${shQuote(matchLabelsB64)}`,
`job_name_prefixes_json_b64=${shQuote(jobNamePrefixesB64)}`,
"match_labels_json=$(printf '%s' \"$match_labels_json_b64\" | base64 -d)",
"job_name_prefixes_json=$(printf '%s' \"$job_name_prefixes_json_b64\" | base64 -d)",
"tmp_dir=$(mktemp -d)",
"trap 'rm -rf \"$tmp_dir\"' EXIT",
"kubectl -n \"$namespace\" get job -o json > \"$tmp_dir/jobs.json\"",
"kubectl -n \"$namespace\" get pod -o json > \"$tmp_dir/pods.json\"",
"facts_exit=0",
"set +e",
"kubectl -n \"$namespace\" exec -i deploy/\"$manager_deployment\" -- env RETENTION_NAMESPACE=\"$namespace\" sh -lc 'cat >/tmp/agentrun-runner-retention.mjs && bun /tmp/agentrun-runner-retention.mjs' > \"$tmp_dir/runner-facts.json\" 2> \"$tmp_dir/runner-facts.err\" <<'NODE'",
cleanupRunnersFactsNodeScript(),
"NODE",
"facts_exit=$?",
"set -e",
"if [ \"$facts_exit\" -ne 0 ]; then",
" printf '%s\\n' '{\"ok\":false,\"items\":[],\"failureKind\":\"manager-facts-unavailable\",\"valuesPrinted\":false}' > \"$tmp_dir/runner-facts.json\"",
"fi",
"MATCH_LABELS_JSON=\"$match_labels_json\" JOB_NAME_PREFIXES_JSON=\"$job_name_prefixes_json\" MAX_RUNNERS=\"$max_runners\" CLEANUP_ORDER=\"$cleanup_order\" ACTIVE_HEARTBEAT_MAX_AGE_MS=\"$active_heartbeat_max_age_ms\" AGE_BASED_CLEANUP_ENABLED=\"$age_based_cleanup_enabled\" AGE_BASED_MAX_AGE_HOURS=\"$age_based_max_age_hours\" FORCE_ACTIVE=\"$force_active\" FACTS_EXIT=\"$facts_exit\" TMP_DIR=\"$tmp_dir\" NAMESPACE=\"$namespace\" node <<'NODE' > \"$tmp_dir/plan.json\"",
cleanupRunnersPlanNodeScript(),
"NODE",
"if [ " + shQuote(options.confirm && !options.dryRun ? "true" : "false") + " != true ]; then",
" cat \"$tmp_dir/plan.json\"",
" exit 0",
"fi",
"node -e 'const fs=require(\"node:fs\"); const plan=JSON.parse(fs.readFileSync(process.argv[1],\"utf8\")); const names=Array.isArray(plan.selectedRunnerJobs)?plan.selectedRunnerJobs:[]; fs.writeFileSync(process.argv[2], names.join(\"\\n\") + (names.length>0?\"\\n\":\"\"));' \"$tmp_dir/plan.json\" \"$tmp_dir/selected-jobs.txt\"",
"delete_exit=0",
"if [ -s \"$tmp_dir/selected-jobs.txt\" ]; then",
" xargs -r kubectl -n \"$namespace\" delete job --ignore-not-found=true --wait=true --timeout=\"${timeout_seconds}s\" < \"$tmp_dir/selected-jobs.txt\" > \"$tmp_dir/delete.out\" 2> \"$tmp_dir/delete.err\" || delete_exit=$?",
"else",
" : > \"$tmp_dir/delete.out\"",
" : > \"$tmp_dir/delete.err\"",
"fi",
"kubectl -n \"$namespace\" get job -o json > \"$tmp_dir/jobs-after.json\"",
"kubectl -n \"$namespace\" get pod -o json > \"$tmp_dir/pods-after.json\"",
"DELETE_EXIT=\"$delete_exit\" TMP_DIR=\"$tmp_dir\" node <<'NODE'",
cleanupRunnersFinalizeNodeScript(),
"NODE",
].join("\n");
}
function cleanupRunsScript(options: CleanupRunsOptions, namespace: string, pipelineRunPrefix: string): string {
return [
"set -eu",
`namespace=${shQuote(namespace)}`,
`pipeline_run_prefix=${shQuote(pipelineRunPrefix)}`,
`min_age_minutes=${String(options.minAgeMinutes)}`,
`limit=${String(options.limit)}`,
`timeout_seconds=${String(options.timeoutSeconds)}`,
"tmp_dir=$(mktemp -d)",
"trap 'rm -rf \"$tmp_dir\"' EXIT",
"kubectl -n \"$namespace\" get pipelinerun -o json > \"$tmp_dir/pipelineruns.json\"",
"kubectl -n \"$namespace\" get pvc -o json > \"$tmp_dir/pvcs.json\"",
"kubectl get pv -o json > \"$tmp_dir/pvs.json\"",
"kubectl -n \"$namespace\" get pod -o json > \"$tmp_dir/pods.json\"",
"NAMESPACE=\"$namespace\" PIPELINE_RUN_PREFIX=\"$pipeline_run_prefix\" MIN_AGE_MINUTES=\"$min_age_minutes\" LIMIT=\"$limit\" TMP_DIR=\"$tmp_dir\" node <<'NODE' > \"$tmp_dir/plan.json\"",
cleanupRunsPlanNodeScript(),
"NODE",
"if [ " + shQuote(options.confirm && !options.dryRun ? "true" : "false") + " != true ]; then",
" cat \"$tmp_dir/plan.json\"",
" exit 0",
"fi",
"node -e 'const fs=require(\"node:fs\"); const plan=JSON.parse(fs.readFileSync(process.argv[1],\"utf8\")); const names=Array.isArray(plan.selectedPipelineRuns)?plan.selectedPipelineRuns:[]; fs.writeFileSync(process.argv[2], names.join(\"\\n\") + (names.length>0?\"\\n\":\"\"));' \"$tmp_dir/plan.json\" \"$tmp_dir/selected-names.txt\"",
"delete_exit=0",
"if [ -s \"$tmp_dir/selected-names.txt\" ]; then",
" xargs -r kubectl -n \"$namespace\" delete pipelinerun --ignore-not-found=true --wait=true --timeout=\"${timeout_seconds}s\" < \"$tmp_dir/selected-names.txt\" > \"$tmp_dir/delete.out\" 2> \"$tmp_dir/delete.err\" || delete_exit=$?",
"else",
" : > \"$tmp_dir/delete.out\"",
" : > \"$tmp_dir/delete.err\"",
"fi",
"kubectl -n \"$namespace\" get pvc -o json > \"$tmp_dir/pvcs-after.json\"",
"DELETE_EXIT=\"$delete_exit\" TMP_DIR=\"$tmp_dir\" node <<'NODE'",
cleanupRunsFinalizeNodeScript(),
"NODE",
].join("\n");
}
function cleanupReleasedPvsScript(options: CleanupReleasedPvOptions, namespace: string): string {
return [
"set -eu",
`namespace=${shQuote(namespace)}`,
`limit=${String(options.limit)}`,
`timeout_seconds=${String(options.timeoutSeconds)}`,
"tmp_dir=$(mktemp -d)",
"trap 'rm -rf \"$tmp_dir\"' EXIT",
"kubectl get pv -o json > \"$tmp_dir/pvs.json\"",
"NAMESPACE=\"$namespace\" LIMIT=\"$limit\" TMP_DIR=\"$tmp_dir\" node <<'NODE' > \"$tmp_dir/plan.json\"",
cleanupReleasedPvsPlanNodeScript(),
"NODE",
"if [ " + shQuote(options.confirm && !options.dryRun ? "true" : "false") + " != true ]; then",
" cat \"$tmp_dir/plan.json\"",
" exit 0",
"fi",
"node -e 'const fs=require(\"node:fs\"); const plan=JSON.parse(fs.readFileSync(process.argv[1],\"utf8\")); const names=Array.isArray(plan.selectedPersistentVolumes)?plan.selectedPersistentVolumes:[]; fs.writeFileSync(process.argv[2], names.join(\"\\n\") + (names.length>0?\"\\n\":\"\"));' \"$tmp_dir/plan.json\" \"$tmp_dir/selected-pvs.txt\"",
"delete_exit=0",
"if [ -s \"$tmp_dir/selected-pvs.txt\" ]; then",
" xargs -r kubectl delete pv --ignore-not-found=true --wait=true --timeout=\"${timeout_seconds}s\" < \"$tmp_dir/selected-pvs.txt\" > \"$tmp_dir/delete.out\" 2> \"$tmp_dir/delete.err\" || delete_exit=$?",
"else",
" : > \"$tmp_dir/delete.out\"",
" : > \"$tmp_dir/delete.err\"",
"fi",
"kubectl get pv -o json > \"$tmp_dir/pvs-after.json\"",
"DELETE_EXIT=\"$delete_exit\" TMP_DIR=\"$tmp_dir\" node <<'NODE'",
cleanupReleasedPvsFinalizeNodeScript(),
"NODE",
].join("\n");
}
function yamlLaneSourceStatusScript(spec: AgentRunLaneSpec): string {
return [
"set +e",
`expected_workspace=${shQuote(spec.source.workspace)}`,
`source_branch=${shQuote(spec.source.branch)}`,
"workspace_exists=false",
"workspace_clean=null",
"local_head=null",
"branch=null",
"remote_url=null",
"remote_branch_exists=false",
"remote_branch_commit=null",
"status_short=''",
"if [ -d .git ] || git rev-parse --show-toplevel >/dev/null 2>&1; then",
" actual_workspace=$(pwd)",
" workspace_exists=true",
" branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || true)",
" remote_url=$(git remote get-url origin 2>/dev/null || true)",
" local_head=$(git rev-parse HEAD 2>/dev/null || true)",
" status_short=$(git status --short 2>/dev/null || true)",
" if [ -z \"$status_short\" ]; then workspace_clean=true; else workspace_clean=false; fi",
" git fetch origin \"$source_branch\" >/dev/null 2>&1 || true",
" remote_branch_commit=$(git rev-parse \"refs/remotes/origin/$source_branch\" 2>/dev/null || true)",
" if [ -n \"$remote_branch_commit\" ]; then remote_branch_exists=true; fi",
"else",
" actual_workspace=$(pwd)",
"fi",
"export expected_workspace source_branch workspace_exists workspace_clean local_head branch remote_url remote_branch_exists remote_branch_commit status_short actual_workspace",
"node <<'NODE'",
"function nullable(value) { return value && value !== 'null' ? value : null; }",
"function booleanValue(value) { if (value === 'true') return true; if (value === 'false') return false; return null; }",
"const env = process.env;",
"console.log(JSON.stringify({",
" ok: env.workspace_exists === 'true',",
" expectedWorkspace: env.expected_workspace,",
" actualWorkspace: env.actual_workspace || null,",
" workspaceExists: env.workspace_exists === 'true',",
" workspaceClean: booleanValue(env.workspace_clean),",
" branch: nullable(env.branch),",
" remoteUrl: nullable(env.remote_url),",
" localHead: nullable(env.local_head),",
" remoteBranch: env.source_branch,",
" remoteBranchExists: env.remote_branch_exists === 'true',",
" remoteBranchCommit: nullable(env.remote_branch_commit),",
" statusShort: nullable(env.status_short),",
" valuesPrinted: false",
"}));",
"NODE",
].join("\n");
}
function yamlLaneRuntimeStatusScript(spec: AgentRunLaneSpec, pipelineRun: string | null): string {
return [
"set +e",
`runtime_namespace=${shQuote(spec.runtime.namespace)}`,
`ci_namespace=${shQuote(spec.ci.namespace)}`,
`pipeline_name=${shQuote(spec.ci.pipeline)}`,
`pipeline_run=${pipelineRun === null ? "''" : shQuote(pipelineRun)}`,
`service_account=${shQuote(spec.ci.serviceAccountName)}`,
`argo_namespace=${shQuote(spec.gitops.argoNamespace)}`,
`argo_application=${shQuote(spec.gitops.argoApplication)}`,
`manager_deployment=${shQuote(spec.runtime.managerDeployment)}`,
`manager_service=${shQuote(spec.runtime.managerService)}`,
`database_secret=${shQuote(spec.database.secretRef.name)}`,
`database_key=${shQuote(spec.database.secretRef.key)}`,
`secrets_json=${shQuote(JSON.stringify(collectLaneSecretSources(spec).map((item) => item.targetRef)))}`,
"export runtime_namespace ci_namespace pipeline_name pipeline_run service_account argo_namespace argo_application manager_deployment manager_service database_secret database_key secrets_json",
"tmp_dir=$(mktemp -d)",
"trap 'rm -rf \"$tmp_dir\"' EXIT",
"kubectl get ns \"$runtime_namespace\" -o json > \"$tmp_dir/runtime-ns.json\" 2>/dev/null",
"runtime_ns_exit=$?",
"kubectl get ns \"$ci_namespace\" -o json > \"$tmp_dir/ci-ns.json\" 2>/dev/null",
"ci_ns_exit=$?",
"kubectl -n \"$ci_namespace\" get pipeline \"$pipeline_name\" -o json > \"$tmp_dir/pipeline.json\" 2>/dev/null",
"pipeline_exit=$?",
"kubectl -n \"$ci_namespace\" get serviceaccount \"$service_account\" -o json > \"$tmp_dir/serviceaccount.json\" 2>/dev/null",
"sa_exit=$?",
"if [ -n \"$pipeline_run\" ]; then kubectl -n \"$ci_namespace\" get pipelinerun \"$pipeline_run\" -o json > \"$tmp_dir/pipelinerun.json\" 2>/dev/null; pr_exit=$?; else pr_exit=2; fi",
"kubectl -n \"$argo_namespace\" get application \"$argo_application\" -o json > \"$tmp_dir/argo.json\" 2>/dev/null",
"argo_exit=$?",
"kubectl -n \"$runtime_namespace\" get deploy \"$manager_deployment\" -o json > \"$tmp_dir/manager-deploy.json\" 2>/dev/null",
"manager_deploy_exit=$?",
"kubectl -n \"$runtime_namespace\" get svc \"$manager_service\" -o json > \"$tmp_dir/manager-svc.json\" 2>/dev/null",
"manager_svc_exit=$?",
"kubectl -n \"$runtime_namespace\" get secret \"$database_secret\" -o json > \"$tmp_dir/db-secret.json\" 2>/dev/null",
"db_secret_exit=$?",
"SECRET_REFS_JSON=\"$secrets_json\" NODE_TMP=\"$tmp_dir\" node <<'NODE' > \"$tmp_dir/secrets.json\"",
"const fs = require('node:fs');",
"const cp = require('node:child_process');",
"const refs = JSON.parse(process.env.SECRET_REFS_JSON || '[]');",
"const result = [];",
"for (const ref of refs) {",
" const out = cp.spawnSync('kubectl', ['-n', ref.namespace, 'get', 'secret', ref.name, '-o', 'json'], { encoding: 'utf8' });",
" let keyPresent = false;",
" if (out.status === 0) {",
" try { keyPresent = Object.prototype.hasOwnProperty.call((JSON.parse(out.stdout).data || {}), ref.key); } catch {}",
" }",
" result.push({ namespace: ref.namespace, name: ref.name, key: ref.key, present: out.status === 0, keyPresent, valuesPrinted: false });",
"}",
"console.log(JSON.stringify({ ready: result.every((item) => item.present && item.keyPresent), count: result.length, items: result, valuesPrinted: false }));",
"NODE",
"kubectl -n \"$runtime_namespace\" get deploy,sts,svc,secret -o name > \"$tmp_dir/runtime-names.txt\" 2>/dev/null",
"NODE_TMP=\"$tmp_dir\" RUNTIME_NS_EXIT=\"$runtime_ns_exit\" CI_NS_EXIT=\"$ci_ns_exit\" PIPELINE_EXIT=\"$pipeline_exit\" SERVICE_ACCOUNT_EXIT=\"$sa_exit\" PIPELINERUN_EXIT=\"$pr_exit\" ARGO_EXIT=\"$argo_exit\" MANAGER_DEPLOY_EXIT=\"$manager_deploy_exit\" MANAGER_SVC_EXIT=\"$manager_svc_exit\" DB_SECRET_EXIT=\"$db_secret_exit\" node <<'NODE'",
"const fs = require('node:fs');",
"const path = require('node:path');",
"const dir = process.env.NODE_TMP;",
"function readJson(name) { try { return JSON.parse(fs.readFileSync(path.join(dir, name), 'utf8')); } catch { return null; } }",
"function exists(exitName) { return process.env[exitName] === '0'; }",
"function condition(obj) { return obj?.status?.conditions?.[0] || null; }",
"function envValue(deploy, name) { return deploy?.spec?.template?.spec?.containers?.[0]?.env?.find((entry) => entry.name === name)?.value || null; }",
"function pipelineRunParam(obj, name) { return (obj?.spec?.params || []).find((entry) => entry.name === name)?.value || null; }",
"const pipelineRun = readJson('pipelinerun.json');",
"const argo = readJson('argo.json');",
"const managerDeploy = readJson('manager-deploy.json');",
"const managerSvc = readJson('manager-svc.json');",
"const dbSecret = readJson('db-secret.json');",
"const secrets = readJson('secrets.json') || { ready: true, count: 0, items: [], valuesPrinted: false };",
"let names = ''; try { names = fs.readFileSync(path.join(dir, 'runtime-names.txt'), 'utf8'); } catch {}",
"const c = condition(pipelineRun);",
"console.log(JSON.stringify({",
" ok: exists('RUNTIME_NS_EXIT') && exists('CI_NS_EXIT'),",
" runtimeNamespaceExists: exists('RUNTIME_NS_EXIT'),",
" ciNamespaceExists: exists('CI_NS_EXIT'),",
" serviceAccountExists: exists('SERVICE_ACCOUNT_EXIT'),",
" pipeline: { exists: exists('PIPELINE_EXIT'), name: process.env.pipeline_name },",
" pipelineRun: { exists: exists('PIPELINERUN_EXIT'), name: process.env.pipeline_run || null, status: c?.status || null, reason: c?.reason || null, sourceCommit: pipelineRunParam(pipelineRun, 'revision'), startTime: pipelineRun?.status?.startTime || null, completionTime: pipelineRun?.status?.completionTime || null },",
" argo: { exists: exists('ARGO_EXIT'), namespace: process.env.argo_namespace, application: process.env.argo_application, revision: argo?.status?.sync?.revision || null, syncStatus: argo?.status?.sync?.status || null, healthStatus: argo?.status?.health?.status || null },",
" manager: { deploymentExists: exists('MANAGER_DEPLOY_EXIT'), serviceExists: exists('MANAGER_SVC_EXIT'), deployment: process.env.manager_deployment, service: process.env.manager_service, image: managerDeploy?.spec?.template?.spec?.containers?.[0]?.image || null, sourceCommit: envValue(managerDeploy, 'AGENTRUN_SOURCE_COMMIT'), servicePorts: Array.isArray(managerSvc?.spec?.ports) ? managerSvc.spec.ports.map((port) => ({ name: port.name || null, port: port.port || null, targetPort: port.targetPort || null })) : [] },",
" database: { secretPresent: exists('DB_SECRET_EXIT'), secretName: process.env.database_secret, key: process.env.database_key, keyPresent: Boolean(dbSecret?.data && Object.prototype.hasOwnProperty.call(dbSecret.data, process.env.database_key || '')) , valuesPrinted: false },",
" secrets,",
" localPostgres: { absent: !/postgres/i.test(names), matchingObjects: names.split(/\\r?\\n/).filter((line) => /postgres/i.test(line)).slice(0, 20) },",
" valuesPrinted: false",
"}));",
"NODE",
].join("\n");
}
function yamlLaneSourceBootstrapProbeScript(spec: AgentRunLaneSpec): string {
return [
"set +e",
`workspace=${shQuote(spec.source.workspace)}`,
`remote=${shQuote(spec.source.remote)}`,
`branch=${shQuote(spec.source.branch)}`,
"workspace_exists=false",
"remote_branch_exists=false",
"source_commit=null",
"if [ -d \"$workspace/.git\" ]; then",
" workspace_exists=true",
" cd \"$workspace\"",
" remote_branch=$(git ls-remote --heads \"$remote\" \"$branch\" 2>/dev/null | awk '{print $1}' | head -n 1)",
" if [ -n \"$remote_branch\" ]; then remote_branch_exists=true; source_commit=\"$remote_branch\"; else source_commit=$(git rev-parse HEAD 2>/dev/null || printf null); fi",
"else",
" remote_branch=$(git ls-remote --heads \"$remote\" \"$branch\" 2>/dev/null | awk '{print $1}' | head -n 1)",
" if [ -n \"$remote_branch\" ]; then remote_branch_exists=true; source_commit=\"$remote_branch\"; fi",
"fi",
"export workspace_exists remote_branch_exists source_commit",
"node <<'NODE'",
"console.log(JSON.stringify({ ok: true, workspaceExists: process.env.workspace_exists === 'true', remoteBranchExists: process.env.remote_branch_exists === 'true', sourceCommit: process.env.source_commit === 'null' ? null : process.env.source_commit, valuesPrinted: false }));",
"NODE",
].join("\n");
}
function yamlLaneSourceBootstrapSubmitScript(spec: AgentRunLaneSpec): string {
const bootstrap = spec.source.bootstrapFromBranch ?? spec.source.branch;
const stateDir = `/tmp/unidesk-agentrun-source-${spec.nodeId}-${spec.lane}`;
return [
"set -eu",
`workspace=${shQuote(spec.source.workspace)}`,
`remote=${shQuote(spec.source.remote)}`,
`branch=${shQuote(spec.source.branch)}`,
`bootstrap_branch=${shQuote(bootstrap)}`,
`state_dir=${shQuote(stateDir)}`,
"mkdir -p \"$state_dir\" \"$(dirname \"$workspace\")\"",
"job_id=\"source-bootstrap-$(date +%s)-$$\"",
"status_file=\"$state_dir/$job_id.json\"",
"stdout_file=\"$state_dir/$job_id.stdout.log\"",
"stderr_file=\"$state_dir/$job_id.stderr.log\"",
"cat > \"$status_file\" <<EOF",
"{\"ok\":false,\"status\":\"running\",\"jobId\":\"$job_id\",\"workspace\":\"$workspace\",\"branch\":\"$branch\",\"valuesPrinted\":false}",
"EOF",
"(",
" set -eu",
" write_failed_status() { code=$?; if [ \"$code\" -ne 0 ]; then tail_text=$(tail -n 60 \"$stderr_file\" 2>/dev/null | sed 's/\"/\\\\\"/g' | tr '\\n' ' ' | cut -c1-3000); CODE=\"$code\" ERROR_TAIL=\"$tail_text\" JOB_ID=\"$job_id\" WORKSPACE=\"$workspace\" BRANCH=\"$branch\" node <<'NODE' > \"$status_file\"",
"const code = Number(process.env.CODE || 1);",
"console.log(JSON.stringify({ ok: false, status: 'failed', exitCode: code, jobId: process.env.JOB_ID, workspace: process.env.WORKSPACE, branch: process.env.BRANCH, errorTail: process.env.ERROR_TAIL || null, valuesPrinted: false }));",
"NODE",
" fi; exit \"$code\"; }",
" trap write_failed_status EXIT",
" if [ -d \"$workspace/.git\" ] && git -C \"$workspace\" rev-parse --git-dir >/dev/null 2>&1; then",
" :",
" else",
" rm -rf \"$workspace\"",
" git clone --no-checkout \"$remote\" \"$workspace\"",
" fi",
" cd \"$workspace\"",
" git remote set-url origin \"$remote\" || git remote add origin \"$remote\"",
" git fetch origin \"$bootstrap_branch\" \"$branch\" || git fetch origin \"$bootstrap_branch\"",
" if git rev-parse --verify \"refs/remotes/origin/$branch^{commit}\" >/dev/null 2>&1; then",
" git checkout -B \"$branch\" \"refs/remotes/origin/$branch\"",
" else",
" git checkout -B \"$branch\" \"refs/remotes/origin/$bootstrap_branch\"",
" fi",
" if [ -f deploy/deploy.json ]; then rm deploy/deploy.json; fi",
" git add -A deploy/deploy.json 2>/dev/null || true",
" if ! git diff --quiet --cached -- deploy/deploy.json 2>/dev/null; then",
" git -c user.email=agentrun@unidesk.local -c user.name='UniDesk AgentRun Ops' commit -m 'chore: remove service deploy json truth'",
" fi",
" git push -u origin \"$branch\"",
" source_commit=$(git rev-parse HEAD)",
" status_short=$(git status --short)",
" SOURCE_COMMIT=\"$source_commit\" STATUS_SHORT=\"$status_short\" JOB_ID=\"$job_id\" WORKSPACE=\"$workspace\" BRANCH=\"$branch\" node <<'NODE' > \"$status_file\"",
"console.log(JSON.stringify({ ok: process.env.STATUS_SHORT === '', status: 'succeeded', jobId: process.env.JOB_ID, workspace: process.env.WORKSPACE, branch: process.env.BRANCH, sourceCommit: process.env.SOURCE_COMMIT, workspaceClean: process.env.STATUS_SHORT === '', statusShort: process.env.STATUS_SHORT || null, removedServiceDeployJson: true, valuesPrinted: false }));",
"NODE",
" trap - EXIT",
") >\"$stdout_file\" 2>\"$stderr_file\" &",
"pid=$!",
"JOB_PID=\"$pid\" JOB_ID=\"$job_id\" STATUS_FILE=\"$status_file\" STDOUT_FILE=\"$stdout_file\" STDERR_FILE=\"$stderr_file\" node <<'NODE'",
"console.log(JSON.stringify({ ok: true, status: 'submitted', jobId: process.env.JOB_ID, pid: Number(process.env.JOB_PID), statusFile: process.env.STATUS_FILE, stdoutFile: process.env.STDOUT_FILE, stderrFile: process.env.STDERR_FILE, valuesPrinted: false }));",
"NODE",
].join("\n");
}
async function waitForYamlLaneSourceBootstrap(config: UniDeskConfig, spec: AgentRunLaneSpec, jobId: string | null): Promise<Record<string, unknown> & { ok: boolean; payload: Record<string, unknown> }> {
if (jobId === null) return { ok: false, payload: { ok: false, degradedReason: "source-bootstrap-job-id-missing", valuesPrinted: false } };
const startedAt = Date.now();
const timeoutMs = spec.source.bootstrapTimeoutSeconds * 1000;
const pollMs = spec.source.bootstrapPollSeconds * 1000;
let lastPayload: Record<string, unknown> = {};
let polls = 0;
while (Date.now() - startedAt < timeoutMs) {
polls += 1;
const probe = await capture(config, spec.nodeRoute, ["sh", "--", yamlLaneSourceBootstrapStatusScript(spec, jobId)]);
const payload = captureJsonPayload(probe);
lastPayload = payload;
progressEvent("agentrun.yaml-lane.source-bootstrap.progress", {
node: spec.nodeId,
lane: spec.lane,
jobId,
polls,
status: stringOrNull(payload.status) ?? "unknown",
sourceCommit: stringOrNull(payload.sourceCommit),
elapsedMs: Date.now() - startedAt,
});
if (payload.status === "succeeded" && stringOrNull(payload.sourceCommit) !== null) return { ok: payload.ok === true, payload, polls, elapsedMs: Date.now() - startedAt };
if (payload.status === "failed") return { ok: false, payload, polls, elapsedMs: Date.now() - startedAt };
await sleep(pollMs);
}
return { ok: false, payload: { ...lastPayload, ok: false, status: "timeout", degradedReason: "source-bootstrap-timeout", valuesPrinted: false }, polls, elapsedMs: Date.now() - startedAt };
}
function yamlLaneSourceBootstrapStatusScript(spec: AgentRunLaneSpec, jobId: string): string {
const stateDir = `/tmp/unidesk-agentrun-source-${spec.nodeId}-${spec.lane}`;
return [
"set +e",
`status_file=${shQuote(`${stateDir}/${jobId}.json`)}`,
"if [ -f \"$status_file\" ]; then cat \"$status_file\"; else printf '{\"ok\":false,\"status\":\"missing\",\"valuesPrinted\":false}\\n'; fi",
].join("\n");
}
function yamlLaneSourceRestoreScript(spec: AgentRunLaneSpec): string {
return [
"set +e",
`workspace=${shQuote(spec.source.workspace)}`,
`branch=${shQuote(spec.source.branch)}`,
"tmp_dir=$(mktemp -d)",
"trap 'rm -rf \"$tmp_dir\"' EXIT",
"workspace_exists=false",
"if git -C \"$workspace\" rev-parse --git-dir >/dev/null 2>&1; then workspace_exists=true; fi",
"if [ \"$workspace_exists\" != true ]; then",
" WORKSPACE=\"$workspace\" BRANCH=\"$branch\" node <<'NODE'",
"console.log(JSON.stringify({ ok: false, status: 'skipped', failureKind: 'source-worktree-missing', workspace: process.env.WORKSPACE, branch: process.env.BRANCH, valuesPrinted: false }));",
"NODE",
" exit 0",
"fi",
"before_branch=$(git -C \"$workspace\" rev-parse --abbrev-ref HEAD 2>/dev/null || true)",
"before_head=$(git -C \"$workspace\" rev-parse HEAD 2>/dev/null || true)",
"status_short=$(git -C \"$workspace\" status --short 2>/dev/null || true)",
"if [ -n \"$status_short\" ]; then",
" WORKSPACE=\"$workspace\" BRANCH=\"$branch\" BEFORE_BRANCH=\"$before_branch\" BEFORE_HEAD=\"$before_head\" STATUS_SHORT=\"$status_short\" node <<'NODE'",
"console.log(JSON.stringify({ ok: false, status: 'skipped', failureKind: 'source-worktree-dirty', workspace: process.env.WORKSPACE, branch: process.env.BRANCH, before: { branch: process.env.BEFORE_BRANCH || null, head: process.env.BEFORE_HEAD || null, detached: process.env.BEFORE_BRANCH === 'HEAD' }, statusShort: process.env.STATUS_SHORT || null, valuesPrinted: false }));",
"NODE",
" exit 0",
"fi",
"git -C \"$workspace\" fetch origin \"$branch\" > \"$tmp_dir/fetch.out\" 2> \"$tmp_dir/fetch.err\"",
"fetch_exit=$?",
"remote_branch_commit=$(git -C \"$workspace\" rev-parse \"refs/remotes/origin/$branch\" 2>/dev/null || true)",
"checkout_exit=1",
"if [ \"$fetch_exit\" -eq 0 ] && [ -n \"$remote_branch_commit\" ]; then",
" git -C \"$workspace\" checkout -B \"$branch\" \"refs/remotes/origin/$branch\" > \"$tmp_dir/checkout.out\" 2> \"$tmp_dir/checkout.err\"",
" checkout_exit=$?",
"else",
" : > \"$tmp_dir/checkout.out\"",
" : > \"$tmp_dir/checkout.err\"",
"fi",
"after_branch=$(git -C \"$workspace\" rev-parse --abbrev-ref HEAD 2>/dev/null || true)",
"after_head=$(git -C \"$workspace\" rev-parse HEAD 2>/dev/null || true)",
"after_status_short=$(git -C \"$workspace\" status --short 2>/dev/null || true)",
"fetch_err=$(tail -n 20 \"$tmp_dir/fetch.err\" 2>/dev/null | tr '\\n' ' ' | cut -c1-1200)",
"checkout_err=$(tail -n 20 \"$tmp_dir/checkout.err\" 2>/dev/null | tr '\\n' ' ' | cut -c1-1200)",
"WORKSPACE=\"$workspace\" BRANCH=\"$branch\" BEFORE_BRANCH=\"$before_branch\" BEFORE_HEAD=\"$before_head\" FETCH_EXIT=\"$fetch_exit\" CHECKOUT_EXIT=\"$checkout_exit\" REMOTE_BRANCH_COMMIT=\"$remote_branch_commit\" AFTER_BRANCH=\"$after_branch\" AFTER_HEAD=\"$after_head\" AFTER_STATUS_SHORT=\"$after_status_short\" FETCH_ERR=\"$fetch_err\" CHECKOUT_ERR=\"$checkout_err\" node <<'NODE'",
"const fetchExit = Number(process.env.FETCH_EXIT || '1');",
"const checkoutExit = Number(process.env.CHECKOUT_EXIT || '1');",
"const afterClean = !process.env.AFTER_STATUS_SHORT;",
"const ok = fetchExit === 0 && checkoutExit === 0 && afterClean && process.env.AFTER_BRANCH === process.env.BRANCH;",
"let failureKind = null;",
"if (fetchExit !== 0) failureKind = 'source-branch-fetch-failed';",
"else if (!process.env.REMOTE_BRANCH_COMMIT) failureKind = 'source-branch-missing';",
"else if (checkoutExit !== 0) failureKind = 'source-branch-checkout-failed';",
"else if (!afterClean) failureKind = 'source-worktree-dirty-after-restore';",
"else if (process.env.AFTER_BRANCH !== process.env.BRANCH) failureKind = 'source-worktree-branch-mismatch';",
"console.log(JSON.stringify({",
" ok,",
" status: ok ? 'restored' : 'failed',",
" failureKind,",
" workspace: process.env.WORKSPACE,",
" branch: process.env.BRANCH,",
" before: { branch: process.env.BEFORE_BRANCH || null, head: process.env.BEFORE_HEAD || null, detached: process.env.BEFORE_BRANCH === 'HEAD' },",
" after: { branch: process.env.AFTER_BRANCH || null, head: process.env.AFTER_HEAD || null, clean: afterClean },",
" remoteBranchCommit: process.env.REMOTE_BRANCH_COMMIT || null,",
" fetch: { exitCode: fetchExit, stderrTail: process.env.FETCH_ERR || null },",
" checkout: { exitCode: checkoutExit, stderrTail: process.env.CHECKOUT_ERR || null },",
" valuesPrinted: false",
"}));",
"NODE",
].join("\n");
}
function yamlLaneBuildImageSubmitScript(spec: AgentRunLaneSpec, sourceCommit: string): string {
const build = spec.deployment.manager.imageBuild;
const noProxy = build.noProxy.join(",");
const imageRepository = `${spec.ci.registryPrefix}/${build.repository}`;
const stateDir = `/tmp/unidesk-agentrun-build-${spec.nodeId}-${spec.lane}`;
const buildArgs = Object.entries(build.buildArgs)
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, value]) => `${key}=${value}`);
const script = [
"set -eu",
`workspace=${shQuote(spec.source.workspace)}`,
`source_commit=${shQuote(sourceCommit)}`,
`state_dir=${shQuote(stateDir)}`,
`containerfile=${shQuote(build.containerfile)}`,
`context_dir=${shQuote(build.context)}`,
`image_repository=${shQuote(imageRepository)}`,
`network=${shQuote(build.network)}`,
`http_proxy_value=${build.httpProxy === null ? "''" : shQuote(build.httpProxy)}`,
`https_proxy_value=${build.httpsProxy === null ? "''" : shQuote(build.httpsProxy)}`,
`no_proxy_value=${shQuote(noProxy)}`,
`env_identity_files=${shQuote(JSON.stringify(build.envIdentityFiles))}`,
`build_args_json=${shQuote(JSON.stringify(buildArgs))}`,
"mkdir -p \"$state_dir\"",
"cd \"$workspace\"",
"git checkout \"$source_commit\"",
"env_identity=$(ENV_IDENTITY_FILES=\"$env_identity_files\" BUILD_ARGS_JSON=\"$build_args_json\" node <<'NODE'",
"const { createHash } = require('node:crypto');",
"const { readFileSync, existsSync } = require('node:fs');",
"const files = JSON.parse(process.env.ENV_IDENTITY_FILES || '[]');",
"const buildArgs = JSON.parse(process.env.BUILD_ARGS_JSON || '[]');",
"const hash = createHash('sha256');",
"for (const item of buildArgs) { hash.update('build-arg'); hash.update('\\0'); hash.update(item); hash.update('\\0'); }",
"for (const file of files) { hash.update(file); hash.update('\\0'); if (existsSync(file)) hash.update(readFileSync(file)); hash.update('\\0'); }",
"process.stdout.write(hash.digest('hex').slice(0, 24));",
"NODE",
")",
"source_short=$(printf '%s' \"$source_commit\" | cut -c1-12)",
"job_id=\"$source_short-$env_identity\"",
"status_file=\"$state_dir/$job_id.json\"",
"stdout_file=\"$state_dir/$job_id.stdout.log\"",
"stderr_file=\"$state_dir/$job_id.stderr.log\"",
"cat > \"$status_file\" <<EOF",
"{\"ok\":false,\"status\":\"running\",\"jobId\":\"$job_id\",\"sourceCommit\":\"$source_commit\",\"envIdentity\":\"$env_identity\",\"image\":\"$image_repository:$env_identity\",\"valuesPrinted\":false}",
"EOF",
"(",
" set -eu",
" write_failed_status() { code=$?; if [ \"$code\" -ne 0 ]; then tail_text=$(tail -n 40 \"$stderr_file\" 2>/dev/null | sed 's/\"/\\\\\"/g' | tr '\\n' ' ' | cut -c1-2000); CODE=\"$code\" ERROR_TAIL=\"$tail_text\" JOB_ID=\"$job_id\" SOURCE_COMMIT=\"$source_commit\" ENV_IDENTITY=\"$env_identity\" IMAGE_REPOSITORY=\"$image_repository\" node <<'NODE' > \"$status_file\"",
"const code = Number(process.env.CODE || 1);",
"console.log(JSON.stringify({ ok: false, status: 'failed', exitCode: code, jobId: process.env.JOB_ID, sourceCommit: process.env.SOURCE_COMMIT, envIdentity: process.env.ENV_IDENTITY, image: `${process.env.IMAGE_REPOSITORY}:${process.env.ENV_IDENTITY}`, errorTail: process.env.ERROR_TAIL || null, valuesPrinted: false }));",
"NODE",
" fi; exit \"$code\"; }",
" trap write_failed_status EXIT",
" cd \"$workspace\"",
" image=\"$image_repository:$env_identity\"",
" args=\"--network $network\"",
" if [ -n \"$http_proxy_value\" ]; then args=\"$args --build-arg HTTP_PROXY=$http_proxy_value --build-arg http_proxy=$http_proxy_value\"; fi",
" if [ -n \"$https_proxy_value\" ]; then args=\"$args --build-arg HTTPS_PROXY=$https_proxy_value --build-arg https_proxy=$https_proxy_value\"; fi",
" if [ -n \"$no_proxy_value\" ]; then args=\"$args --build-arg NO_PROXY=$no_proxy_value --build-arg no_proxy=$no_proxy_value\"; fi",
" build_arg_values=$(BUILD_ARGS_JSON=\"$build_args_json\" node <<'NODE'",
"const values = JSON.parse(process.env.BUILD_ARGS_JSON || '[]');",
"for (const value of values) console.log(value);",
"NODE",
" )",
" while IFS= read -r build_arg_value; do [ -n \"$build_arg_value\" ] && args=\"$args --build-arg $build_arg_value\"; done <<EOF",
"$build_arg_values",
"EOF",
" if docker image inspect \"$image\" >/dev/null 2>&1; then build_status=reused; else docker build $args -f \"$containerfile\" -t \"$image\" \"$context_dir\"; build_status=built; fi",
" manifest_accept='application/vnd.oci.image.index.v1+json, application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json'",
" fetch_manifest_digest() {",
" ref=\"$1\"",
" attempt=1",
" max_attempts=30",
" while [ \"$attempt\" -le \"$max_attempts\" ]; do",
" headers=$(mktemp)",
" if curl -fsSI -H \"Accept: $manifest_accept\" \"http://127.0.0.1:5000/v2/${image_repository#127.0.0.1:5000/}/manifests/$ref\" >\"$headers\"; then",
" found_digest=$(awk -F': ' 'tolower($1)==\"docker-content-digest\"{print $2}' \"$headers\" | tr -d '\\r' | head -n 1)",
" rm -f \"$headers\"",
" if [ -n \"$found_digest\" ]; then printf '%s' \"$found_digest\"; return 0; fi",
" else",
" rm -f \"$headers\"",
" fi",
" printf '{\"event\":\"agentrun-registry-manifest-probe\",\"status\":\"retrying\",\"ref\":\"%s\",\"attempt\":\"%s/%s\"}\\n' \"$ref\" \"$attempt\" \"$max_attempts\" >&2",
" sleep 1",
" attempt=$((attempt + 1))",
" done",
" printf '{\"event\":\"agentrun-registry-manifest-probe\",\"status\":\"exhausted\",\"ref\":\"%s\",\"attempt\":\"%s/%s\"}\\n' \"$ref\" \"$max_attempts\" \"$max_attempts\" >&2",
" return 1",
" }",
" docker push \"$image\"",
" digest=$(fetch_manifest_digest \"$env_identity\" || true)",
" if [ -n \"$digest\" ]; then verified_digest=$(fetch_manifest_digest \"$digest\" || true); if [ \"$verified_digest\" != \"$digest\" ]; then digest=; fi; fi",
" STATUS=\"$build_status\" DIGEST=\"$digest\" JOB_ID=\"$job_id\" SOURCE_COMMIT=\"$source_commit\" ENV_IDENTITY=\"$env_identity\" IMAGE_REPOSITORY=\"$image_repository\" node <<'NODE' > \"$status_file\"",
"const digest = process.env.DIGEST || null;",
"console.log(JSON.stringify({ ok: Boolean(digest), status: process.env.STATUS || 'built', jobId: process.env.JOB_ID, sourceCommit: process.env.SOURCE_COMMIT, envIdentity: process.env.ENV_IDENTITY, image: `${process.env.IMAGE_REPOSITORY}:${process.env.ENV_IDENTITY}`, digest, repositoryDigest: digest ? `${process.env.IMAGE_REPOSITORY}@${digest}` : null, valuesPrinted: false }));",
"NODE",
" trap - EXIT",
") >\"$stdout_file\" 2>\"$stderr_file\" &",
"pid=$!",
"JOB_PID=\"$pid\" JOB_ID=\"$job_id\" STATUS_FILE=\"$status_file\" STDOUT_FILE=\"$stdout_file\" STDERR_FILE=\"$stderr_file\" node <<'NODE'",
"console.log(JSON.stringify({ ok: true, status: 'submitted', jobId: process.env.JOB_ID, pid: Number(process.env.JOB_PID), statusFile: process.env.STATUS_FILE, stdoutFile: process.env.STDOUT_FILE, stderrFile: process.env.STDERR_FILE, valuesPrinted: false }));",
"NODE",
].join("\n");
return script;
}
async function waitForYamlLaneBuildImage(config: UniDeskConfig, spec: AgentRunLaneSpec, sourceCommit: string, jobId: string | null): Promise<Record<string, unknown> & { ok: boolean; payload: Record<string, unknown> }> {
if (jobId === null) return { ok: false, payload: { ok: false, degradedReason: "build-job-id-missing", valuesPrinted: false } };
const startedAt = Date.now();
const timeoutMs = Math.max(60, spec.deployment.manager.imageBuild.timeoutSeconds) * 1000;
let lastPayload: Record<string, unknown> = {};
let polls = 0;
while (Date.now() - startedAt < timeoutMs) {
polls += 1;
const probe = await capture(config, spec.nodeRoute, ["sh", "--", yamlLaneBuildImageStatusScript(spec, jobId)]);
const payload = captureJsonPayload(probe);
lastPayload = payload;
progressEvent("agentrun.yaml-lane.image-build.progress", {
node: spec.nodeId,
lane: spec.lane,
sourceCommit,
jobId,
polls,
status: stringOrNull(payload.status) ?? "unknown",
elapsedMs: Date.now() - startedAt,
});
if (payload.ok === true && stringOrNull(payload.digest) !== null) return { ok: true, payload, polls, elapsedMs: Date.now() - startedAt };
if (payload.status === "failed") return { ok: false, payload, polls, elapsedMs: Date.now() - startedAt };
await sleep(spec.deployment.manager.imageBuild.pollSeconds * 1000);
}
return { ok: false, payload: { ...lastPayload, ok: false, status: "timeout", degradedReason: "image-build-timeout", valuesPrinted: false }, polls, elapsedMs: Date.now() - startedAt };
}
function yamlLaneBuildImageStatusScript(spec: AgentRunLaneSpec, jobId: string): string {
const stateDir = `/tmp/unidesk-agentrun-build-${spec.nodeId}-${spec.lane}`;
return [
"set +e",
`status_file=${shQuote(`${stateDir}/${jobId}.json`)}`,
`stdout_file=${shQuote(`${stateDir}/${jobId}.stdout.log`)}`,
`stderr_file=${shQuote(`${stateDir}/${jobId}.stderr.log`)}`,
"if [ -f \"$status_file\" ]; then cat \"$status_file\"; else printf '{\"ok\":false,\"status\":\"missing\",\"valuesPrinted\":false}\\n'; fi",
"if [ -f \"$stderr_file\" ] && tail -n 20 \"$stderr_file\" | grep -Eq 'ERROR|error|failed|denied'; then :; fi",
].join("\n");
}
function yamlLaneGitopsPublishSubmitScript(spec: AgentRunLaneSpec, files: readonly { path: string; content: string }[]): string {
const stateDir = `/tmp/unidesk-agentrun-gitops-${spec.nodeId}-${spec.lane}`;
const filesB64 = Buffer.from(JSON.stringify(files.map((file) => ({
path: file.path,
contentBase64: Buffer.from(file.content, "utf8").toString("base64"),
}))), "utf8").toString("base64");
return [
"set -eu",
`state_dir=${shQuote(stateDir)}`,
`remote=${shQuote(spec.source.remote)}`,
`gitops_branch=${shQuote(spec.gitops.branch)}`,
`gitops_root=${shQuote(spec.deployment.gitopsRoot)}`,
`artifact_catalog=${shQuote(spec.deployment.artifactCatalogPath)}`,
`files_b64=${shQuote(filesB64)}`,
"mkdir -p \"$state_dir\"",
"job_id=\"gitops-publish-$(date +%s)-$$\"",
"status_file=\"$state_dir/$job_id.json\"",
"stdout_file=\"$state_dir/$job_id.stdout.log\"",
"stderr_file=\"$state_dir/$job_id.stderr.log\"",
"publish_root=\"$state_dir/worktrees\"",
"publish_worktree=\"$publish_root/$job_id\"",
"cat > \"$status_file\" <<EOF",
"{\"ok\":false,\"status\":\"running\",\"jobId\":\"$job_id\",\"publishWorkspace\":\"$publish_worktree\",\"gitopsBranch\":\"$gitops_branch\",\"valuesPrinted\":false}",
"EOF",
"(",
" set -eu",
" write_failed_status() { code=$?; if [ \"$code\" -ne 0 ]; then tail_text=$(tail -n 80 \"$stderr_file\" 2>/dev/null | sed 's/\"/\\\\\"/g' | tr '\\n' ' ' | cut -c1-4000); CODE=\"$code\" ERROR_TAIL=\"$tail_text\" JOB_ID=\"$job_id\" PUBLISH_WORKSPACE=\"$publish_worktree\" GITOPS_BRANCH=\"$gitops_branch\" node <<'NODE' > \"$status_file\"",
"const code = Number(process.env.CODE || 1);",
"console.log(JSON.stringify({ ok: false, status: 'failed', exitCode: code, jobId: process.env.JOB_ID, publishWorkspace: process.env.PUBLISH_WORKSPACE, gitopsBranch: process.env.GITOPS_BRANCH, errorTail: process.env.ERROR_TAIL || null, valuesPrinted: false }));",
"NODE",
" fi; exit \"$code\"; }",
" trap write_failed_status EXIT",
" mkdir -p \"$publish_root\"",
" rm -rf \"$publish_worktree\"",
" git clone --no-checkout \"$remote\" \"$publish_worktree\"",
" cd \"$publish_worktree\"",
" git fetch origin \"$gitops_branch\" || true",
" if git rev-parse --verify \"refs/remotes/origin/$gitops_branch^{commit}\" >/dev/null 2>&1; then",
" git checkout -B \"$gitops_branch\" \"refs/remotes/origin/$gitops_branch\"",
" else",
" git checkout --orphan \"$gitops_branch\"",
" git rm -rf . >/dev/null 2>&1 || true",
" fi",
" git rm -rf --ignore-unmatch \"$gitops_root\" \"$artifact_catalog\" source.json >/dev/null 2>&1 || true",
" rm -rf \"$gitops_root\" \"$artifact_catalog\" source.json",
" FILES_B64=\"$files_b64\" node <<'NODE'",
"const fs = require('node:fs');",
"const path = require('node:path');",
"const files = JSON.parse(Buffer.from(process.env.FILES_B64 || '', 'base64').toString('utf8'));",
"for (const file of files) {",
" const target = path.resolve(process.cwd(), file.path);",
" if (!target.startsWith(process.cwd() + path.sep)) throw new Error(`refuse path outside workspace: ${file.path}`);",
" fs.mkdirSync(path.dirname(target), { recursive: true });",
" fs.writeFileSync(target, Buffer.from(file.contentBase64, 'base64'));",
"}",
"NODE",
" git add source.json \"$artifact_catalog\" \"$gitops_root\"",
" if git diff --quiet --cached; then changed=false; else changed=true; git -c user.email=agentrun@unidesk.local -c user.name='UniDesk AgentRun Ops' commit -m \"deploy: render AgentRun ${gitops_branch} from UniDesk YAML\"; fi",
" git push -u origin \"$gitops_branch\"",
" gitops_commit=$(git rev-parse HEAD)",
" rm -rf \"$publish_worktree\"",
" CHANGED=\"$changed\" GITOPS_BRANCH=\"$gitops_branch\" GITOPS_COMMIT=\"$gitops_commit\" FILE_COUNT=\"" + String(files.length) + "\" JOB_ID=\"$job_id\" node <<'NODE' > \"$status_file\"",
"console.log(JSON.stringify({ ok: true, status: 'succeeded', jobId: process.env.JOB_ID, changed: process.env.CHANGED === 'true', gitopsBranch: process.env.GITOPS_BRANCH, gitopsCommit: process.env.GITOPS_COMMIT, fileCount: Number(process.env.FILE_COUNT || 0), valuesPrinted: false }));",
"NODE",
" trap - EXIT",
") >\"$stdout_file\" 2>\"$stderr_file\" &",
"pid=$!",
"JOB_PID=\"$pid\" JOB_ID=\"$job_id\" STATUS_FILE=\"$status_file\" STDOUT_FILE=\"$stdout_file\" STDERR_FILE=\"$stderr_file\" node <<'NODE'",
"console.log(JSON.stringify({ ok: true, status: 'submitted', jobId: process.env.JOB_ID, pid: Number(process.env.JOB_PID), statusFile: process.env.STATUS_FILE, stdoutFile: process.env.STDOUT_FILE, stderrFile: process.env.STDERR_FILE, valuesPrinted: false }));",
"NODE",
].join("\n");
}
async function waitForYamlLaneGitopsPublish(config: UniDeskConfig, spec: AgentRunLaneSpec, sourceCommit: string, jobId: string | null): Promise<Record<string, unknown> & { ok: boolean; payload: Record<string, unknown> }> {
if (jobId === null) return { ok: false, payload: { ok: false, degradedReason: "gitops-publish-job-id-missing", valuesPrinted: false } };
const startedAt = Date.now();
const timeoutMs = 300_000;
let lastPayload: Record<string, unknown> = {};
let polls = 0;
while (Date.now() - startedAt < timeoutMs) {
polls += 1;
const probe = await capture(config, spec.nodeRoute, ["sh", "--", yamlLaneGitopsPublishStatusScript(spec, jobId)]);
const payload = captureJsonPayload(probe);
lastPayload = payload;
progressEvent("agentrun.yaml-lane.gitops-publish.progress", {
node: spec.nodeId,
lane: spec.lane,
sourceCommit,
jobId,
polls,
status: stringOrNull(payload.status) ?? "unknown",
gitopsCommit: stringOrNull(payload.gitopsCommit),
elapsedMs: Date.now() - startedAt,
});
if (payload.ok === true && stringOrNull(payload.gitopsCommit) !== null) return { ok: true, payload, polls, elapsedMs: Date.now() - startedAt };
if (payload.status === "failed") return { ok: false, payload, polls, elapsedMs: Date.now() - startedAt };
await sleep(5_000);
}
return { ok: false, payload: { ...lastPayload, ok: false, status: "timeout", degradedReason: "gitops-publish-timeout", valuesPrinted: false }, polls, elapsedMs: Date.now() - startedAt };
}
function yamlLaneGitopsPublishStatusScript(spec: AgentRunLaneSpec, jobId: string): string {
const stateDir = `/tmp/unidesk-agentrun-gitops-${spec.nodeId}-${spec.lane}`;
return [
"set +e",
`status_file=${shQuote(`${stateDir}/${jobId}.json`)}`,
"if [ -f \"$status_file\" ]; then cat \"$status_file\"; else printf '{\"ok\":false,\"status\":\"missing\",\"valuesPrinted\":false}\\n'; fi",
].join("\n");
}
const AGENTRUN_GIT_MIRROR_RETRY_MAX_ATTEMPTS = 5;
const AGENTRUN_GIT_MIRROR_RETRYABLE_PATTERN =
/(kex_exchange_identification|connection closed by remote host|connection closed by unknown|could not read from remote repository|ssh\.github\.com|github\.com|fetch-pack|early eof|econnreset|read econnreset|proxy-connect|tunnel socket error)/i;
function agentRunGitMirrorRetryDelayMs(attempt: number): number {
return Math.min(15_000, 1_000 * Math.pow(2, Math.max(0, attempt - 1)));
}
function agentRunGitMirrorFailureText(value: unknown): string {
try {
return JSON.stringify(value);
} catch {
return String(value);
}
}
function agentRunGitMirrorRetryableFailure(
spec: AgentRunLaneSpec,
action: "sync" | "flush",
attempt: number,
value: unknown,
): Record<string, unknown> | null {
const text = agentRunGitMirrorFailureText(value);
if (!AGENTRUN_GIT_MIRROR_RETRYABLE_PATTERN.test(text)) return null;
const retryExhausted = attempt >= AGENTRUN_GIT_MIRROR_RETRY_MAX_ATTEMPTS;
const retryBackoffMs = retryExhausted ? 0 : agentRunGitMirrorRetryDelayMs(attempt);
return {
retryable: true,
retryAttempt: attempt,
retryMaxAttempts: AGENTRUN_GIT_MIRROR_RETRY_MAX_ATTEMPTS,
retryLabel: `${attempt}/${AGENTRUN_GIT_MIRROR_RETRY_MAX_ATTEMPTS}`,
retryExhausted,
retryBackoffMs,
backoffPolicy: "exponential",
failureKind: "git-mirror-upstream-transient",
upstream: "git-mirror-ssh",
action,
node: spec.nodeId,
lane: spec.lane,
message: retryExhausted
? "AgentRun git-mirror upstream transient failure exhausted retries; stopping instead of continuing silently."
: "AgentRun git-mirror upstream transient failure; retrying with exponential backoff.",
next: retryExhausted
? { stopped: true, reason: "retry-exhausted" }
: {
retry: `bun scripts/cli.ts agentrun git-mirror ${action} --node ${spec.nodeId} --lane ${spec.lane} --confirm --wait`,
delayMs: retryBackoffMs,
},
valuesPrinted: false,
};
}
function agentRunGitMirrorRetrySummary(attempts: Record<string, unknown>[]): Record<string, unknown> {
const last = attempts.length > 0 ? record(attempts[attempts.length - 1]) : {};
return {
policy: "exponential",
maxAttempts: AGENTRUN_GIT_MIRROR_RETRY_MAX_ATTEMPTS,
attempts,
exhausted: last.retryExhausted === true,
stopped: last.retryExhausted === true,
valuesPrinted: false,
};
}
async function runYamlLaneGitMirrorSyncJob(config: UniDeskConfig, spec: AgentRunLaneSpec): Promise<Record<string, unknown>> {
const attempts: Record<string, unknown>[] = [];
for (let attempt = 1; attempt <= AGENTRUN_GIT_MIRROR_RETRY_MAX_ATTEMPTS; attempt += 1) {
const jobName = `${spec.gitMirror.syncJobPrefix}-${Date.now().toString(36)}-${attempt}`.slice(0, 63);
const manifest = yamlLaneGitMirrorJobManifest(spec, "sync", jobName);
const created = await capture(config, spec.nodeKubeRoute, ["sh", "--", createYamlLaneJobScript(spec.gitMirror.namespace, jobName, manifest)]);
if (created.exitCode !== 0) {
const result = { ok: false, phase: "create-job", jobName, capture: compactCapture(created, { full: true, stdoutTailChars: 4000, stderrTailChars: 4000 }), valuesPrinted: false };
const retryableFailure = agentRunGitMirrorRetryableFailure(spec, "sync", attempt, result);
attempts.push({ attempt, retryLabel: `${attempt}/${AGENTRUN_GIT_MIRROR_RETRY_MAX_ATTEMPTS}`, jobName, ok: false, phase: "create-job", retryableFailure, retryExhausted: retryableFailure !== null && record(retryableFailure).retryExhausted === true });
if (retryableFailure !== null && record(retryableFailure).retryExhausted !== true) {
progressEvent("agentrun.yaml-lane.git-mirror.retry", retryableFailure);
await sleep(agentRunGitMirrorRetryDelayMs(attempt));
continue;
}
return { ...result, retryableFailure, retry: agentRunGitMirrorRetrySummary(attempts) };
}
const startedAt = Date.now();
let polls = 0;
let lastProbe: SshCaptureResult | null = null;
while (Date.now() - startedAt < 300_000) {
polls += 1;
lastProbe = await capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneJobProbeScript(spec.gitMirror.namespace, jobName)]);
const payload = captureJsonPayload(lastProbe);
progressEvent("agentrun.yaml-lane.git-mirror.progress", {
node: spec.nodeId,
lane: spec.lane,
jobName,
polls,
retryAttempt: attempt,
retryMaxAttempts: AGENTRUN_GIT_MIRROR_RETRY_MAX_ATTEMPTS,
retryLabel: `${attempt}/${AGENTRUN_GIT_MIRROR_RETRY_MAX_ATTEMPTS}`,
succeeded: payload.succeeded === true,
failed: payload.failed === true,
elapsedMs: Date.now() - startedAt,
});
if (payload.succeeded === true) {
attempts.push({ attempt, retryLabel: `${attempt}/${AGENTRUN_GIT_MIRROR_RETRY_MAX_ATTEMPTS}`, jobName, ok: true, polls, elapsedMs: Date.now() - startedAt });
return { ok: true, jobName, polls, elapsedMs: Date.now() - startedAt, result: payload, retry: agentRunGitMirrorRetrySummary(attempts), valuesPrinted: false };
}
if (payload.failed === true) {
const result = { ok: false, jobName, polls, elapsedMs: Date.now() - startedAt, result: payload, capture: compactCapture(lastProbe, { full: true, stdoutTailChars: 5000, stderrTailChars: 3000 }), valuesPrinted: false };
const retryableFailure = agentRunGitMirrorRetryableFailure(spec, "sync", attempt, result);
attempts.push({ attempt, retryLabel: `${attempt}/${AGENTRUN_GIT_MIRROR_RETRY_MAX_ATTEMPTS}`, jobName, ok: false, polls, elapsedMs: Date.now() - startedAt, retryableFailure, retryExhausted: retryableFailure !== null && record(retryableFailure).retryExhausted === true });
if (retryableFailure !== null && record(retryableFailure).retryExhausted !== true) {
progressEvent("agentrun.yaml-lane.git-mirror.retry", retryableFailure);
await sleep(agentRunGitMirrorRetryDelayMs(attempt));
break;
}
return { ...result, retryableFailure, retry: agentRunGitMirrorRetrySummary(attempts) };
}
await sleep(5_000);
}
if (attempts.length > 0 && record(attempts[attempts.length - 1]).jobName === jobName) continue;
const result = { ok: false, jobName, polls, elapsedMs: Date.now() - startedAt, degradedReason: "git-mirror-sync-job-timeout", capture: lastProbe === null ? null : compactCapture(lastProbe, { full: true, stdoutTailChars: 5000, stderrTailChars: 3000 }), valuesPrinted: false };
const retryableFailure = agentRunGitMirrorRetryableFailure(spec, "sync", attempt, result);
attempts.push({ attempt, retryLabel: `${attempt}/${AGENTRUN_GIT_MIRROR_RETRY_MAX_ATTEMPTS}`, jobName, ok: false, polls, elapsedMs: Date.now() - startedAt, degradedReason: "git-mirror-sync-job-timeout", retryableFailure, retryExhausted: retryableFailure !== null && record(retryableFailure).retryExhausted === true });
if (retryableFailure !== null && record(retryableFailure).retryExhausted !== true) {
progressEvent("agentrun.yaml-lane.git-mirror.retry", retryableFailure);
await sleep(agentRunGitMirrorRetryDelayMs(attempt));
continue;
}
return { ...result, retryableFailure, retry: agentRunGitMirrorRetrySummary(attempts) };
}
return { ok: false, degradedReason: "git-mirror-sync-retry-loop-exhausted", retry: agentRunGitMirrorRetrySummary(attempts), valuesPrinted: false };
}
function yamlLanePipelineRunCreateScript(spec: AgentRunLaneSpec, sourceCommit: string, pipelineRun: string): string {
const manifest = {
apiVersion: "tekton.dev/v1",
kind: "PipelineRun",
metadata: {
name: pipelineRun,
namespace: spec.ci.namespace,
labels: {
"app.kubernetes.io/part-of": "agentrun",
"agentrun.pikastech.local/lane": spec.version,
"agentrun.pikastech.local/node": spec.nodeId,
"agentrun.pikastech.local/source-commit": sourceCommit,
"agentrun.pikastech.local/trigger": "unidesk-yaml-only",
},
},
spec: {
pipelineRef: { name: spec.ci.pipeline },
taskRunTemplate: {
serviceAccountName: spec.ci.serviceAccountName,
podTemplate: { hostNetwork: true, dnsPolicy: "ClusterFirstWithHostNet", securityContext: { fsGroup: 1000 } },
},
params: [
{ name: "git-url", value: spec.source.remote },
{ name: "git-read-url", value: spec.gitMirror.readUrl },
{ name: "git-write-url", value: spec.gitMirror.writeUrl },
{ name: "source-branch", value: spec.source.branch },
{ name: "gitops-branch", value: spec.gitops.branch },
{ name: "revision", value: sourceCommit },
{ name: "registry-prefix", value: spec.ci.registryPrefix },
{ name: "tools-image", value: spec.ci.toolsImage },
],
workspaces: [
{ name: "source", volumeClaimTemplate: { spec: { accessModes: ["ReadWriteOnce"], resources: { requests: { storage: "4Gi" } } } } },
{ name: "git-ssh", secret: { secretName: spec.gitMirror.sshSecretName } },
],
},
};
const manifestB64 = Buffer.from(JSON.stringify(manifest), "utf8").toString("base64");
return [
"set -eu",
`namespace=${shQuote(spec.ci.namespace)}`,
`pipeline_run=${shQuote(pipelineRun)}`,
`manifest_b64=${shQuote(manifestB64)}`,
"tmp=$(mktemp)",
"trap 'rm -f \"$tmp\"' EXIT",
"printf '%s' \"$manifest_b64\" | base64 -d > \"$tmp\"",
"existing_status=$(kubectl -n \"$namespace\" get pipelinerun \"$pipeline_run\" -o 'jsonpath={.status.conditions[0].status}' 2>/dev/null || true)",
"if [ \"$existing_status\" = False ]; then kubectl -n \"$namespace\" delete pipelinerun \"$pipeline_run\" --ignore-not-found=true --wait=false >/dev/null 2>&1 || true; fi",
"if kubectl -n \"$namespace\" get pipelinerun \"$pipeline_run\" >/dev/null 2>&1; then created=false; else kubectl create -f \"$tmp\"; created=true; fi",
"status=$(kubectl -n \"$namespace\" get pipelinerun \"$pipeline_run\" -o 'jsonpath={.status.conditions[0].status}' 2>/dev/null || true)",
"CREATED=\"$created\" STATUS=\"$status\" PIPELINE_RUN=\"$pipeline_run\" node <<'NODE'",
"console.log(JSON.stringify({ ok: true, pipelineRun: process.env.PIPELINE_RUN, created: process.env.CREATED === 'true', status: process.env.STATUS || null, valuesPrinted: false }));",
"NODE",
].join("\n");
}
function createYamlLaneJobScript(namespace: string, jobName: string, manifest: Record<string, unknown>): string {
const manifestB64 = Buffer.from(JSON.stringify(manifest), "utf8").toString("base64");
return [
"set -eu",
`namespace=${shQuote(namespace)}`,
`job=${shQuote(jobName)}`,
`manifest_b64=${shQuote(manifestB64)}`,
"tmp=$(mktemp)",
"trap 'rm -f \"$tmp\"' EXIT",
"printf '%s' \"$manifest_b64\" | base64 -d > \"$tmp\"",
"kubectl -n \"$namespace\" delete job \"$job\" --ignore-not-found=true --wait=false >/dev/null 2>&1 || true",
"kubectl create -f \"$tmp\"",
"JOB=\"$job\" node <<'NODE'",
"console.log(JSON.stringify({ ok: true, jobName: process.env.JOB, valuesPrinted: false }));",
"NODE",
].join("\n");
}
function yamlLaneJobProbeScript(namespace: string, jobName: string): string {
return [
"set +e",
`namespace=${shQuote(namespace)}`,
`job=${shQuote(jobName)}`,
"kubectl -n \"$namespace\" get job \"$job\" -o json > /tmp/agentrun-job.json 2>/dev/null",
"job_exit=$?",
"kubectl -n \"$namespace\" logs \"job/$job\" --tail=120 > /tmp/agentrun-job.log 2>/dev/null",
"JOB_EXIT=\"$job_exit\" JOB=\"$job\" node <<'NODE'",
"const fs = require('node:fs');",
"let job = null; try { job = JSON.parse(fs.readFileSync('/tmp/agentrun-job.json', 'utf8')); } catch {}",
"let log = ''; try { log = fs.readFileSync('/tmp/agentrun-job.log', 'utf8'); } catch {}",
"const succeeded = Number(job?.status?.succeeded || 0) > 0;",
"const failed = Number(job?.status?.failed || 0) > 0;",
"console.log(JSON.stringify({ ok: process.env.JOB_EXIT === '0', jobName: process.env.JOB, succeeded, failed, active: job?.status?.active || 0, logsTail: log.slice(-4000), valuesPrinted: false }));",
"NODE",
].join("\n");
}
function yamlLaneGitMirrorCacheVolume(spec: AgentRunLaneSpec): Record<string, unknown> {
if (spec.gitMirror.cacheHostPath !== null) {
return { name: "cache", hostPath: { path: spec.gitMirror.cacheHostPath, type: "DirectoryOrCreate" } };
}
return { name: "cache", persistentVolumeClaim: { claimName: spec.gitMirror.cachePvc } };
}
function yamlLaneGitMirrorJobManifest(spec: AgentRunLaneSpec, action: "sync" | "flush", name: string): Record<string, unknown> {
const proxyHostNeedsHostNetwork = spec.gitMirror.githubProxy.host === "127.0.0.1" || spec.gitMirror.githubProxy.host === "localhost";
return {
apiVersion: "batch/v1",
kind: "Job",
metadata: {
name,
namespace: spec.gitMirror.namespace,
labels: {
"app.kubernetes.io/name": "git-mirror",
"app.kubernetes.io/part-of": "agentrun",
"agentrun.pikastech.local/lane": spec.version,
"agentrun.pikastech.local/node": spec.nodeId,
"agentrun.pikastech.local/component": action,
},
},
spec: {
backoffLimit: 0,
activeDeadlineSeconds: 600,
ttlSecondsAfterFinished: 3600,
template: {
metadata: {
labels: {
"app.kubernetes.io/name": "git-mirror",
"app.kubernetes.io/part-of": "agentrun",
"agentrun.pikastech.local/lane": spec.version,
"agentrun.pikastech.local/node": spec.nodeId,
"agentrun.pikastech.local/component": action,
},
},
spec: {
restartPolicy: "Never",
...(proxyHostNeedsHostNetwork ? { hostNetwork: true, dnsPolicy: "ClusterFirstWithHostNet" } : {}),
volumes: [
yamlLaneGitMirrorCacheVolume(spec),
{ name: "git-ssh", secret: { secretName: spec.gitMirror.sshSecretName, defaultMode: 0o400 } },
],
containers: [{
name: action,
image: spec.gitMirror.toolsImage,
imagePullPolicy: "IfNotPresent",
command: ["/bin/sh", "-ec", action === "sync" ? yamlLaneGitMirrorSyncShell(spec) : yamlLaneGitMirrorFlushShell(spec)],
volumeMounts: [
{ name: "cache", mountPath: "/cache" },
{ name: "git-ssh", mountPath: "/git-ssh", readOnly: true },
],
}],
},
},
},
};
}
function yamlLaneGitMirrorSshSetupShellLines(spec: AgentRunLaneSpec): string[] {
const proxyHost = spec.gitMirror.githubProxy.host;
const proxyPort = spec.gitMirror.githubProxy.port;
const proxyUrl = `http://${proxyHost}:${proxyPort}`;
const proxySummary = `agentrun git-mirror-egress-proxy node=${spec.nodeId} lane=${spec.lane} host=${proxyHost} port=${proxyPort} ssh=GIT_SSH-wrapper source=yaml`;
const proxyCommand = `ProxyCommand=node /tmp/agentrun-github-proxy-connect.cjs ${proxyHost} ${proxyPort} %h %p`;
return [
"mkdir -p /root/.ssh",
"cp /git-ssh/ssh-privatekey /root/.ssh/id_rsa",
"chmod 0400 /root/.ssh/id_rsa",
`printf '%s\\n' ${shQuote(proxySummary)} >&2`,
"cat > /tmp/agentrun-github-proxy-connect.cjs <<'NODE_PROXY'",
"#!/usr/bin/env node",
"const net = require('node:net');",
"const [proxyHost, proxyPortRaw, targetHost, targetPortRaw] = process.argv.slice(2);",
"const proxyPort = Number.parseInt(proxyPortRaw || '', 10);",
"const targetPort = Number.parseInt(targetPortRaw || '', 10);",
"if (!proxyHost || !Number.isInteger(proxyPort) || !targetHost || !Number.isInteger(targetPort)) {",
" console.error('agentrun git-mirror proxy-connect: invalid ProxyCommand arguments');",
" process.exit(64);",
"}",
"let settled = false;",
"let tunnelEstablished = false;",
"function finish(code, message) {",
" if (settled) return;",
" settled = true;",
" if (message) console.error('agentrun git-mirror proxy-connect: ' + message);",
" process.exit(code);",
"}",
"const socket = net.createConnection({ host: proxyHost, port: proxyPort });",
"let buffer = Buffer.alloc(0);",
"socket.setTimeout(15000, () => { socket.destroy(); finish(65, 'timeout connecting via ' + proxyHost + ':' + proxyPort + ' to ' + targetHost + ':' + targetPort); });",
"socket.on('connect', () => socket.write('CONNECT ' + targetHost + ':' + targetPort + ' HTTP/1.1\\r\\nHost: ' + targetHost + ':' + targetPort + '\\r\\nProxy-Connection: Keep-Alive\\r\\n\\r\\n'));",
"socket.on('error', (error) => finish(tunnelEstablished ? 69 : 66, (tunnelEstablished ? 'tunnel socket error: ' : 'tcp error connecting to proxy: ') + (error && error.message ? error.message : String(error))));",
"socket.on('close', () => { if (!tunnelEstablished) finish(68, 'proxy closed before CONNECT completed via ' + proxyHost + ':' + proxyPort + ' to ' + targetHost + ':' + targetPort); else finish(0); });",
"function onData(chunk) {",
" buffer = Buffer.concat([buffer, chunk]);",
" const headerEnd = buffer.indexOf('\\r\\n\\r\\n');",
" if (headerEnd === -1 && buffer.length < 8192) return;",
" if (headerEnd === -1) { socket.destroy(); finish(68, 'proxy response header exceeded 8192 bytes before CONNECT status via ' + proxyHost + ':' + proxyPort + ' to ' + targetHost + ':' + targetPort); return; }",
" const head = buffer.slice(0, headerEnd + 4).toString('latin1');",
" const statusLine = head.split('\\r\\n', 1)[0] || '';",
" const statusCode = Number.parseInt(statusLine.split(' ')[1] || '', 10);",
" if (!statusLine.startsWith('HTTP/1.') || !Number.isInteger(statusCode) || statusCode < 200 || statusCode > 299) {",
" const safeStatus = statusLine.replace(/[^\\x20-\\x7e]/g, '?').slice(0, 160);",
" socket.destroy();",
" finish(67, 'proxy CONNECT failed via ' + proxyHost + ':' + proxyPort + ' to ' + targetHost + ':' + targetPort + ': ' + safeStatus);",
" return;",
" }",
" socket.off('data', onData);",
" socket.setTimeout(0);",
" tunnelEstablished = true;",
" const rest = buffer.slice(headerEnd + 4);",
" if (rest.length) process.stdout.write(rest);",
" process.stdin.on('error', () => {});",
" process.stdout.on('error', () => {});",
" process.stdin.pipe(socket);",
" socket.pipe(process.stdout);",
"}",
"socket.on('data', onData);",
"NODE_PROXY",
"chmod 0700 /tmp/agentrun-github-proxy-connect.cjs",
"cat > /tmp/agentrun-git-ssh-proxy.sh <<'SH_PROXY'",
"#!/bin/sh",
`exec ssh -i /root/.ssh/id_rsa -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/root/.ssh/known_hosts -o ConnectTimeout=15 -o ServerAliveInterval=5 -o ServerAliveCountMax=1 -o ${shQuote(proxyCommand)} "$@"`,
"SH_PROXY",
"chmod 0700 /tmp/agentrun-git-ssh-proxy.sh",
`export HTTP_PROXY=${shQuote(proxyUrl)}`,
`export HTTPS_PROXY=${shQuote(proxyUrl)}`,
`export ALL_PROXY=${shQuote(proxyUrl)}`,
`export http_proxy=${shQuote(proxyUrl)}`,
`export https_proxy=${shQuote(proxyUrl)}`,
`export all_proxy=${shQuote(proxyUrl)}`,
"export GIT_SSH=/tmp/agentrun-git-ssh-proxy.sh",
"unset GIT_SSH_COMMAND",
];
}
function yamlLaneGitMirrorSyncShell(spec: AgentRunLaneSpec): string {
return [
"set -eu",
...yamlLaneGitMirrorSshSetupShellLines(spec),
`repository=${shQuote(spec.source.repository)}`,
`source_branch=${shQuote(spec.source.branch)}`,
`gitops_branch=${shQuote(spec.gitops.branch)}`,
"repo=\"/cache/${repository}.git\"",
"remote=\"ssh://git@ssh.github.com:443/${repository}.git\"",
"mkdir -p \"$(dirname \"$repo\")\"",
"if [ -d \"$repo/objects\" ] && [ -f \"$repo/HEAD\" ]; then",
" git --git-dir=\"$repo\" remote set-url origin \"$remote\" || git --git-dir=\"$repo\" remote add origin \"$remote\"",
"else",
" rm -rf \"$repo\"",
" git init --bare \"$repo\"",
" git --git-dir=\"$repo\" remote add origin \"$remote\"",
"fi",
"git --git-dir=\"$repo\" config uploadpack.allowReachableSHA1InWant true",
"git --git-dir=\"$repo\" config uploadpack.allowAnySHA1InWant true",
"git --git-dir=\"$repo\" config http.uploadpack true",
"git --git-dir=\"$repo\" config http.receivepack true",
"timeout 240 git --git-dir=\"$repo\" fetch origin \"+refs/heads/${source_branch}:refs/mirror-stage/heads/${source_branch}\"",
"source_sha=$(git --git-dir=\"$repo\" rev-parse --verify \"refs/mirror-stage/heads/${source_branch}^{commit}\")",
"git --git-dir=\"$repo\" update-ref \"refs/heads/${source_branch}\" \"$source_sha\"",
"timeout 240 git --git-dir=\"$repo\" fetch origin \"+refs/heads/${gitops_branch}:refs/mirror-stage/heads/${gitops_branch}\"",
"gitops_sha=$(git --git-dir=\"$repo\" rev-parse --verify \"refs/mirror-stage/heads/${gitops_branch}^{commit}\")",
"git --git-dir=\"$repo\" update-ref \"refs/heads/${gitops_branch}\" \"$gitops_sha\"",
"git --git-dir=\"$repo\" update-server-info",
"SOURCE_SHA=\"$source_sha\" GITOPS_SHA=\"$gitops_sha\" node <<'NODE'",
"console.log(JSON.stringify({ ok: true, localSource: process.env.SOURCE_SHA, localGitops: process.env.GITOPS_SHA, valuesPrinted: false }));",
"NODE",
].join("\n");
}
function yamlLaneGitMirrorFlushShell(spec: AgentRunLaneSpec): string {
return [
"set -eu",
...yamlLaneGitMirrorSshSetupShellLines(spec),
`repository=${shQuote(spec.source.repository)}`,
`gitops_branch=${shQuote(spec.gitops.branch)}`,
"repo=\"/cache/${repository}.git\"",
"remote=\"ssh://git@ssh.github.com:443/${repository}.git\"",
"test -d \"$repo\"",
"git --git-dir=\"$repo\" remote set-url origin \"$remote\" || git --git-dir=\"$repo\" remote add origin \"$remote\"",
"local_gitops=$(git --git-dir=\"$repo\" rev-parse --verify \"refs/heads/${gitops_branch}^{commit}\" 2>/dev/null || true)",
"if [ -n \"$local_gitops\" ]; then",
" git --git-dir=\"$repo\" -c remote.origin.mirror=false push origin \"refs/heads/${gitops_branch}:refs/heads/${gitops_branch}\"",
" git --git-dir=\"$repo\" fetch origin \"+refs/heads/${gitops_branch}:refs/mirror-stage/heads/${gitops_branch}\"",
"fi",
"github_gitops=$(git --git-dir=\"$repo\" rev-parse --verify \"refs/mirror-stage/heads/${gitops_branch}^{commit}\" 2>/dev/null || true)",
"pending=false; if [ -n \"$local_gitops\" ] && { [ -z \"$github_gitops\" ] || [ \"$local_gitops\" != \"$github_gitops\" ]; }; then pending=true; fi",
"LOCAL_GITOPS=\"$local_gitops\" GITHUB_GITOPS=\"$github_gitops\" PENDING=\"$pending\" node <<'NODE'",
"console.log(JSON.stringify({ ok: process.env.PENDING !== 'true', localGitops: process.env.LOCAL_GITOPS || null, githubGitops: process.env.GITHUB_GITOPS || null, pendingFlush: process.env.PENDING === 'true', valuesPrinted: false }));",
"NODE",
].join("\n");
}
function yamlLaneGitMirrorStatusScript(spec: AgentRunLaneSpec): string {
return [
"set +e",
`namespace=${shQuote(spec.gitMirror.namespace)}`,
`read_deployment=${shQuote(spec.gitMirror.readDeployment)}`,
`read_service=${shQuote(spec.gitMirror.readService)}`,
`write_service=${shQuote(spec.gitMirror.writeService)}`,
`cache_pvc=${shQuote(spec.gitMirror.cachePvc)}`,
`repository=${shQuote(spec.source.repository)}`,
`source_branch=${shQuote(spec.source.branch)}`,
`gitops_branch=${shQuote(spec.gitops.branch)}`,
`repositories_json=${shQuote(JSON.stringify(spec.gitMirror.repositories))}`,
"kubectl -n \"$namespace\" get svc \"$read_service\" -o json >/tmp/agentrun-gitmirror-read.json 2>/dev/null",
"read_exit=$?",
"kubectl -n \"$namespace\" get svc \"$write_service\" -o json >/tmp/agentrun-gitmirror-write.json 2>/dev/null",
"write_exit=$?",
"kubectl -n \"$namespace\" get pvc \"$cache_pvc\" -o json >/tmp/agentrun-gitmirror-cache.json 2>/dev/null",
"cache_exit=$?",
"kubectl -n \"$namespace\" get deploy,svc,pvc -o name > /tmp/agentrun-gitmirror-names.txt 2>/dev/null",
"repo_path=\"/cache/${repository}.git\"",
"rm -f /tmp/agentrun-gitmirror-refs.txt",
"if [ \"$read_exit\" -eq 0 ]; then",
" kubectl -n \"$namespace\" exec deploy/\"$read_deployment\" -- sh -lc 'repo_path=\"$1\"; source_branch=\"$2\"; gitops_branch=\"$3\"; printf \"sourceCommit=\"; git --git-dir=\"$repo_path\" rev-parse \"refs/heads/$source_branch\" 2>/dev/null || true; printf \"gitopsCommit=\"; git --git-dir=\"$repo_path\" rev-parse \"refs/heads/$gitops_branch\" 2>/dev/null || true' sh \"$repo_path\" \"$source_branch\" \"$gitops_branch\" > /tmp/agentrun-gitmirror-refs.txt 2>/dev/null",
"fi",
"NAMESPACE=\"$namespace\" READ_EXIT=\"$read_exit\" WRITE_EXIT=\"$write_exit\" CACHE_EXIT=\"$cache_exit\" REPOSITORY=\"$repository\" SOURCE_BRANCH=\"$source_branch\" GITOPS_BRANCH=\"$gitops_branch\" REPOSITORIES_JSON=\"$repositories_json\" node <<'NODE'",
"const fs = require('node:fs');",
"const repositories = JSON.parse(process.env.REPOSITORIES_JSON || '[]');",
"let names = ''; try { names = fs.readFileSync('/tmp/agentrun-gitmirror-names.txt', 'utf8'); } catch {}",
"let refs = ''; try { refs = fs.readFileSync('/tmp/agentrun-gitmirror-refs.txt', 'utf8'); } catch {}",
"const refValue = (key) => refs.split(/\\r?\\n/).find((line) => line.startsWith(`${key}=`))?.slice(key.length + 1).trim() || null;",
"const readReady = process.env.READ_EXIT === '0';",
"const writeReady = process.env.WRITE_EXIT === '0';",
"const cachePvcExists = process.env.CACHE_EXIT === '0';",
"console.log(JSON.stringify({",
" ok: readReady && writeReady && cachePvcExists,",
" namespace: process.env.NAMESPACE,",
" readReady,",
" writeReady,",
" cachePvcExists,",
" resources: names.split(/\\r?\\n/).filter(Boolean).slice(0, 40),",
" repository: process.env.REPOSITORY,",
" sourceBranch: process.env.SOURCE_BRANCH,",
" gitopsBranch: process.env.GITOPS_BRANCH,",
" sourceCommit: refValue('sourceCommit'),",
" gitopsCommit: refValue('gitopsCommit'),",
" repositories,",
" valuesPrinted: false",
"}));",
"NODE",
].join("\n");
}
type LaneSecretSource = {
id: string;
sourceRef: string;
sourceMode: "env" | "file";
sourceKey: string | null;
targetRef: { namespace: string; name: string; key: string };
};
function readSecretSourceValue(spec: AgentRunLaneSpec, source: LaneSecretSource): { redactedPath: string; value: string; valueBytes: number; fingerprint: string } {
const { sourceRef } = source;
if (sourceRef.includes("..")) throw new Error(`secret sourceRef must not contain ..: ${sourceRef}`);
const sourcePath = sourceRef.startsWith("/")
? sourceRef
: join(resolveSecretSourceRoot(spec), ...sourceRef.split("/"));
if (!existsSync(sourcePath)) throw new Error(`secret source ${sourceRef} is missing`);
let value: string;
if (source.sourceMode === "file") {
value = readFileSync(sourcePath, "utf8");
} else {
if (source.sourceKey === null) throw new Error(`secret source ${sourceRef} is missing sourceKey`);
const values = parseEnvFile(readFileSync(sourcePath, "utf8"));
const envValue = values.get(source.sourceKey);
if (envValue === undefined || envValue.length === 0) throw new Error(`secret source ${sourceRef} is missing required key ${source.sourceKey}`);
value = envValue;
}
if (value.length === 0) throw new Error(`secret source ${sourceRef} is empty`);
return {
redactedPath: sourceRef.startsWith("/") ? redactAbsoluteSecretPath(sourceRef) : `.state/secrets/${sourceRef}`,
value,
valueBytes: Buffer.byteLength(value, "utf8"),
fingerprint: sha256Fingerprint(value),
};
}
function redactAbsoluteSecretPath(sourceRef: string): string {
const parts = sourceRef.split("/").filter(Boolean);
return parts.length === 0 ? "/" : `/${parts.slice(0, -1).join("/")}/<redacted>`;
}
function resolveSecretSourceRoot(spec: AgentRunLaneSpec): string {
if (spec.database.configRef === null) return rootPath(".state", "secrets");
const configPath = spec.database.configRef.startsWith("/") ? spec.database.configRef : rootPath(spec.database.configRef);
const configRoot = record(Bun.YAML.parse(readFileSync(configPath, "utf8")) as unknown);
const secrets = record(configRoot.secrets);
const root = stringOrNull(secrets.root);
if (root === null || !root.startsWith("/") || root.includes("..")) throw new Error(`${spec.database.configRef}.secrets.root must be an absolute path without ..`);
return root;
}
function parseEnvFile(text: string): Map<string, string> {
const result = new Map<string, string>();
for (const rawLine of text.split(/\r?\n/u)) {
let line = rawLine.trim();
if (line.length === 0 || line.startsWith("#")) continue;
if (line.startsWith("export ")) line = line.slice("export ".length).trim();
const index = line.indexOf("=");
if (index <= 0) continue;
const key = line.slice(0, index).trim();
let value = line.slice(index + 1).trim();
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
value = value.slice(1, -1);
}
result.set(key, value);
}
return result;
}
function collectLaneSecretSources(spec: AgentRunLaneSpec): LaneSecretSource[] {
const result: LaneSecretSource[] = [];
if (spec.database.secretSourceRef !== null) {
result.push({
id: "database",
sourceRef: spec.database.secretSourceRef,
sourceMode: "env",
sourceKey: spec.database.secretRef.key,
targetRef: { namespace: spec.runtime.namespace, name: spec.database.secretRef.name, key: spec.database.secretRef.key },
});
}
for (const secret of spec.secrets) {
result.push({
id: secret.id,
sourceRef: secret.sourceRef,
sourceMode: secret.sourceMode,
sourceKey: secret.sourceKey,
targetRef: secret.targetRef,
});
}
const seen = new Set<string>();
return result.filter((item) => {
const key = `${item.targetRef.namespace}/${item.targetRef.name}/${item.targetRef.key}`;
if (seen.has(key)) return false;
seen.add(key);
return true;
});
}
function secretSyncScript(_spec: AgentRunLaneSpec, values: Array<{ targetRef: { namespace: string; name: string; key: string }; value: string }>): string {
const encoded = Buffer.from(JSON.stringify(values.map((item) => ({
targetRef: item.targetRef,
valueBase64: Buffer.from(item.value, "utf8").toString("base64"),
}))), "utf8").toString("base64");
return [
"set -eu",
`payload_b64=${shQuote(encoded)}`,
"tmp_dir=$(mktemp -d)",
"trap 'rm -rf \"$tmp_dir\"' EXIT",
"payload_json=\"$tmp_dir/payload.json\"",
"printf '%s' \"$payload_b64\" | base64 -d > \"$payload_json\"",
"PAYLOAD_JSON=\"$payload_json\" TMP_DIR=\"$tmp_dir\" node <<'NODE'",
"const fs = require('node:fs');",
"const cp = require('node:child_process');",
"const crypto = require('node:crypto');",
"const items = JSON.parse(fs.readFileSync(process.env.PAYLOAD_JSON, 'utf8'));",
"const results = [];",
"const groups = new Map();",
"function run(argv, input) {",
" const out = cp.spawnSync(argv[0], argv.slice(1), { input, encoding: 'utf8' });",
" if (out.status !== 0) throw new Error(`${argv.join(' ')} failed: ${out.stderr || out.stdout}`);",
" return out;",
"}",
"function tryRun(argv) {",
" return cp.spawnSync(argv[0], argv.slice(1), { encoding: 'utf8' });",
"}",
"for (let index = 0; index < items.length; index += 1) {",
" const item = items[index];",
" const ref = item.targetRef;",
" const groupKey = `${ref.namespace}\\u0000${ref.name}`;",
" if (!groups.has(groupKey)) groups.set(groupKey, { namespace: ref.namespace, name: ref.name, items: [] });",
" groups.get(groupKey).items.push({ key: ref.key, valueBase64: item.valueBase64 });",
"}",
"for (const group of groups.values()) {",
" const ns = run(['kubectl', 'create', 'namespace', group.namespace, '--dry-run=client', '-o', 'yaml']).stdout;",
" run(['kubectl', 'apply', '--server-side', '--field-manager=unidesk-agentrun-secret-sync', '-f', '-'], ns);",
" const existing = tryRun(['kubectl', '-n', group.namespace, 'get', 'secret', group.name]);",
" if (existing.status !== 0) run(['kubectl', '-n', group.namespace, 'create', 'secret', 'generic', group.name]);",
" const patch = { data: {} };",
" for (const entry of group.items) patch.data[entry.key] = entry.valueBase64;",
" const patchFile = `${process.env.TMP_DIR}/${group.namespace}-${group.name}-patch.json`;",
" fs.writeFileSync(patchFile, JSON.stringify(patch));",
" run(['kubectl', '-n', group.namespace, 'patch', 'secret', group.name, '--type=merge', `--patch-file=${patchFile}`]);",
" const fetched = JSON.parse(run(['kubectl', '-n', group.namespace, 'get', 'secret', group.name, '-o', 'json']).stdout);",
" for (const entry of group.items) {",
" const raw = fetched.data?.[entry.key] || '';",
" const decoded = raw ? Buffer.from(raw, 'base64') : Buffer.alloc(0);",
" results.push({ namespace: group.namespace, secret: group.name, key: entry.key, ok: raw.length > 0, valueBytes: decoded.length, fingerprint: raw ? 'sha256:' + crypto.createHash('sha256').update(decoded).digest('hex') : null, valuesPrinted: false });",
" }",
"}",
"console.log(JSON.stringify({ ok: results.every((item) => item.ok), secretCount: results.length, items: results, valuesPrinted: false }));",
"NODE",
].join("\n");
}
function restartYamlLaneScript(spec: AgentRunLaneSpec): string {
return [
"set +e",
`namespace=${shQuote(spec.runtime.namespace)}`,
`deployment=${shQuote(spec.runtime.managerDeployment)}`,
"ts=$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"kubectl -n \"$namespace\" patch deployment \"$deployment\" --type='merge' -p \"{\\\"spec\\\":{\\\"template\\\":{\\\"metadata\\\":{\\\"annotations\\\":{\\\"kubectl.kubernetes.io/restartedAt\\\":\\\"$ts\\\"}}}}}\" >/tmp/agentrun-restart-patch.out 2>/tmp/agentrun-restart-patch.err",
"patch_rc=$?",
"if [ \"$patch_rc\" -eq 0 ]; then",
" kubectl -n \"$namespace\" rollout status deployment/\"$deployment\" --timeout=180s >/tmp/agentrun-rollout-status.out 2>/tmp/agentrun-rollout-status.err",
" rollout_rc=$?",
"else",
" rollout_rc=$patch_rc",
"fi",
"pods_json=$(kubectl -n \"$namespace\" get pod -l app.kubernetes.io/name=agentrun-mgr -o json 2>/dev/null || printf '{}')",
"deployment_json=$(kubectl -n \"$namespace\" get deployment \"$deployment\" -o json 2>/dev/null || printf '{}')",
"patch_err=$(cat /tmp/agentrun-restart-patch.err 2>/dev/null || true)",
"rollout_err=$(cat /tmp/agentrun-rollout-status.err 2>/dev/null || true)",
"PODS_JSON=\"$pods_json\" DEPLOYMENT_JSON=\"$deployment_json\" PATCH_RC=\"$patch_rc\" ROLLOUT_RC=\"$rollout_rc\" TS=\"$ts\" PATCH_ERR=\"$patch_err\" ROLLOUT_ERR=\"$rollout_err\" node <<'NODE'",
"const pods = JSON.parse(process.env.PODS_JSON || '{}');",
"const deployment = JSON.parse(process.env.DEPLOYMENT_JSON || '{}');",
"const conditions = Array.isArray(deployment.status?.conditions) ? deployment.status.conditions : [];",
"const items = Array.isArray(pods.items) ? pods.items : [];",
"const compactError = (value) => String(value || '').replace(/\\s+/g, ' ').trim().slice(0, 500) || null;",
"console.log(JSON.stringify({",
" ok: process.env.PATCH_RC === '0' && process.env.ROLLOUT_RC === '0',",
" restartedAt: process.env.TS,",
" patch: { exitCode: Number(process.env.PATCH_RC || '1'), error: compactError(process.env.PATCH_ERR) },",
" rollout: { exitCode: Number(process.env.ROLLOUT_RC || '1'), error: compactError(process.env.ROLLOUT_ERR) },",
" readyReplicas: deployment.status?.readyReplicas ?? null,",
" replicas: deployment.status?.replicas ?? null,",
" conditions: conditions.map((item) => ({ type: item.type, status: item.status, reason: item.reason ?? null })),",
" pods: items.map((pod) => ({",
" name: pod.metadata?.name ?? null,",
" phase: pod.status?.phase ?? null,",
" ready: (pod.status?.containerStatuses ?? []).map((item) => item.ready),",
" restarts: (pod.status?.containerStatuses ?? []).map((item) => item.restartCount),",
" waiting: (pod.status?.containerStatuses ?? []).map((item) => item.state?.waiting?.reason ?? null),",
" })),",
" valuesPrinted: false",
"}));",
"NODE",
"exit \"$rollout_rc\"",
].join("\n");
}
function applyYamlScript(yaml: string, fieldManager: string, dryRun: boolean): string {
const encoded = Buffer.from(yaml, "utf8").toString("base64");
return [
"set -eu",
`manifest_b64=${shQuote(encoded)}`,
`field_manager=${shQuote(fieldManager)}`,
`dry_run=${dryRun ? "true" : "false"}`,
"tmp_dir=$(mktemp -d)",
"trap 'rm -rf \"$tmp_dir\"' EXIT",
"manifest=\"$tmp_dir/manifest.yaml\"",
"printf '%s' \"$manifest_b64\" | base64 -d > \"$manifest\"",
"args=\"--server-side --force-conflicts --field-manager=$field_manager\"",
"if [ \"$dry_run\" = true ]; then args=\"$args --dry-run=server\"; fi",
"set +e",
"kubectl apply $args -f \"$manifest\" > \"$tmp_dir/apply.out\" 2> \"$tmp_dir/apply.err\"",
"apply_exit=$?",
"set -e",
"APPLY_EXIT=\"$apply_exit\" APPLY_OUT=\"$tmp_dir/apply.out\" APPLY_ERR=\"$tmp_dir/apply.err\" MANIFEST=\"$manifest\" node <<'NODE'",
"const fs = require('node:fs');",
"const crypto = require('node:crypto');",
"const out = fs.readFileSync(process.env.APPLY_OUT, 'utf8');",
"const err = fs.readFileSync(process.env.APPLY_ERR, 'utf8');",
"const manifest = fs.readFileSync(process.env.MANIFEST, 'utf8');",
"const resources = out.split(/\\r?\\n/).map((line) => line.trim()).filter(Boolean).slice(0, 80);",
"console.log(JSON.stringify({ ok: process.env.APPLY_EXIT === '0', exitCode: Number(process.env.APPLY_EXIT), resourceCount: resources.length, resources, manifestBytes: Buffer.byteLength(manifest, 'utf8'), manifestDigest: 'sha256:' + crypto.createHash('sha256').update(manifest).digest('hex'), stderrTail: err.slice(-3000), valuesPrinted: false }));",
"NODE",
"exit \"$apply_exit\"",
].join("\n");
}
function manifestObjectRef(object: Record<string, unknown>): Record<string, unknown> {
const metadata = record(object.metadata);
return {
apiVersion: stringOrNull(object.apiVersion),
kind: stringOrNull(object.kind),
namespace: stringOrNull(metadata.namespace),
name: stringOrNull(metadata.name),
};
}
function cleanupRunnersFactsNodeScript(): string {
return String.raw`
function iso(value) {
if (value instanceof Date) return value.toISOString();
if (typeof value === "string" && value.length > 0) {
const ms = Date.parse(value);
return Number.isFinite(ms) ? new Date(ms).toISOString() : null;
}
return null;
}
try {
const { Pool } = await import("pg");
const namespace = process.env.RETENTION_NAMESPACE || process.env.AGENTRUN_RUNTIME_NAMESPACE || "";
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const sql = [
"SELECT",
" j.id, j.run_id, j.command_id, j.attempt_id, j.runner_id, j.namespace, j.job_name, j.created_at, j.updated_at,",
" r.registered_at AS runner_registered_at, r.heartbeat_at AS runner_heartbeat_at,",
" rr.status AS run_status, rr.terminal_status AS run_terminal_status, rr.failure_kind AS run_failure_kind, rr.updated_at AS run_updated_at",
"FROM agentrun_runner_jobs j",
"LEFT JOIN agentrun_runners r ON r.id = j.runner_id",
"LEFT JOIN agentrun_runs rr ON rr.id = j.run_id",
"WHERE j.namespace = $1",
"ORDER BY COALESCE(r.heartbeat_at, j.updated_at, j.created_at) ASC",
].join("\n");
const result = await pool.query(sql, [namespace]);
await pool.end();
console.log(JSON.stringify({
ok: true,
itemCount: result.rows.length,
items: result.rows.map((row) => ({
id: row.id,
runId: row.run_id,
commandId: row.command_id,
attemptId: row.attempt_id,
runnerId: row.runner_id,
namespace: row.namespace,
jobName: row.job_name,
runnerJobCreatedAt: iso(row.created_at),
runnerJobUpdatedAt: iso(row.updated_at),
runnerRegisteredAt: iso(row.runner_registered_at),
runnerHeartbeatAt: iso(row.runner_heartbeat_at),
runStatus: row.run_status || null,
runTerminalStatus: row.run_terminal_status || null,
runFailureKind: row.run_failure_kind || null,
runUpdatedAt: iso(row.run_updated_at),
valuesPrinted: false,
})),
valuesPrinted: false,
}));
} catch (error) {
console.log(JSON.stringify({
ok: false,
itemCount: 0,
items: [],
failureKind: "manager-runner-facts-query-failed",
message: error instanceof Error ? error.message : String(error),
valuesPrinted: false,
}));
}
`;
}
function cleanupRunnersPlanNodeScript(): string {
return String.raw`
const fs = require("node:fs");
const path = require("node:path");
const tmp = process.env.TMP_DIR;
const namespace = process.env.NAMESPACE;
const maxRunners = Number(process.env.MAX_RUNNERS || 0);
const cleanupOrder = process.env.CLEANUP_ORDER || "";
const activeHeartbeatMaxAgeMs = Number(process.env.ACTIVE_HEARTBEAT_MAX_AGE_MS || 0);
const ageBasedCleanupEnabled = process.env.AGE_BASED_CLEANUP_ENABLED === "true";
const ageBasedMaxAgeHours = process.env.AGE_BASED_MAX_AGE_HOURS ? Number(process.env.AGE_BASED_MAX_AGE_HOURS) : null;
const forceActive = process.env.FORCE_ACTIVE === "true";
const matchLabels = JSON.parse(process.env.MATCH_LABELS_JSON || "{}");
const jobNamePrefixes = JSON.parse(process.env.JOB_NAME_PREFIXES_JSON || "[]");
const now = Date.now();
function readJson(name) {
return JSON.parse(fs.readFileSync(path.join(tmp, name), "utf8"));
}
function dateMs(value) {
const ms = Date.parse(value || "");
return Number.isFinite(ms) ? ms : null;
}
function ageMs(value) {
const ms = dateMs(value);
return ms === null ? null : Math.max(0, now - ms);
}
function labelsOf(item) {
return item?.metadata?.labels && typeof item.metadata.labels === "object" ? item.metadata.labels : {};
}
function annotationsOf(item) {
return item?.metadata?.annotations && typeof item.metadata.annotations === "object" ? item.metadata.annotations : {};
}
function matchesLabels(labels) {
return Object.entries(matchLabels).every(([key, value]) => labels?.[key] === value);
}
function matchesPrefix(name) {
return jobNamePrefixes.length === 0 || jobNamePrefixes.some((prefix) => name === prefix || name.startsWith(prefix + "-"));
}
function terminalRunStatus(value) {
return value === "completed" || value === "failed" || value === "blocked" || value === "cancelled";
}
function jobCondition(job, type) {
const conditions = Array.isArray(job?.status?.conditions) ? job.status.conditions : [];
return conditions.find((entry) => entry?.type === type && entry?.status === "True") || null;
}
function isTerminalJob(job) {
return jobCondition(job, "Complete") !== null || jobCondition(job, "Failed") !== null || Number(job?.status?.succeeded || 0) > 0 || Number(job?.status?.failed || 0) > 0;
}
function isTerminalPod(pod) {
const phase = pod?.status?.phase || "";
return phase === "Succeeded" || phase === "Failed";
}
function jobNameForPod(pod) {
const labels = labelsOf(pod);
if (typeof labels["job-name"] === "string") return labels["job-name"];
const owner = (Array.isArray(pod?.metadata?.ownerReferences) ? pod.metadata.ownerReferences : []).find((entry) => entry?.kind === "Job" && typeof entry?.name === "string");
return owner?.name || null;
}
function preferredLastActiveAt(fact, job) {
return fact?.runnerHeartbeatAt
|| fact?.runUpdatedAt
|| fact?.runnerJobUpdatedAt
|| job?.status?.completionTime
|| job?.status?.startTime
|| job?.metadata?.creationTimestamp
|| null;
}
function sortOldestLastActive(left, right) {
const leftMs = dateMs(left.lastActiveAt) ?? dateMs(left.createdAt) ?? Number.MAX_SAFE_INTEGER;
const rightMs = dateMs(right.lastActiveAt) ?? dateMs(right.createdAt) ?? Number.MAX_SAFE_INTEGER;
if (leftMs !== rightMs) return leftMs - rightMs;
return String(left.name).localeCompare(String(right.name));
}
function countNonTerminalPods(pods) {
return pods.filter((pod) => !isTerminalPod(pod)).length;
}
const jobs = readJson("jobs.json");
const pods = readJson("pods.json");
const facts = readJson("runner-facts.json");
const factItems = Array.isArray(facts.items) ? facts.items : [];
const factByJob = new Map(factItems.map((item) => [item.jobName, item]));
const allPods = Array.isArray(pods.items) ? pods.items : [];
const podsByJob = new Map();
for (const pod of allPods) {
const jobName = jobNameForPod(pod);
if (!jobName) continue;
const entry = podsByJob.get(jobName) || [];
entry.push(pod);
podsByJob.set(jobName, entry);
}
const matchedJobs = (Array.isArray(jobs.items) ? jobs.items : [])
.map((job) => {
const name = job?.metadata?.name || "";
return { job, name, labels: labelsOf(job), annotations: annotationsOf(job) };
})
.filter((item) => item.name && matchesLabels(item.labels) && matchesPrefix(item.name));
const runnerJobs = matchedJobs.map(({ job, name, labels, annotations }) => {
const fact = factByJob.get(name) || null;
const jobPods = podsByJob.get(name) || [];
const nonTerminalPods = jobPods.filter((pod) => !isTerminalPod(pod));
const terminatingPods = nonTerminalPods.filter((pod) => typeof pod?.metadata?.deletionTimestamp === "string");
const terminal = isTerminalJob(job) || terminalRunStatus(fact?.runStatus) || terminalRunStatus(fact?.runTerminalStatus);
const heartbeatAgeMs = ageMs(fact?.runnerHeartbeatAt);
const heartbeatFresh = heartbeatAgeMs !== null && heartbeatAgeMs <= activeHeartbeatMaxAgeMs;
const hasActivePod = Number(job?.status?.active || 0) > 0 || nonTerminalPods.length > 0;
const lastActiveAt = preferredLastActiveAt(fact, job);
let inactive = false;
let protectedActive = false;
let classification = "unknown";
if (terminal) {
inactive = true;
classification = "terminal";
} else if (heartbeatFresh) {
protectedActive = true;
classification = "active-fresh-heartbeat";
} else if (heartbeatAgeMs !== null) {
inactive = true;
classification = "inactive-stale-heartbeat";
} else if (terminatingPods.length > 0 && nonTerminalPods.length === terminatingPods.length) {
inactive = true;
classification = "inactive-terminating";
} else if (hasActivePod) {
protectedActive = true;
classification = facts.ok === true ? "active-no-heartbeat-row" : "active-unverified-manager-facts";
} else {
inactive = true;
classification = "inactive-no-active-pod";
}
return {
name,
namespace,
createdAt: job?.metadata?.creationTimestamp || null,
lastActiveAt,
lastActiveAgeMs: ageMs(lastActiveAt),
lastActiveSource: fact?.runnerHeartbeatAt ? "runner-heartbeat" : fact?.runUpdatedAt ? "run-updated" : fact?.runnerJobUpdatedAt ? "runner-job-updated" : job?.status?.completionTime ? "job-completion" : job?.status?.startTime ? "job-start" : "job-created",
runId: annotations["agentrun.pikastech.local/run-id"] || fact?.runId || null,
commandId: annotations["agentrun.pikastech.local/command-id"] || fact?.commandId || null,
runnerJobId: fact?.id || null,
runnerId: fact?.runnerId || null,
runStatus: fact?.runStatus || null,
runTerminalStatus: fact?.runTerminalStatus || null,
jobStatus: {
active: Number(job?.status?.active || 0),
succeeded: Number(job?.status?.succeeded || 0),
failed: Number(job?.status?.failed || 0),
ready: Number(job?.status?.ready || 0),
terminating: Number(job?.status?.terminating || 0),
},
podCount: jobPods.length,
nonTerminalPodCount: nonTerminalPods.length,
terminatingPodCount: terminatingPods.length,
inactive,
protectedActive,
classification,
labels,
valuesPrinted: false,
};
});
const runnerJobCount = runnerJobs.length;
const inactiveCandidates = runnerJobs.filter((item) => item.inactive).sort(sortOldestLastActive);
const overLimitCount = Math.max(0, runnerJobCount - maxRunners);
const selectedByName = new Map();
const selectionReasons = new Map();
for (const item of inactiveCandidates.slice(0, overLimitCount)) {
selectedByName.set(item.name, item);
selectionReasons.set(item.name, "over-max-runners");
}
if (ageBasedCleanupEnabled && ageBasedMaxAgeHours !== null) {
const maxAgeMs = ageBasedMaxAgeHours * 3600 * 1000;
for (const item of inactiveCandidates) {
const itemAgeMs = item.lastActiveAgeMs;
if (itemAgeMs !== null && itemAgeMs >= maxAgeMs) {
selectedByName.set(item.name, item);
if (!selectionReasons.has(item.name)) selectionReasons.set(item.name, "age-based-cleanup");
}
}
}
if (forceActive) {
for (const item of runnerJobs) {
selectedByName.set(item.name, item);
selectionReasons.set(item.name, item.protectedActive ? "force-active-runner-cleanup" : "force-runner-cleanup");
}
}
const selectionPool = forceActive ? runnerJobs : inactiveCandidates;
const selected = selectionPool
.filter((item) => selectedByName.has(item.name))
.map((item) => ({ ...item, selectionReason: selectionReasons.get(item.name) || "selected" }));
const matchedPodNames = new Set(runnerJobs.map((item) => item.name));
const matchedPods = allPods.filter((pod) => {
const jobName = jobNameForPod(pod);
return jobName !== null && matchedPodNames.has(jobName);
});
const remainingAfterSelection = runnerJobCount - selected.length;
console.log(JSON.stringify({
ok: true,
planKind: "agentrun-runtime-runner-retention",
generatedAt: new Date(now).toISOString(),
namespace,
criteria: {
maxRunners,
cleanupOrder,
activeHeartbeatMaxAgeMs,
forceActive,
selectors: { matchLabels, jobNamePrefixes },
ageBasedCleanup: { enabled: ageBasedCleanupEnabled, maxAgeHours: ageBasedMaxAgeHours },
},
managerFacts: {
ok: facts.ok === true,
factCount: factItems.length,
factsExit: Number(process.env.FACTS_EXIT || 0),
failureKind: facts.failureKind || null,
message: facts.ok === true ? null : facts.message || null,
valuesPrinted: false,
},
runnerJobCount,
nonTerminalRunnerPodCount: countNonTerminalPods(matchedPods),
overLimitCount,
inactiveCandidateCount: inactiveCandidates.length,
protectedActiveRunnerCount: runnerJobs.filter((item) => item.protectedActive).length,
activeRunRisk: remainingAfterSelection > maxRunners,
remainingRunnerJobCountAfterSelection: remainingAfterSelection,
runnerJobs: runnerJobs.sort(sortOldestLastActive),
candidates: inactiveCandidates,
selected,
selectedRunnerJobs: selected.map((item) => item.name),
selectedRunnerJobCount: selected.length,
valuesPrinted: false,
}));
`;
}
function cleanupRunnersFinalizeNodeScript(): string {
return String.raw`
const fs = require("node:fs");
const path = require("node:path");
const tmp = process.env.TMP_DIR;
const deleteExit = Number(process.env.DELETE_EXIT || 0);
const plan = JSON.parse(fs.readFileSync(path.join(tmp, "plan.json"), "utf8"));
const jobsAfter = JSON.parse(fs.readFileSync(path.join(tmp, "jobs-after.json"), "utf8"));
const podsAfter = JSON.parse(fs.readFileSync(path.join(tmp, "pods-after.json"), "utf8"));
const matchLabels = plan.criteria?.selectors?.matchLabels || {};
const jobNamePrefixes = plan.criteria?.selectors?.jobNamePrefixes || [];
function labelsOf(item) {
return item?.metadata?.labels && typeof item.metadata.labels === "object" ? item.metadata.labels : {};
}
function matchesLabels(labels) {
return Object.entries(matchLabels).every(([key, value]) => labels?.[key] === value);
}
function matchesPrefix(name) {
return jobNamePrefixes.length === 0 || jobNamePrefixes.some((prefix) => name === prefix || name.startsWith(prefix + "-"));
}
function isTerminalPod(pod) {
const phase = pod?.status?.phase || "";
return phase === "Succeeded" || phase === "Failed";
}
function jobNameForPod(pod) {
const labels = labelsOf(pod);
if (typeof labels["job-name"] === "string") return labels["job-name"];
const owner = (Array.isArray(pod?.metadata?.ownerReferences) ? pod.metadata.ownerReferences : []).find((entry) => entry?.kind === "Job" && typeof entry?.name === "string");
return owner?.name || null;
}
function tail(name) {
try {
const text = fs.readFileSync(path.join(tmp, name), "utf8");
return text.length > 3000 ? text.slice(-3000) : text;
} catch {
return "";
}
}
const selected = new Set(Array.isArray(plan.selectedRunnerJobs) ? plan.selectedRunnerJobs : []);
const remainingJobs = (Array.isArray(jobsAfter.items) ? jobsAfter.items : [])
.map((job) => ({ name: job?.metadata?.name || "", labels: labelsOf(job) }))
.filter((item) => item.name && matchesLabels(item.labels) && matchesPrefix(item.name));
const remainingJobNames = new Set(remainingJobs.map((item) => item.name));
const matchedPods = (Array.isArray(podsAfter.items) ? podsAfter.items : []).filter((pod) => {
const jobName = jobNameForPod(pod);
return jobName !== null && remainingJobNames.has(jobName);
});
const remainingSelectedRunnerJobs = Array.from(selected).filter((name) => remainingJobNames.has(name));
const deletedRunnerJobs = Array.from(selected).filter((name) => !remainingJobNames.has(name));
console.log(JSON.stringify({
...plan,
ok: deleteExit === 0,
deletion: { exitCode: deleteExit, stdoutTail: tail("delete.out"), stderrTail: tail("delete.err") },
deletedRunnerJobs,
deletedRunnerJobCount: deletedRunnerJobs.length,
remainingSelectedRunnerJobs,
remainingSelectedRunnerJobCount: remainingSelectedRunnerJobs.length,
after: {
runnerJobCount: remainingJobs.length,
nonTerminalRunnerPodCount: matchedPods.filter((pod) => !isTerminalPod(pod)).length,
overLimitCount: Math.max(0, remainingJobs.length - Number(plan.criteria?.maxRunners || 0)),
},
valuesPrinted: false,
}));
`;
}
function cleanupRunsPlanNodeScript(): string {
return String.raw`
const fs = require("node:fs");
const path = require("node:path");
const cp = require("node:child_process");
const tmp = process.env.TMP_DIR;
const namespace = process.env.NAMESPACE;
const pipelineRunPrefix = process.env.PIPELINE_RUN_PREFIX || "";
const minAgeMinutes = Number(process.env.MIN_AGE_MINUTES || 60);
const limit = Number(process.env.LIMIT || 20);
const now = Date.now();
function readJson(name) {
return JSON.parse(fs.readFileSync(path.join(tmp, name), "utf8"));
}
function conditionOf(item) {
const conditions = Array.isArray(item?.status?.conditions) ? item.status.conditions : [];
return conditions.find((entry) => entry.type === "Succeeded") || conditions[0] || {};
}
function ageMinutes(createdAt) {
const createdMs = Date.parse(createdAt || "");
return Number.isFinite(createdMs) ? Math.floor((now - createdMs) / 60000) : null;
}
function localPathOf(pv) {
return pv?.spec?.local?.path || pv?.spec?.hostPath?.path || null;
}
function duBytes(hostPath) {
if (typeof hostPath !== "string" || !hostPath.startsWith("/var/lib/rancher/k3s/storage/")) return null;
try {
const out = cp.execFileSync("du", ["-sB1", hostPath], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
const bytes = Number(String(out).trim().split(/\s+/)[0]);
return Number.isFinite(bytes) ? bytes : null;
} catch {
return null;
}
}
function formatBytes(bytes) {
if (!Number.isFinite(bytes)) return null;
const units = ["B", "KiB", "MiB", "GiB", "TiB"];
let value = bytes;
let unit = 0;
while (value >= 1024 && unit < units.length - 1) {
value /= 1024;
unit += 1;
}
return value.toFixed(unit === 0 ? 0 : 1) + units[unit];
}
const pipelineRuns = readJson("pipelineruns.json");
const pvcs = readJson("pvcs.json");
const pvs = readJson("pvs.json");
const pods = readJson("pods.json");
const pvByName = new Map((Array.isArray(pvs.items) ? pvs.items : []).map((item) => [item?.metadata?.name, item]));
const activeClaimPods = new Map();
for (const pod of Array.isArray(pods.items) ? pods.items : []) {
const phase = pod?.status?.phase || "";
if (phase === "Succeeded" || phase === "Failed") continue;
for (const volume of Array.isArray(pod?.spec?.volumes) ? pod.spec.volumes : []) {
const claimName = volume?.persistentVolumeClaim?.claimName;
if (!claimName) continue;
const entry = activeClaimPods.get(claimName) || [];
entry.push(pod?.metadata?.name || null);
activeClaimPods.set(claimName, entry.filter(Boolean));
}
}
const pvcsByOwner = new Map();
for (const pvc of Array.isArray(pvcs.items) ? pvcs.items : []) {
const owner = (Array.isArray(pvc?.metadata?.ownerReferences) ? pvc.metadata.ownerReferences : []).find((entry) => entry.kind === "PipelineRun");
if (!owner?.name) continue;
const entry = pvcsByOwner.get(owner.name) || [];
entry.push(pvc);
pvcsByOwner.set(owner.name, entry);
}
const allPipelineRuns = (Array.isArray(pipelineRuns.items) ? pipelineRuns.items : [])
.map((item) => {
const condition = conditionOf(item);
return {
name: item?.metadata?.name || "",
createdAt: item?.metadata?.creationTimestamp || null,
ageMinutes: ageMinutes(item?.metadata?.creationTimestamp),
status: condition.status || null,
reason: condition.reason || null,
};
})
.filter((item) => pipelineRunPrefix.length === 0 || item.name.startsWith(pipelineRunPrefix + "-"));
const protectedActivePipelineRuns = allPipelineRuns
.filter((item) => item.status !== "True" && item.status !== "False")
.sort((a, b) => String(a.createdAt).localeCompare(String(b.createdAt)));
const protectedLatestPipelineRun = allPipelineRuns
.filter((item) => item.status === "True" || item.status === "False")
.sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt)))[0]?.name || null;
const candidates = allPipelineRuns
.filter((item) => item.status === "True" || item.status === "False")
.filter((item) => typeof item.ageMinutes === "number" && item.ageMinutes >= minAgeMinutes)
.sort((a, b) => String(a.createdAt).localeCompare(String(b.createdAt)))
.slice(0, limit)
.map((item) => {
const owned = pvcsByOwner.get(item.name) || [];
const activeMountPods = owned.flatMap((pvc) => activeClaimPods.get(pvc?.metadata?.name) || []);
const protectedLatest = item.name === protectedLatestPipelineRun;
return {
...item,
selected: activeMountPods.length === 0 && !protectedLatest,
selectedReason: protectedLatest ? "protected-latest-pipelinerun" : activeMountPods.length === 0 ? "terminal-and-unmounted" : "owned-pvc-active-mounted",
ownedPvcCount: owned.length,
activeMountPods,
};
});
const selectedPipelineRuns = candidates.filter((item) => item.selected).map((item) => item.name);
const selectedSet = new Set(selectedPipelineRuns);
const ownedPvcs = [];
const protectedOwnedPvcs = [];
for (const [owner, items] of pvcsByOwner.entries()) {
if (!selectedSet.has(owner) && !candidates.some((item) => item.name === owner)) continue;
for (const pvc of items) {
const volume = pvc?.spec?.volumeName || null;
const pv = volume ? pvByName.get(volume) : null;
const hostPath = localPathOf(pv);
const activeMountPods = activeClaimPods.get(pvc?.metadata?.name) || [];
const entry = {
name: pvc?.metadata?.name || null,
volume,
phase: pvc?.status?.phase || null,
ownerKind: "PipelineRun",
owner,
storageClass: pv?.spec?.storageClassName || null,
reclaimPolicy: pv?.spec?.persistentVolumeReclaimPolicy || null,
hostPath,
estimatedBytes: duBytes(hostPath),
activeMountPods,
};
if (selectedSet.has(owner)) ownedPvcs.push(entry);
else protectedOwnedPvcs.push(entry);
}
}
const estimatedReclaimBytes = ownedPvcs.reduce((sum, item) => sum + (Number.isFinite(item.estimatedBytes) ? item.estimatedBytes : 0), 0);
console.log(JSON.stringify({
ok: true,
planKind: "agentrun-ci-completed-pipelinerun-workspace-retention",
generatedAt: new Date().toISOString(),
namespace,
criteria: { prefix: pipelineRunPrefix + "-", terminalStatuses: ["True", "False"], minAgeMinutes, limit },
candidates,
candidateCount: candidates.length,
protectedActivePipelineRuns,
protectedActivePipelineRunCount: protectedActivePipelineRuns.length,
protectedLatestPipelineRun,
selectedPipelineRuns,
selectedPipelineRunCount: selectedPipelineRuns.length,
ownedPvcs,
ownedPvcCount: ownedPvcs.length,
protectedOwnedPvcs,
protectedOwnedPvcCount: protectedOwnedPvcs.length,
estimatedReclaimBytes,
estimatedReclaimHuman: formatBytes(estimatedReclaimBytes),
}));
`;
}
function cleanupRunsFinalizeNodeScript(): string {
return String.raw`
const fs = require("node:fs");
const path = require("node:path");
const tmp = process.env.TMP_DIR;
const deleteExit = Number(process.env.DELETE_EXIT || 0);
const plan = JSON.parse(fs.readFileSync(path.join(tmp, "plan.json"), "utf8"));
const pvcsAfter = JSON.parse(fs.readFileSync(path.join(tmp, "pvcs-after.json"), "utf8"));
const selected = new Set(Array.isArray(plan.selectedPipelineRuns) ? plan.selectedPipelineRuns : []);
const remainingOwnedPvcs = (Array.isArray(pvcsAfter.items) ? pvcsAfter.items : [])
.map((pvc) => {
const owner = (Array.isArray(pvc?.metadata?.ownerReferences) ? pvc.metadata.ownerReferences : []).find((entry) => entry.kind === "PipelineRun");
return {
name: pvc?.metadata?.name || null,
volume: pvc?.spec?.volumeName || null,
phase: pvc?.status?.phase || null,
ownerKind: owner?.kind || null,
owner: owner?.name || null,
};
})
.filter((item) => item.owner && selected.has(item.owner));
function tail(name) {
try {
const text = fs.readFileSync(path.join(tmp, name), "utf8");
return text.length > 3000 ? text.slice(-3000) : text;
} catch {
return "";
}
}
console.log(JSON.stringify({
...plan,
ok: deleteExit === 0,
deletedPipelineRuns: Array.from(selected),
deletedPipelineRunCount: selected.size,
deletion: { exitCode: deleteExit, stdoutTail: tail("delete.out"), stderrTail: tail("delete.err") },
remainingOwnedPvcs,
remainingOwnedPvcCount: remainingOwnedPvcs.length,
}));
`;
}
function cleanupReleasedPvsPlanNodeScript(): string {
return String.raw`
const fs = require("node:fs");
const path = require("node:path");
const cp = require("node:child_process");
const tmp = process.env.TMP_DIR;
const namespace = process.env.NAMESPACE;
const limit = Number(process.env.LIMIT || 20);
function readJson(name) {
return JSON.parse(fs.readFileSync(path.join(tmp, name), "utf8"));
}
function localPathOf(pv) {
return pv?.spec?.local?.path || pv?.spec?.hostPath?.path || null;
}
function duBytes(hostPath) {
if (typeof hostPath !== "string" || !hostPath.startsWith("/var/lib/rancher/k3s/storage/")) return null;
try {
const out = cp.execFileSync("du", ["-sB1", hostPath], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
const bytes = Number(String(out).trim().split(/\s+/)[0]);
return Number.isFinite(bytes) ? bytes : null;
} catch {
return null;
}
}
function formatBytes(bytes) {
if (!Number.isFinite(bytes)) return null;
const units = ["B", "KiB", "MiB", "GiB", "TiB"];
let value = bytes;
let unit = 0;
while (value >= 1024 && unit < units.length - 1) {
value /= 1024;
unit += 1;
}
return value.toFixed(unit === 0 ? 0 : 1) + units[unit];
}
const pvs = readJson("pvs.json");
const candidates = (Array.isArray(pvs.items) ? pvs.items : [])
.map((pv) => {
const hostPath = localPathOf(pv);
return {
name: pv?.metadata?.name || "",
createdAt: pv?.metadata?.creationTimestamp || null,
phase: pv?.status?.phase || null,
storageClass: pv?.spec?.storageClassName || null,
reclaimPolicy: pv?.spec?.persistentVolumeReclaimPolicy || null,
claimNamespace: pv?.spec?.claimRef?.namespace || null,
claimName: pv?.spec?.claimRef?.name || null,
capacity: pv?.spec?.capacity?.storage || null,
hostPath,
estimatedBytes: duBytes(hostPath),
};
})
.filter((item) => item.phase === "Released")
.filter((item) => item.storageClass === "local-path" && item.reclaimPolicy === "Delete")
.filter((item) => item.claimNamespace === namespace)
.sort((a, b) => String(a.createdAt).localeCompare(String(b.createdAt)))
.slice(0, limit);
const selectedPersistentVolumes = candidates.map((item) => item.name);
const estimatedReclaimBytes = candidates.reduce((sum, item) => sum + (Number.isFinite(item.estimatedBytes) ? item.estimatedBytes : 0), 0);
console.log(JSON.stringify({
ok: true,
planKind: "agentrun-ci-released-local-path-pv-retention",
generatedAt: new Date().toISOString(),
namespace,
criteria: { phase: "Released", storageClass: "local-path", reclaimPolicy: "Delete", claimNamespace: namespace, limit },
candidates,
candidateCount: candidates.length,
selectedPersistentVolumes,
selectedPersistentVolumeCount: selectedPersistentVolumes.length,
estimatedReclaimBytes,
estimatedReclaimHuman: formatBytes(estimatedReclaimBytes),
}));
`;
}
function cleanupReleasedPvsFinalizeNodeScript(): string {
return String.raw`
const fs = require("node:fs");
const path = require("node:path");
const tmp = process.env.TMP_DIR;
const deleteExit = Number(process.env.DELETE_EXIT || 0);
const plan = JSON.parse(fs.readFileSync(path.join(tmp, "plan.json"), "utf8"));
const pvsAfter = JSON.parse(fs.readFileSync(path.join(tmp, "pvs-after.json"), "utf8"));
const selected = new Set(Array.isArray(plan.selectedPersistentVolumes) ? plan.selectedPersistentVolumes : []);
const remainingPersistentVolumes = (Array.isArray(pvsAfter.items) ? pvsAfter.items : [])
.filter((pv) => selected.has(pv?.metadata?.name))
.map((pv) => ({ name: pv?.metadata?.name || null, phase: pv?.status?.phase || null, claimNamespace: pv?.spec?.claimRef?.namespace || null, claimName: pv?.spec?.claimRef?.name || null }));
function tail(name) {
try {
const text = fs.readFileSync(path.join(tmp, name), "utf8");
return text.length > 3000 ? text.slice(-3000) : text;
} catch {
return "";
}
}
console.log(JSON.stringify({
...plan,
ok: deleteExit === 0,
deletedPersistentVolumes: Array.from(selected),
deletedPersistentVolumeCount: selected.size,
deletion: { exitCode: deleteExit, stdoutTail: tail("delete.out"), stderrTail: tail("delete.err") },
remainingPersistentVolumes,
remainingPersistentVolumeCount: remainingPersistentVolumes.length,
}));
`;
}
function refreshYamlLaneScript(spec: AgentRunLaneSpec): string {
return [
"set -eu",
`namespace=${shQuote(spec.gitops.argoNamespace)}`,
`application=${shQuote(spec.gitops.argoApplication)}`,
"kubectl -n \"$namespace\" annotate application \"$application\" argocd.argoproj.io/refresh=hard --overwrite >/tmp/agentrun-refresh-output.txt",
"kubectl -n \"$namespace\" get application \"$application\" -o json >/tmp/agentrun-refresh-app.json",
"NAMESPACE=\"$namespace\" APPLICATION=\"$application\" node <<'NODE'",
"const fs = require('node:fs');",
"let app = {};",
"try { app = JSON.parse(fs.readFileSync('/tmp/agentrun-refresh-app.json', 'utf8')); } catch {}",
"console.log(JSON.stringify({",
" ok: true,",
" namespace: process.env.NAMESPACE,",
" application: process.env.APPLICATION,",
" revision: app.status?.sync?.revision ?? null,",
" syncStatus: app.status?.sync?.status ?? null,",
" healthStatus: app.status?.health?.status ?? null,",
" observedAt: new Date().toISOString(),",
" valuesPrinted: false",
"}));",
"NODE",
].join("\n");
}
async function gitMirrorStatus(config: UniDeskConfig, options: GitMirrorStatusOptions = { full: false, raw: false, node: null, lane: null }): Promise<Record<string, unknown>> {
const target = resolveAgentRunLaneTarget(options);
const spec = target.spec;
const observation = await readGitMirrorStatus(config, target);
const summary = observation.summary;
return {
ok: observation.ok,
command: "agentrun git-mirror status",
mode: "yaml-declared-node-lane",
configPath: target.configPath,
target: agentRunLaneSummary(spec),
namespace: spec.gitMirror.namespace,
readUrl: spec.gitMirror.readUrl,
writeUrl: spec.gitMirror.writeUrl,
summary,
...(options.raw ? { raw: observation.raw } : {}),
probe: compactCapture(observation.result, { full: options.full || options.raw, stdoutTailChars: 6000, stderrTailChars: 3000 }),
disclosure: {
defaultView: "compact-low-noise",
full: options.full,
raw: options.raw,
rawOmitted: !options.raw,
probeTailOmitted: !(options.full || options.raw),
expandWith: `bun scripts/cli.ts agentrun git-mirror status --node ${spec.nodeId} --lane ${spec.lane} --full`,
rawWith: `bun scripts/cli.ts agentrun git-mirror status --node ${spec.nodeId} --lane ${spec.lane} --raw`,
},
next: {
sync: `bun scripts/cli.ts agentrun git-mirror sync --node ${spec.nodeId} --lane ${spec.lane} --confirm`,
flush: summary.pendingFlush === true ? `bun scripts/cli.ts agentrun git-mirror flush --node ${spec.nodeId} --lane ${spec.lane} --confirm` : null,
},
};
}
async function readGitMirrorStatus(config: UniDeskConfig, target: { configPath: string; spec: AgentRunLaneSpec }): Promise<Record<string, unknown> & { result: SshCaptureResult; raw: string; summary: Record<string, unknown> }> {
const spec = target.spec;
const result = await capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneGitMirrorStatusScript(spec)]);
const raw = result.stdout;
const summary = captureJsonPayload(result);
return {
ok: result.exitCode === 0 && summary.ok !== false,
configPath: target.configPath,
target: agentRunLaneSummary(spec),
namespace: spec.gitMirror.namespace,
readUrl: spec.gitMirror.readUrl,
writeUrl: spec.gitMirror.writeUrl,
raw,
summary,
result,
};
}
async function runGitMirrorJob(config: UniDeskConfig, action: "sync" | "flush", options: GitMirrorOptions): Promise<Record<string, unknown>> {
const { configPath, spec } = resolveAgentRunLaneTarget(options);
const previewJobName = `${action === "sync" ? spec.gitMirror.syncJobPrefix : spec.gitMirror.flushJobPrefix}-${Date.now().toString(36)}`.slice(0, 63);
const previewManifest = yamlLaneGitMirrorJobManifest(spec, action, previewJobName);
const command = `agentrun git-mirror ${action}`;
if (options.dryRun || !options.confirm) {
return {
ok: true,
command,
mode: "yaml-declared-node-lane",
configPath,
target: agentRunLaneSummary(spec),
dryRun: true,
namespace: spec.gitMirror.namespace,
jobName: previewJobName,
manifest: previewManifest,
next: { confirm: `bun scripts/cli.ts ${command} --node ${spec.nodeId} --lane ${spec.lane} --confirm` },
};
}
if (!options.wait) {
const jobName = previewJobName;
const manifest = yamlLaneGitMirrorJobManifest(spec, action, jobName);
const created = await capture(config, spec.nodeKubeRoute, ["sh", "--", createYamlLaneJobScript(spec.gitMirror.namespace, jobName, manifest)]);
const status = await gitMirrorStatus(config, { full: false, raw: false, node: spec.nodeId, lane: spec.lane });
return {
ok: created.exitCode === 0,
command,
mode: created.exitCode === 0 ? "submitted" : "create-failed",
configPath,
target: agentRunLaneSummary(spec),
dryRun: false,
namespace: spec.gitMirror.namespace,
jobName,
result: compactCapture(created),
status,
next: {
status: `bun scripts/cli.ts agentrun git-mirror status --node ${spec.nodeId} --lane ${spec.lane}`,
wait: `bun scripts/cli.ts agentrun git-mirror ${action} --node ${spec.nodeId} --lane ${spec.lane} --confirm --wait`,
},
};
}
const attempts: Record<string, unknown>[] = [];
let lastCreated: SshCaptureResult | null = null;
let lastWait: Record<string, unknown> | null = null;
for (let attempt = 1; attempt <= AGENTRUN_GIT_MIRROR_RETRY_MAX_ATTEMPTS; attempt += 1) {
const jobName = `${action === "sync" ? spec.gitMirror.syncJobPrefix : spec.gitMirror.flushJobPrefix}-${Date.now().toString(36)}-${attempt}`.slice(0, 63);
const manifest = yamlLaneGitMirrorJobManifest(spec, action, jobName);
const created = await capture(config, spec.nodeKubeRoute, ["sh", "--", createYamlLaneJobScript(spec.gitMirror.namespace, jobName, manifest)]);
lastCreated = created;
if (created.exitCode !== 0) {
const result = { phase: "create-job", capture: compactCapture(created, { full: true, stdoutTailChars: 4000, stderrTailChars: 4000 }) };
const retryableFailure = agentRunGitMirrorRetryableFailure(spec, action, attempt, result);
attempts.push({ attempt, retryLabel: `${attempt}/${AGENTRUN_GIT_MIRROR_RETRY_MAX_ATTEMPTS}`, jobName, ok: false, phase: "create-job", retryableFailure, retryExhausted: retryableFailure !== null && record(retryableFailure).retryExhausted === true });
if (retryableFailure !== null && record(retryableFailure).retryExhausted !== true) {
progressEvent(`agentrun.git-mirror.${action}.retry`, retryableFailure);
await sleep(agentRunGitMirrorRetryDelayMs(attempt));
continue;
}
const status = await gitMirrorStatus(config, { full: false, raw: false, node: spec.nodeId, lane: spec.lane });
return {
ok: false,
command,
mode: "create-failed",
configPath,
target: agentRunLaneSummary(spec),
dryRun: false,
namespace: spec.gitMirror.namespace,
jobName,
result: compactCapture(created),
retryableFailure,
retry: agentRunGitMirrorRetrySummary(attempts),
status,
next: {
status: `bun scripts/cli.ts agentrun git-mirror status --node ${spec.nodeId} --lane ${spec.lane}`,
wait: `bun scripts/cli.ts agentrun git-mirror ${action} --node ${spec.nodeId} --lane ${spec.lane} --confirm --wait`,
},
};
}
const wait = await waitForGitMirrorJob(config, spec, action, jobName, options.timeoutSeconds);
lastWait = wait;
if (wait.ok === true) {
attempts.push({ attempt, retryLabel: `${attempt}/${AGENTRUN_GIT_MIRROR_RETRY_MAX_ATTEMPTS}`, jobName, ok: true, wait });
const status = await gitMirrorStatus(config, { full: false, raw: false, node: spec.nodeId, lane: spec.lane });
return {
ok: true,
command,
mode: "waited",
configPath,
target: agentRunLaneSummary(spec),
dryRun: false,
namespace: spec.gitMirror.namespace,
jobName,
result: compactCapture(created),
wait,
retry: agentRunGitMirrorRetrySummary(attempts),
status,
next: {
status: `bun scripts/cli.ts agentrun git-mirror status --node ${spec.nodeId} --lane ${spec.lane}`,
flush: action === "sync" && status.summary && record(status.summary).pendingFlush === true ? `bun scripts/cli.ts agentrun git-mirror flush --node ${spec.nodeId} --lane ${spec.lane} --confirm` : null,
},
};
}
const retryableFailure = agentRunGitMirrorRetryableFailure(spec, action, attempt, wait);
attempts.push({ attempt, retryLabel: `${attempt}/${AGENTRUN_GIT_MIRROR_RETRY_MAX_ATTEMPTS}`, jobName, ok: false, wait, retryableFailure, retryExhausted: retryableFailure !== null && record(retryableFailure).retryExhausted === true });
if (retryableFailure !== null && record(retryableFailure).retryExhausted !== true) {
progressEvent(`agentrun.git-mirror.${action}.retry`, retryableFailure);
await sleep(agentRunGitMirrorRetryDelayMs(attempt));
continue;
}
const status = await gitMirrorStatus(config, { full: false, raw: false, node: spec.nodeId, lane: spec.lane });
return {
ok: false,
command,
mode: "waited",
configPath,
target: agentRunLaneSummary(spec),
dryRun: false,
namespace: spec.gitMirror.namespace,
jobName,
result: compactCapture(created),
wait,
retryableFailure,
retry: agentRunGitMirrorRetrySummary(attempts),
status,
next: {
status: `bun scripts/cli.ts agentrun git-mirror status --node ${spec.nodeId} --lane ${spec.lane}`,
wait: `bun scripts/cli.ts agentrun git-mirror ${action} --node ${spec.nodeId} --lane ${spec.lane} --confirm --wait`,
},
};
}
const status = await gitMirrorStatus(config, { full: false, raw: false, node: spec.nodeId, lane: spec.lane });
return {
ok: false,
command,
mode: "retry-exhausted",
configPath,
target: agentRunLaneSummary(spec),
dryRun: false,
namespace: spec.gitMirror.namespace,
result: lastCreated === null ? null : compactCapture(lastCreated),
wait: lastWait,
retry: agentRunGitMirrorRetrySummary(attempts),
status,
next: { status: `bun scripts/cli.ts agentrun git-mirror status --node ${spec.nodeId} --lane ${spec.lane}` },
};
}
async function waitForGitMirrorJob(config: UniDeskConfig, spec: AgentRunLaneSpec, action: "sync" | "flush", jobName: string, timeoutSeconds: number): Promise<Record<string, unknown>> {
const startedAtMs = Date.now();
let lastProbe: SshCaptureResult | null = null;
let polls = 0;
while (Date.now() - startedAtMs <= timeoutSeconds * 1000) {
polls += 1;
lastProbe = await capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneJobProbeScript(spec.gitMirror.namespace, jobName)]);
const summary = captureJsonPayload(lastProbe);
process.stderr.write(`${JSON.stringify({
event: `agentrun.git-mirror.${action}.progress`,
at: new Date().toISOString(),
stage: "k8s-job",
node: spec.nodeId,
lane: spec.lane,
status: summary.succeeded === true ? "succeeded" : summary.failed === true ? "failed" : "running",
jobName,
polls,
elapsedMs: Date.now() - startedAtMs,
})}\n`);
if (summary.succeeded === true) {
return {
ok: true,
action,
jobName,
polls,
elapsedMs: Date.now() - startedAtMs,
summary,
probe: compactCapture(lastProbe),
};
}
if (summary.failed === true) {
return {
ok: false,
action,
jobName,
polls,
elapsedMs: Date.now() - startedAtMs,
degradedReason: "git-mirror-job-failed",
summary,
probe: compactCapture(lastProbe),
};
}
await sleep(5_000);
}
return {
ok: false,
action,
jobName,
polls,
elapsedMs: Date.now() - startedAtMs,
degradedReason: "git-mirror-job-timeout",
probe: lastProbe ? compactCapture(lastProbe) : null,
};
}
function startAsyncAgentRunJob(name: string, command: string[], note: string): Record<string, unknown> {
const job = startJob(name, command, note);
const statusCommand = `bun scripts/cli.ts job status ${job.id} --tail-bytes 12000`;
return {
ok: true,
mode: "async-job",
job,
statusCommand,
tailCommand: `tail -f ${job.stdoutFile}`,
next: {
status: statusCommand,
tail: `tail -f ${job.stdoutFile}`,
},
};
}
async function runAgentRunRestCompatCommand(config: UniDeskConfig | null, group: AgentRunRestCompatGroup, args: string[], canonicalArgs: string[]): Promise<Record<string, unknown>> {
try {
return await withAgentRunRestTarget(resolveAgentRunRestTarget(config, parseAgentRunRestTargetOptions(args)), async () => {
return await runAgentRunRestCommand(config, group, stripAgentRunLaneTargetArgs(args));
});
} catch (error) {
if (error instanceof AgentRunRestError) return error.toPayload(`agentrun ${canonicalArgs.join(" ")}`.trim());
throw error;
}
}
async function runAgentRunRestCommand(config: UniDeskConfig | null, group: AgentRunRestCompatGroup, args: string[]): Promise<Record<string, unknown>> {
void config;
const compatGroup = group === "aipods" ? "aipod-specs" : group;
const [action, id] = args;
if (compatGroup === "queue") return await runAgentRunQueueRest(action, id, args);
if (compatGroup === "sessions") return await runAgentRunSessionsRest(action, id, args);
if (compatGroup === "runs") return await runAgentRunRunsRest(action, id, args);
if (compatGroup === "commands") return await runAgentRunCommandsRest(action, id, args);
if (compatGroup === "runner") return await runAgentRunRunnerRest(action, id, args);
if (compatGroup === "aipod-specs") return await runAgentRunAipodSpecsRest(action, id, args);
throw new AgentRunRestError("unsupported-version", `unsupported AgentRun REST compatibility group: ${group}`);
}
function parseAgentRunRestTargetOptions(args: string[]): AgentRunRestTargetOptions {
return {
node: agentRunOption(args, "node"),
lane: agentRunOption(args, "lane"),
};
}
function stripAgentRunLaneTargetArgs(args: string[]): string[] {
const result: string[] = [];
for (let index = 0; index < args.length; index += 1) {
const arg = args[index] ?? "";
if (arg === "--node" || arg === "--lane") {
index += 1;
continue;
}
if (arg.startsWith("--node=") || arg.startsWith("--lane=")) continue;
result.push(arg);
}
return result;
}
async function runAgentRunQueueRest(action: string | undefined, id: string | undefined, args: string[]): Promise<Record<string, unknown>> {
if (action === "list") return await agentRunRestRequest("agentrun queue list", "GET", `/api/v1/queue/tasks${agentRunQuery(args, ["queue", "state", "cursor", "limit", "updated-after"])}`);
if (action === "commander") return await agentRunRestRequest("agentrun queue commander", "GET", `/api/v1/queue/commander${agentRunQuery(args, ["queue", "reader-id"])}`);
if (action === "stats") return await agentRunRestRequest("agentrun queue stats", "GET", `/api/v1/queue/stats${agentRunQuery(args, ["queue"])}`);
if (action === "show" && id) return await agentRunRestRequest("agentrun queue show", "GET", `/api/v1/queue/tasks/${encodeURIComponent(id)}`);
if (action === "submit") return await submitQueueTaskRest(args);
if (action === "dispatch" && id) return await mutateQueueTaskRest("queue-dispatch", id, "dispatch", queueDispatchBodyFromArgs(args), args);
if (action === "read" && id) return await mutateQueueTaskRest("queue-read", id, "read", { readerId: agentRunOption(args, "reader-id") ?? "cli" }, args);
if (action === "cancel" && id) return await mutateQueueTaskRest("queue-cancel", id, "cancel", cancelBodyFromArgs(args), args);
if (action === "refresh" && id) return await mutateQueueTaskRest("queue-refresh", id, "refresh", {}, args);
throw new AgentRunRestError("validation-failed", `unsupported queue command: ${[action, id].filter(Boolean).join(" ") || "(empty)"}`);
}
async function runAgentRunSessionsRest(action: string | undefined, id: string | undefined, args: string[]): Promise<Record<string, unknown>> {
if (action === "ps") return await agentRunRestRequest("agentrun sessions ps", "GET", `/api/v1/sessions${agentRunQuery(args, ["state", "profile", "backend-profile", "reader-id", "cursor", "limit"])}`);
if (action === "show" && id) return await agentRunRestRequest("agentrun sessions show", "GET", `/api/v1/sessions/${encodeURIComponent(id)}${agentRunQuery(args, ["reader-id"])}`);
if ((action === "trace" || action === "output") && id) return await agentRunRestRequest(`agentrun sessions ${action}`, "GET", `/api/v1/sessions/${encodeURIComponent(id)}/${action}${agentRunQuery(args, ["after-seq", "limit", "run-id"])}`);
if (action === "read" && id) {
const body = { readerId: agentRunOption(args, "reader-id") ?? "cli" };
if (agentRunHasFlag(args, "dry-run")) return agentRunDryRunPlan("session-read", `/api/v1/sessions/${encodeURIComponent(id)}/read`, body, `bun scripts/cli.ts agentrun sessions read ${id} --reader-id ${body.readerId}`);
return await agentRunRestRequest("agentrun sessions read", "POST", `/api/v1/sessions/${encodeURIComponent(id)}/read`, body);
}
if (action === "cancel" && id) {
const body = { action: "cancel", ...cancelBodyFromArgs(args) };
if (agentRunHasFlag(args, "dry-run")) return agentRunDryRunPlan("session-cancel", `/api/v1/sessions/${encodeURIComponent(id)}/control`, body, `bun scripts/cli.ts agentrun sessions cancel ${id}`);
return await agentRunRestRequest("agentrun sessions cancel", "POST", `/api/v1/sessions/${encodeURIComponent(id)}/control`, body);
}
if (action === "send" && id) return await sessionSendRest(id, args);
throw new AgentRunRestError("validation-failed", `unsupported sessions command: ${[action, id].filter(Boolean).join(" ") || "(empty)"}`);
}
async function runAgentRunRunsRest(action: string | undefined, id: string | undefined, args: string[]): Promise<Record<string, unknown>> {
if (action === "create") return await agentRunRestRequest("agentrun runs create", "POST", "/api/v1/runs", await requiredJsonBody(args, "runs create"));
if (action === "show" && id) return await agentRunRestRequest("agentrun runs show", "GET", `/api/v1/runs/${encodeURIComponent(id)}`);
if (action === "events" && id) return await agentRunRestRequest("agentrun runs events", "GET", `/api/v1/runs/${encodeURIComponent(id)}/events${agentRunQuery(args, ["after-seq", "limit"])}`);
if (action === "result" && id) return await agentRunRestRequest("agentrun runs result", "GET", `/api/v1/runs/${encodeURIComponent(id)}/result${agentRunQuery(args, ["command-id", "command"])}`);
if (action === "cancel" && id) return await agentRunRestRequest("agentrun runs cancel", "POST", `/api/v1/runs/${encodeURIComponent(id)}/cancel`, cancelBodyFromArgs(args));
throw new AgentRunRestError("validation-failed", `unsupported runs command: ${[action, id].filter(Boolean).join(" ") || "(empty)"}`);
}
async function runAgentRunCommandsRest(action: string | undefined, id: string | undefined, args: string[]): Promise<Record<string, unknown>> {
if (action === "create" && id) {
const body = await optionalJsonBody(args);
if (!body.type) body.type = agentRunOption(args, "type") ?? "turn";
const idempotencyKey = agentRunOption(args, "idempotency-key");
if (idempotencyKey) body.idempotencyKey = idempotencyKey;
return await agentRunRestRequest("agentrun commands create", "POST", `/api/v1/runs/${encodeURIComponent(id)}/commands`, body);
}
if (action === "show" && id) {
const runId = requiredAgentRunOption(args, ["run-id", "run"], "commands show requires --run-id");
return await agentRunRestRequest("agentrun commands show", "GET", `/api/v1/runs/${encodeURIComponent(runId)}/commands/${encodeURIComponent(id)}`);
}
if (action === "result" && id) {
const runId = requiredAgentRunOption(args, ["run-id", "run"], "commands result requires --run-id");
return await agentRunRestRequest("agentrun commands result", "GET", `/api/v1/runs/${encodeURIComponent(runId)}/commands/${encodeURIComponent(id)}/result`);
}
if (action === "cancel" && id) return await agentRunRestRequest("agentrun commands cancel", "POST", `/api/v1/commands/${encodeURIComponent(id)}/cancel`, cancelBodyFromArgs(args));
throw new AgentRunRestError("validation-failed", `unsupported commands command: ${[action, id].filter(Boolean).join(" ") || "(empty)"}`);
}
async function runAgentRunRunnerRest(action: string | undefined, id: string | undefined, args: string[]): Promise<Record<string, unknown>> {
if (action === "jobs") {
const runId = requiredAgentRunOption(args, ["run-id", "run"], "runner jobs requires --run-id");
return await agentRunRestRequest("agentrun runner jobs", "GET", `/api/v1/runs/${encodeURIComponent(runId)}/runner-jobs${agentRunQuery(args, ["command-id", "command"])}`);
}
if (action === "job-status") {
const runId = requiredAgentRunOption(args, ["run-id", "run"], "runner job-status requires --run-id");
const runnerJobId = id ?? agentRunOption(args, "runner-job-id");
if (!runnerJobId) return await agentRunRestRequest("agentrun runner job-status", "GET", `/api/v1/runs/${encodeURIComponent(runId)}/runner-jobs${agentRunQuery(args, ["command-id", "command"])}`);
return await agentRunRestRequest("agentrun runner job-status", "GET", `/api/v1/runs/${encodeURIComponent(runId)}/runner-jobs/${encodeURIComponent(runnerJobId)}`);
}
if (action === "job") {
const runId = requiredAgentRunOption(args, ["run-id", "run"], "runner job requires --run-id");
const body = queueDispatchBodyFromArgs(args);
const commandId = requiredAgentRunOption(args, ["command-id", "command"], "runner job requires --command-id");
body.commandId = commandId;
if (agentRunHasFlag(args, "dry-run")) return agentRunDryRunPlan("runner-job", `/api/v1/runs/${encodeURIComponent(runId)}/runner-jobs`, body, `bun scripts/cli.ts agentrun runner job --run-id ${runId} --command-id ${commandId}`);
return await agentRunRestRequest("agentrun runner job", "POST", `/api/v1/runs/${encodeURIComponent(runId)}/runner-jobs`, body);
}
throw new AgentRunRestError("validation-failed", `unsupported runner command: ${[action, id].filter(Boolean).join(" ") || "(empty)"}`);
}
async function runAgentRunAipodSpecsRest(action: string | undefined, id: string | undefined, args: string[]): Promise<Record<string, unknown>> {
if (action === "list") return await agentRunRestRequest("agentrun aipod-specs list", "GET", "/api/v1/aipod-specs");
if (action === "show" && id) return await agentRunRestRequest("agentrun aipod-specs show", "GET", `/api/v1/aipod-specs/${encodeURIComponent(id)}`);
if (action === "render" && id) return await agentRunRestRequest("agentrun aipod-specs render", "POST", `/api/v1/aipod-specs/${encodeURIComponent(id)}/render`, await aipodRenderInputFromArgs(args, 2));
if (action === "apply" || action === "set") {
const yaml = readYamlInputFromArgs(args);
const pathValue = id ? `/api/v1/aipod-specs/${encodeURIComponent(id)}` : "/api/v1/aipod-specs";
const method: AgentRunHttpMethod = id ? "PUT" : "POST";
const body = { yaml };
if (agentRunHasFlag(args, "dry-run")) return agentRunDryRunPlan("aipod-spec-apply", pathValue, { yamlBytes: Buffer.byteLength(yaml, "utf8") }, `bun scripts/cli.ts agentrun aipod-specs apply${id ? ` ${id}` : ""} --yaml-stdin`, method);
return await agentRunRestRequest("agentrun aipod-specs apply", method, pathValue, body);
}
if ((action === "delete" || action === "rm") && id) return await agentRunRestRequest("agentrun aipod-specs delete", "DELETE", `/api/v1/aipod-specs/${encodeURIComponent(id)}`);
throw new AgentRunRestError("validation-failed", `unsupported aipod-specs command: ${[action, id].filter(Boolean).join(" ") || "(empty)"}`);
}
async function submitQueueTaskRest(args: string[]): Promise<Record<string, unknown>> {
const aipod = agentRunOption(args, "aipod") ?? agentRunOption(args, "aipod-spec");
if (aipod) {
const rendered = await agentRunRestRequest("agentrun aipod-specs render", "POST", `/api/v1/aipod-specs/${encodeURIComponent(aipod)}/render`, await aipodRenderInputFromArgs(args, 2));
const body = record(record(innerData(rendered)).queueTask);
if (Object.keys(body).length === 0) throw new AgentRunRestError("schema-mismatch", `aipod-spec ${aipod} render did not return queueTask`);
if (agentRunHasFlag(args, "dry-run")) return agentRunDryRunPlan("queue-submit", "/api/v1/queue/tasks", body, queueSubmitConfirmCommand(args, aipod), "POST", { jsonInput: { source: "aipod-spec", aipod, valuesPrinted: false } });
return await agentRunRestRequest("agentrun queue submit", "POST", "/api/v1/queue/tasks", body);
}
const body = await requiredJsonBody(args, "queue submit");
const idempotencyKey = agentRunOption(args, "idempotency-key");
if (idempotencyKey) body.idempotencyKey = idempotencyKey;
if (agentRunHasFlag(args, "dry-run")) return agentRunDryRunPlan("queue-submit", "/api/v1/queue/tasks", body, queueSubmitConfirmCommand(args), "POST", { jsonInput: jsonInputDisclosureFromArgs(args) });
return await agentRunRestRequest("agentrun queue submit", "POST", "/api/v1/queue/tasks", body);
}
async function mutateQueueTaskRest(action: string, taskId: string, suffix: string, body: Record<string, unknown>, args: string[]): Promise<Record<string, unknown>> {
const pathValue = `/api/v1/queue/tasks/${encodeURIComponent(taskId)}/${suffix}`;
if (agentRunHasFlag(args, "dry-run")) return agentRunDryRunPlan(action, pathValue, body, `bun scripts/cli.ts agentrun queue ${suffix} ${taskId}`);
return await agentRunRestRequest(`agentrun queue ${suffix}`, "POST", pathValue, body);
}
async function sessionSendRest(sessionId: string, args: string[]): Promise<Record<string, unknown>> {
const aipod = agentRunOption(args, "aipod") ?? agentRunOption(args, "aipod-spec");
if (aipod) return await sessionSendWithAipodRest(sessionId, aipod, args);
const input = await optionalJsonBody(args);
const prompt = optionalPromptFromArgs(args, 2);
const sendBody = await sessionSendBodyFromRunInput(sessionId, args, input, sessionSendPayloadFromInput(input, prompt));
const response = await agentRunRestRequest("agentrun sessions send", "POST", `/api/v1/sessions/${encodeURIComponent(sessionId)}/send`, sendBody);
return { ok: response.ok !== false, command: "agentrun sessions send", data: innerData(response), bridge: response.bridge };
}
function sessionSendPayloadFromInput(input: Record<string, unknown>, prompt: string | null): Record<string, unknown> {
const payload = record(input.payload);
if (Object.keys(payload).length > 0) return payload;
const text = prompt ?? stringOrNull(input.prompt) ?? stringOrNull(input.message) ?? stringOrNull(input.text);
if (text === null) throw new AgentRunRestError("validation-failed", "send requires payload or non-empty prompt/message/text; use --prompt-stdin, --prompt-file, --prompt, a trailing prompt, or JSON payload");
return { prompt: text };
}
async function sessionSendBodyFromRunInput(sessionId: string, args: string[], input: Record<string, unknown>, payload: Record<string, unknown>): Promise<Record<string, unknown>> {
const fullSendBody = isExplicitSessionSendBody(input);
const runInput = fullSendBody ? record(input.run ?? input.runBase) : input;
const runBody = await sessionRunBodyFromArgs(sessionId, args, runInput);
const runnerDefaults = fullSendBody ? record(input.runnerJob) : {};
const runnerJob = await sessionRunnerJobBody(args, runnerDefaults);
const body: Record<string, unknown> = fullSendBody ? { ...input } : {};
body.run = runBody;
body.payload = record(input.payload);
if (Object.keys(record(body.payload)).length === 0) body.payload = payload;
body.createRunnerJob = agentRunHasFlag(args, "no-runner-job") ? false : input.createRunnerJob !== false;
body.runnerJob = runnerJob;
body.dryRun = agentRunHasFlag(args, "dry-run");
const commandIdempotencyKey = agentRunOption(args, "command-idempotency-key") ?? agentRunOption(args, "idempotency-key");
if (commandIdempotencyKey) body.commandIdempotencyKey = commandIdempotencyKey;
return body;
}
function isExplicitSessionSendBody(input: Record<string, unknown>): boolean {
return isRecord(input.run)
|| isRecord(input.runBase)
|| isRecord(input.payload)
|| isRecord(input.runnerJob)
|| input.createRunnerJob !== undefined
|| input.commandIdempotencyKey !== undefined;
}
async function sessionRunBodyFromArgs(sessionId: string, args: string[], input: Record<string, unknown>): Promise<Record<string, unknown>> {
const existing = await fetchAgentRunSessionOrNull(sessionId, args);
const sessionPolicy = readAgentRunClientConfig().client.sessionPolicy;
const profile = agentRunOption(args, "profile")
?? agentRunOption(args, "backend-profile")
?? stringOrNull(input.backendProfile)
?? stringOrNull(existing?.backendProfile)
?? sessionPolicy.backendProfile;
const body: Record<string, unknown> = { ...input };
body.tenantId = agentRunOption(args, "tenant-id") ?? stringOrNull(body.tenantId) ?? stringOrNull(existing?.tenantId) ?? sessionPolicy.tenantId;
body.projectId = agentRunOption(args, "project-id") ?? stringOrNull(body.projectId) ?? stringOrNull(existing?.projectId) ?? sessionPolicy.projectId;
body.providerId = agentRunOption(args, "provider-id") ?? stringOrNull(body.providerId) ?? stringOrNull(existing?.providerId) ?? sessionPolicy.providerId;
body.backendProfile = profile;
body.workspaceRef = jsonObjectOption(args, "workspace-json") ?? record(body.workspaceRef);
if (Object.keys(record(body.workspaceRef)).length === 0) body.workspaceRef = cloneJsonRecord(sessionPolicy.workspaceRef);
body.executionPolicy = jsonObjectOption(args, "execution-policy-json") ?? record(body.executionPolicy);
if (Object.keys(record(body.executionPolicy)).length === 0) body.executionPolicy = defaultAgentRunExecutionPolicy(profile, sessionPolicy);
const inheritedSessionRef = existing === null ? {} : {
conversationId: existing.conversationId,
threadId: existing.threadId,
metadata: existing.metadata,
};
const sessionRef = { ...inheritedSessionRef, ...record(body.sessionRef) };
const metadata = { ...record(inheritedSessionRef.metadata), ...record(sessionRef.metadata) };
const title = agentRunOption(args, "title");
if (title) metadata.title = title;
body.sessionRef = { ...sessionRef, sessionId, metadata };
return body;
}
async function sessionSendWithAipodRest(sessionId: string, aipod: string, args: string[]): Promise<Record<string, unknown>> {
const rendered = await agentRunRestRequest("agentrun aipod-specs render", "POST", `/api/v1/aipod-specs/${encodeURIComponent(aipod)}/render`, await aipodRenderInputFromArgs(args, 2, { sessionId }));
const renderedData = record(innerData(rendered));
const task = record(renderedData.queueTask);
if (Object.keys(task).length === 0) throw new AgentRunRestError("schema-mismatch", `aipod-spec ${aipod} render did not return queueTask`);
const sessionRef = record(task.sessionRef);
const metadata = record(sessionRef.metadata);
const title = agentRunOption(args, "title") ?? stringOrNull(task.title);
if (title) metadata.title = title;
const sessionPolicy = readAgentRunClientConfig().client.sessionPolicy;
const backendProfile = stringOrNull(task.backendProfile) ?? sessionPolicy.backendProfile;
const executionPolicy = record(task.executionPolicy);
const runBody: Record<string, unknown> = {
tenantId: stringOrNull(task.tenantId) ?? sessionPolicy.tenantId,
projectId: stringOrNull(task.projectId) ?? sessionPolicy.projectId,
providerId: stringOrNull(task.providerId) ?? sessionPolicy.providerId,
backendProfile,
workspaceRef: task.workspaceRef ?? cloneJsonRecord(sessionPolicy.workspaceRef),
sessionRef: { ...sessionRef, sessionId, metadata },
executionPolicy: Object.keys(executionPolicy).length === 0 ? defaultAgentRunExecutionPolicy(backendProfile, sessionPolicy) : executionPolicy,
resourceBundleRef: task.resourceBundleRef,
traceSink: { kind: "aipod-session", aipod, sessionId, valuesPrinted: false },
};
const runnerDefaults = record(record(renderedData.dispatchDefaults).runnerJob);
const sendBody: Record<string, unknown> = {
run: runBody,
payload: task.payload,
createRunnerJob: !agentRunHasFlag(args, "no-runner-job"),
runnerJob: await sessionRunnerJobBody(args, runnerDefaults),
dryRun: agentRunHasFlag(args, "dry-run"),
};
const commandIdempotencyKey = agentRunOption(args, "command-idempotency-key") ?? agentRunOption(args, "idempotency-key");
if (commandIdempotencyKey) sendBody.commandIdempotencyKey = commandIdempotencyKey;
const response = await agentRunRestRequest("agentrun sessions send", "POST", `/api/v1/sessions/${encodeURIComponent(sessionId)}/send`, sendBody);
const data = record(innerData(response));
return { ok: response.ok !== false, command: "agentrun sessions send", data: { ...data, aipod, profile: String(task.backendProfile ?? ""), valuesPrinted: false }, bridge: response.bridge };
}
async function fetchAgentRunSessionOrNull(sessionId: string, args: string[]): Promise<Record<string, unknown> | null> {
try {
return record(innerData(await agentRunRestRequest("agentrun sessions show", "GET", `/api/v1/sessions/${encodeURIComponent(sessionId)}${agentRunQuery(args, ["reader-id"])}`)));
} catch (error) {
if (error instanceof AgentRunRestError && error.httpStatus === 404) return null;
throw error;
}
}
async function sessionRunnerJobBody(args: string[], defaults: Record<string, unknown> = {}): Promise<Record<string, unknown>> {
const runnerBody = { ...defaults, ...(await optionalRunnerJsonBody(args)) };
copyAgentRunOptions(args, runnerBody, ["image", "namespace", "attempt-id", "runner-id", "source-commit", "service-account-name"]);
const managerUrl = agentRunOption(args, "runner-manager-url");
if (managerUrl !== null) runnerBody.managerUrl = managerUrl;
const runnerIdempotencyKey = agentRunOption(args, "runner-idempotency-key");
if (runnerIdempotencyKey !== null) runnerBody.idempotencyKey = runnerIdempotencyKey;
return runnerBody;
}
async function agentRunRestRequest(command: string, method: AgentRunHttpMethod, pathValue: string, body?: unknown): Promise<Record<string, unknown>> {
const clientConfig = readAgentRunClientConfig();
if (activeAgentRunRestTarget !== null) return await agentRunLaneRestRequest(command, method, pathValue, body, clientConfig, activeAgentRunRestTarget);
const auth = resolveAgentRunAuth(clientConfig);
const bridgeBase = agentRunRestBridgeMetadata(clientConfig, auth, method, pathValue);
const startedAt = Date.now();
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), clientConfig.manager.timeoutMs);
let response: Response;
try {
const headers: Record<string, string> = {
[clientConfig.auth.header]: `${clientConfig.auth.scheme} ${auth.value}`,
};
const init: RequestInit = { method, headers, signal: controller.signal };
if (body !== undefined) {
headers["content-type"] = "application/json";
init.body = JSON.stringify(body);
}
response = await fetch(new URL(pathValue, clientConfig.manager.baseUrl), init);
} catch (error) {
throw new AgentRunRestError("agentrun-unreachable", `AgentRun server is unreachable for ${method} ${pathValue}: ${error instanceof Error ? error.message : String(error)}`, { bridge: { ...bridgeBase, elapsedMs: Date.now() - startedAt } });
} finally {
clearTimeout(timeout);
}
const text = await response.text();
const bridge = { ...bridgeBase, httpStatus: response.status, elapsedMs: Date.now() - startedAt };
if (text.trim().length === 0) throw new AgentRunRestError("schema-mismatch", `AgentRun server returned an empty response for ${method} ${pathValue}`, { bridge, httpStatus: response.status });
let envelope: Record<string, unknown>;
try {
envelope = record(JSON.parse(text) as unknown);
} catch {
throw new AgentRunRestError("schema-mismatch", `AgentRun server returned non-JSON response for ${method} ${pathValue}`, { bridge, httpStatus: response.status });
}
if (response.status === 401 || response.status === 403) throw new AgentRunRestError("auth-failed", stringOrNull(envelope.message) ?? "AgentRun API key was rejected", { bridge, httpStatus: response.status, details: safeAgentRunEnvelope(envelope) });
if (!response.ok) {
const details = response.status === 404
? addAgentRunNotFoundLookupHint(safeAgentRunEnvelope(envelope), clientConfig, method, pathValue, null)
: safeAgentRunEnvelope(envelope);
throw new AgentRunRestError(response.status === 404 ? "not-found" : "validation-failed", stringOrNull(envelope.message) ?? `AgentRun request failed with HTTP ${response.status}`, { bridge, httpStatus: response.status, details });
}
if (envelope.ok !== true) {
const failureKind = normalizeAgentRunFailureKind(stringOrNull(envelope.failureKind), response.status);
throw new AgentRunRestError(failureKind, stringOrNull(envelope.message) ?? `AgentRun request failed for ${method} ${pathValue}`, { bridge, httpStatus: response.status, details: safeAgentRunEnvelope(envelope) });
}
return {
ok: true,
command,
data: envelope.data ?? null,
bridge,
};
}
async function agentRunLaneRestRequest(command: string, method: AgentRunHttpMethod, pathValue: string, body: unknown, clientConfig: AgentRunClientConfig, target: AgentRunRestTarget): Promise<Record<string, unknown>> {
const bridgeBase = agentRunLaneRestBridgeMetadata(clientConfig, target, method, pathValue);
const startedAt = Date.now();
const request = {
method,
path: pathValue,
body: body ?? null,
baseUrl: target.spec.runtime.internalBaseUrl,
timeoutMs: clientConfig.manager.timeoutMs,
authEnv: clientConfig.auth.env,
header: clientConfig.auth.header,
scheme: clientConfig.auth.scheme,
valuesPrinted: false,
};
const captureResult = await capture(target.config, target.spec.nodeKubeRoute, ["sh", "--", agentRunLaneRestProxyScript(target.spec, request)]);
const captureSummary = compactCapture(captureResult, { full: false, stdoutTailChars: 1200, stderrTailChars: 2000 });
const bridge = { ...bridgeBase, elapsedMs: Date.now() - startedAt, capture: captureSummary };
if (captureResult.exitCode !== 0) {
throw new AgentRunRestError("agentrun-unreachable", `AgentRun lane ${target.spec.nodeId}/${target.spec.lane} manager proxy failed for ${method} ${pathValue}`, { bridge });
}
const proxy = captureJsonPayload(captureResult);
if (proxy.ok !== true) {
throw new AgentRunRestError("agentrun-unreachable", stringOrNull(proxy.message) ?? `AgentRun lane ${target.spec.nodeId}/${target.spec.lane} manager proxy failed`, { bridge: { ...bridge, proxy }, details: pickCompact(proxy, ["ok", "failureKind", "message", "elapsedMs", "valuesPrinted"]) });
}
const httpStatus = nonNegativeIntegerOrNull(proxy.httpStatus) ?? 0;
const text = stringOrNull(proxy.text) ?? "";
const responseBridge = { ...bridge, httpStatus, proxyElapsedMs: nonNegativeIntegerOrNull(proxy.elapsedMs) };
if (text.trim().length === 0) throw new AgentRunRestError("schema-mismatch", `AgentRun server returned an empty response for ${method} ${pathValue}`, { bridge: responseBridge, httpStatus });
let envelope: Record<string, unknown>;
try {
envelope = record(JSON.parse(text) as unknown);
} catch {
throw new AgentRunRestError("schema-mismatch", `AgentRun server returned non-JSON response for ${method} ${pathValue}`, { bridge: responseBridge, httpStatus });
}
if (httpStatus === 401 || httpStatus === 403) throw new AgentRunRestError("auth-failed", stringOrNull(envelope.message) ?? "AgentRun API key was rejected", { bridge: responseBridge, httpStatus, details: safeAgentRunEnvelope(envelope) });
if (httpStatus < 200 || httpStatus >= 300) {
const details = httpStatus === 404
? addAgentRunNotFoundLookupHint(safeAgentRunEnvelope(envelope), clientConfig, method, pathValue, target)
: safeAgentRunEnvelope(envelope);
throw new AgentRunRestError(httpStatus === 404 ? "not-found" : "validation-failed", stringOrNull(envelope.message) ?? `AgentRun request failed with HTTP ${httpStatus}`, { bridge: responseBridge, httpStatus, details });
}
if (envelope.ok !== true) {
const failureKind = normalizeAgentRunFailureKind(stringOrNull(envelope.failureKind), httpStatus);
throw new AgentRunRestError(failureKind, stringOrNull(envelope.message) ?? `AgentRun request failed for ${method} ${pathValue}`, { bridge: responseBridge, httpStatus, details: safeAgentRunEnvelope(envelope) });
}
return {
ok: true,
command,
data: envelope.data ?? null,
bridge: responseBridge,
};
}
function agentRunLaneRestProxyScript(spec: AgentRunLaneSpec, request: Record<string, unknown>): string {
const requestB64 = Buffer.from(JSON.stringify(request), "utf8").toString("base64");
const evalScript = String.raw`
const startedAt = Date.now();
try {
const input = JSON.parse(Buffer.from(process.env.AGENTRUN_LANE_REST_REQUEST_B64 || "", "base64").toString("utf8"));
const headers = {};
const authEnv = String(input.authEnv || "HWLAB_API_KEY");
const apiKey = process.env[authEnv] || process.env.AGENTRUN_API_KEY || process.env.HWLAB_API_KEY || "";
if (apiKey && input.header && input.scheme) headers[String(input.header)] = String(input.scheme) + " " + apiKey;
let body = undefined;
if (input.body !== null && input.body !== undefined) {
headers["content-type"] = "application/json";
body = JSON.stringify(input.body);
}
const controller = new AbortController();
const timeoutMs = Number(input.timeoutMs || 15000);
const timeout = setTimeout(() => controller.abort(), timeoutMs);
let response;
try {
response = await fetch(new URL(String(input.path || "/"), String(input.baseUrl || "http://127.0.0.1:8080")).toString(), {
method: String(input.method || "GET"),
headers,
body,
signal: controller.signal,
});
} finally {
clearTimeout(timeout);
}
const text = await response.text();
console.log(JSON.stringify({ ok: true, httpStatus: response.status, statusText: response.statusText, text, elapsedMs: Date.now() - startedAt, valuesPrinted: false }));
} catch (error) {
console.log(JSON.stringify({ ok: false, failureKind: "manager-pod-fetch-failed", message: error instanceof Error ? error.message : String(error), elapsedMs: Date.now() - startedAt, valuesPrinted: false }));
}
`;
return [
"set -eu",
`namespace=${shQuote(spec.runtime.namespace)}`,
`deployment=${shQuote(spec.runtime.managerDeployment)}`,
`request_b64=${shQuote(requestB64)}`,
`kubectl -n "$namespace" exec deploy/"$deployment" -- env AGENTRUN_LANE_REST_REQUEST_B64="$request_b64" bun --eval ${shQuote(evalScript)}`,
].join("\n");
}
function resolveAgentRunRestTarget(config: UniDeskConfig | null, options: AgentRunRestTargetOptions): AgentRunRestTarget | null {
if (options.node === null && options.lane === null) return null;
if (config === null) throw new AgentRunRestError("validation-failed", "--node/--lane resource queries require UniDesk config");
const { configPath, spec } = resolveAgentRunLaneTarget(options);
return { config, configPath, spec };
}
async function withAgentRunRestTarget<T>(target: AgentRunRestTarget | null, action: () => Promise<T>): Promise<T> {
const previous = activeAgentRunRestTarget;
activeAgentRunRestTarget = target;
try {
return await action();
} finally {
activeAgentRunRestTarget = previous;
}
}
function renderAgentRunRestError(command: string, error: AgentRunRestError, options: AgentRunResourceOptions): RenderedCliResult {
const payload = error.toPayload(command);
if (options.raw || options.output === "json" || options.output === "yaml") return renderMachine(command, payload, options.output === "yaml" ? "yaml" : "json", false);
const laneHint = record(record(payload.agentrun).laneAwareLookup);
const currentEndpoint = record(laneHint.currentEndpoint);
const nextCommands = Array.isArray(laneHint.nextCommands)
? laneHint.nextCommands.filter((value): value is string => typeof value === "string" && value.length > 0).slice(0, 4)
: [];
const hintLines = Object.keys(laneHint).length === 0
? []
: [
"",
"Lane lookup hint:",
` Current baseUrl: ${String(currentEndpoint.baseUrl ?? "(unknown)")}`,
` Config: ${String(currentEndpoint.configPath ?? "(unknown)")}`,
` ${String(laneHint.summary ?? "The resource was not found on the currently configured AgentRun manager endpoint.")}`,
];
const nextLines = nextCommands.length > 0
? nextCommands
: [
"Check config/agentrun.yaml manager.baseUrl and the AgentRun API route.",
"Verify HWLAB_API_KEY is present in env or in the configured auth.file without printing the key.",
];
return renderedCliResult(false, command, [
`Error: ${payload.failureKind}`,
String(payload.message ?? ""),
...hintLines,
"",
"Next:",
...nextLines.map((line) => ` ${line}`),
].join("\n"));
}
class AgentRunRestError extends Error {
readonly failureKind: AgentRunFailureKind;
readonly bridge: Record<string, unknown> | null;
readonly httpStatus: number | null;
readonly details: Record<string, unknown> | null;
constructor(failureKind: AgentRunFailureKind, message: string, options: { bridge?: Record<string, unknown>; httpStatus?: number; details?: Record<string, unknown> } = {}) {
super(message);
this.name = "AgentRunRestError";
this.failureKind = failureKind;
this.bridge = options.bridge ?? null;
this.httpStatus = options.httpStatus ?? null;
this.details = options.details ?? null;
}
toPayload(command: string): Record<string, unknown> {
const transport = stringOrNull(this.bridge?.mode) ?? "direct-http";
return {
ok: false,
command,
failureKind: this.failureKind,
message: this.message,
transport,
clientRole: "render-only",
...(this.httpStatus === null ? {} : { httpStatus: this.httpStatus }),
...(this.bridge === null ? {} : { bridge: this.bridge }),
...(this.details === null ? {} : { agentrun: this.details }),
valuesPrinted: false,
};
}
}
function readAgentRunClientConfig(env: NodeJS.ProcessEnv = process.env): AgentRunClientConfig {
const configPath = env.AGENTRUN_CLIENT_CONFIG ?? rootPath("config", "agentrun.yaml");
if (!existsSync(configPath)) throw new AgentRunRestError("validation-failed", `AgentRun client config not found: ${configPath}`);
try {
const parsed = parseAgentRunClientConfigYaml(readFileSync(configPath, "utf8"), configPath);
return { ...parsed, sourcePath: configPath };
} catch (error) {
if (error instanceof AgentRunRestError) throw error;
throw new AgentRunRestError("validation-failed", `AgentRun client config invalid: ${error instanceof Error ? error.message : String(error)}`, {
bridge: { mode: "direct-http", clientRole: "render-only", configPath },
});
}
}
export function parseAgentRunClientConfigYaml(raw: string, sourcePath = "config/agentrun.yaml"): AgentRunClientConfig {
const input = record(Bun.YAML.parse(raw) as unknown);
validateAgentRunConfigEnvelope(input, sourcePath);
const manager = record(input.manager);
const auth = record(input.auth);
const client = record(input.client);
const baseUrl = stringFieldFromRecord(manager, "baseUrl", "manager");
try {
const parsed = new URL(baseUrl);
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") throw new Error("invalid protocol");
} catch {
throw new Error(`${sourcePath}: manager.baseUrl must be an http(s) URL`);
}
const timeoutMs = numberFieldFromRecord(manager, "timeoutMs", "manager", { min: 1, max: 60_000 });
const header = stringFieldFromRecord(auth, "header", "auth");
const scheme = stringFieldFromRecord(auth, "scheme", "auth");
if (header.toLowerCase() !== "authorization") throw new Error(`${sourcePath}: auth.header must be Authorization`);
if (scheme !== "Bearer") throw new Error(`${sourcePath}: auth.scheme must be Bearer`);
const role = stringFieldFromRecord(client, "role", "client");
const transport = stringFieldFromRecord(client, "transport", "client");
if (role !== "render-only") throw new Error(`${sourcePath}: client.role must be render-only`);
if (transport !== "direct-http") throw new Error(`${sourcePath}: client.transport must be direct-http`);
const sessionPolicy = readAgentRunSessionPolicyConfig(client, sourcePath);
const publicExposure = readAgentRunPublicExposureConfig(input.publicExposure, baseUrl.replace(/\/+$/u, "/"), sourcePath);
return {
sourcePath,
manager: {
baseUrl: baseUrl.replace(/\/+$/u, "/"),
timeoutMs,
},
auth: {
env: stringFieldFromRecord(auth, "env", "auth"),
file: stringFieldFromRecord(auth, "file", "auth"),
header,
scheme,
},
client: {
role,
transport,
sessionPolicy,
},
publicExposure,
};
}
function readAgentRunSessionPolicyConfig(client: Record<string, unknown>, sourcePath: string): AgentRunSessionPolicyConfig {
const pathValue = "client.sessionPolicy";
const sessionPolicy = record(client.sessionPolicy);
const workspaceRef = record(sessionPolicy.workspaceRef);
const executionPolicy = record(sessionPolicy.executionPolicy);
const secretScope = record(executionPolicy.secretScope);
stringFieldFromRecord(workspaceRef, "kind", `${pathValue}.workspaceRef`);
stringFieldFromRecord(executionPolicy, "sandbox", `${pathValue}.executionPolicy`);
stringFieldFromRecord(executionPolicy, "approval", `${pathValue}.executionPolicy`);
positiveIntegerFieldFromRecord(executionPolicy, "timeoutMs", `${pathValue}.executionPolicy`);
stringFieldFromRecord(executionPolicy, "network", `${pathValue}.executionPolicy`);
booleanFieldFromRecord(secretScope, "allowCredentialEcho", `${pathValue}.executionPolicy.secretScope`);
return {
sourcePath,
tenantId: stringFieldFromRecord(sessionPolicy, "tenantId", pathValue),
projectId: stringFieldFromRecord(sessionPolicy, "projectId", pathValue),
providerId: stringFieldFromRecord(sessionPolicy, "providerId", pathValue),
backendProfile: stringFieldFromRecord(sessionPolicy, "backendProfile", pathValue),
workspaceRef: cloneJsonRecord(workspaceRef),
executionPolicy: cloneJsonRecord(executionPolicy),
};
}
function validateAgentRunConfigEnvelope(input: Record<string, unknown>, sourcePath: string): void {
if (input.version !== 1) throw new Error(`${sourcePath}: version must be 1`);
if (input.kind !== "AgentRunConfig") throw new Error(`${sourcePath}: kind must be AgentRunConfig`);
const metadata = record(input.metadata);
if (stringFieldFromRecord(metadata, "name", "metadata") !== "agentrun") throw new Error(`${sourcePath}: metadata.name must be agentrun`);
}
function readAgentRunPublicExposureConfig(value: unknown, managerBaseUrl: string, sourcePath: string): AgentRunPublicExposure | null {
if (value === undefined || value === null) return null;
const exposure = record(value);
if (exposure.enabled !== true) return null;
const proxyName = stringFieldFromRecord(exposure, "proxyName", "publicExposure");
if (!/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/u.test(proxyName)) throw new Error(`${sourcePath}: publicExposure.proxyName must be a DNS label`);
const remotePort = numberFieldFromRecord(exposure, "remotePort", "publicExposure", { min: 1, max: 65_535 });
const publicBaseUrl = normalizedHttpUrl(stringFieldFromRecord(exposure, "publicBaseUrl", "publicExposure"), `${sourcePath}: publicExposure.publicBaseUrl`);
const masterBaseUrl = normalizedHttpUrl(stringFieldFromRecord(exposure, "masterBaseUrl", "publicExposure"), `${sourcePath}: publicExposure.masterBaseUrl`);
if (!publicBaseUrl.startsWith("https://")) throw new Error(`${sourcePath}: publicExposure.publicBaseUrl must use https://`);
if (publicBaseUrl !== managerBaseUrl) throw new Error(`${sourcePath}: manager.baseUrl must match publicExposure.publicBaseUrl`);
if (!masterBaseUrl.startsWith(`http://127.0.0.1:${remotePort}`)) throw new Error(`${sourcePath}: publicExposure.masterBaseUrl must point to http://127.0.0.1:${remotePort}`);
const masterFrps = record(exposure.masterFrps);
const masterCaddy = record(exposure.masterCaddy);
const caddyEnabled = masterCaddy.enabled === true;
const domain = stringFieldFromRecord(masterCaddy, "domain", "publicExposure.masterCaddy");
if (!/^[a-z0-9.-]+$/u.test(domain) || !domain.endsWith(".nip.io")) throw new Error(`${sourcePath}: publicExposure.masterCaddy.domain must be a nip.io hostname`);
const caddyUpstreamBaseUrl = normalizedHttpUrl(stringFieldFromRecord(masterCaddy, "upstreamBaseUrl", "publicExposure.masterCaddy"), `${sourcePath}: publicExposure.masterCaddy.upstreamBaseUrl`);
if (caddyUpstreamBaseUrl !== masterBaseUrl) throw new Error(`${sourcePath}: publicExposure.masterCaddy.upstreamBaseUrl must match publicExposure.masterBaseUrl`);
return {
enabled: true,
proxyName,
remotePort,
publicBaseUrl,
masterBaseUrl,
masterFrps: {
configPath: absolutePathField(masterFrps, "configPath", "publicExposure.masterFrps", sourcePath),
containerName: stringFieldFromRecord(masterFrps, "containerName", "publicExposure.masterFrps"),
},
masterCaddy: {
enabled: caddyEnabled,
domain,
configPath: absolutePathField(masterCaddy, "configPath", "publicExposure.masterCaddy", sourcePath),
serviceName: stringFieldFromRecord(masterCaddy, "serviceName", "publicExposure.masterCaddy"),
upstreamBaseUrl: caddyUpstreamBaseUrl,
responseHeaderTimeoutSeconds: numberFieldFromRecord(masterCaddy, "responseHeaderTimeoutSeconds", "publicExposure.masterCaddy", { min: 1, max: 600 }),
},
};
}
function normalizedHttpUrl(value: string, label: string): string {
let url: URL;
try {
url = new URL(value);
} catch {
throw new Error(`${label} must be an http(s) URL`);
}
if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error(`${label} must be an http(s) URL`);
return value.replace(/\/+$/u, "/");
}
function absolutePathField(obj: Record<string, unknown>, key: string, pathValue: string, sourcePath: string): string {
const value = stringFieldFromRecord(obj, key, pathValue);
if (!value.startsWith("/")) throw new Error(`${sourcePath}: ${pathValue}.${key} must be an absolute path`);
return value;
}
export function resolveAgentRunAuth(config: AgentRunClientConfig, env: NodeJS.ProcessEnv = process.env): AgentRunResolvedAuth {
const envValue = env[config.auth.env]?.trim();
if (envValue) return { source: "env", value: envValue };
if (existsSync(config.auth.file)) {
const fromFile = readEnvValueFromFile(config.auth.file, config.auth.env);
if (fromFile !== null) return { source: "file", value: fromFile };
}
throw new AgentRunRestError("auth-missing", `${config.auth.env} is required for direct AgentRun REST access`, {
bridge: {
mode: "direct-http",
clientRole: "render-only",
configPath: config.sourcePath,
auth: { env: config.auth.env, file: config.auth.file, source: "missing", header: config.auth.header, scheme: config.auth.scheme, valuesPrinted: false },
},
});
}
function readEnvValueFromFile(pathValue: string, envName: string): string | null {
const text = readFileSync(pathValue, "utf8");
for (const line of text.split(/\r?\n/u)) {
const trimmed = line.trim();
if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
const normalized = trimmed.startsWith("export ") ? trimmed.slice("export ".length).trim() : trimmed;
const index = normalized.indexOf("=");
if (index <= 0) continue;
if (normalized.slice(0, index).trim() !== envName) continue;
const raw = normalized.slice(index + 1).trim();
const value = raw.replace(/^['"]|['"]$/gu, "");
return value.length > 0 ? value : null;
}
return null;
}
function agentRunRestBridgeMetadata(config: AgentRunClientConfig, auth: AgentRunResolvedAuth, method: AgentRunHttpMethod, pathValue: string): Record<string, unknown> {
return {
mode: "direct-http",
clientRole: "render-only",
compatibility: "no-hwlab-runtime-proxy-no-ssh-official-cli",
configPath: config.sourcePath,
baseUrl: config.manager.baseUrl,
request: { method, path: pathValue, timeoutMs: config.manager.timeoutMs },
auth: { env: config.auth.env, file: config.auth.file, source: auth.source, header: config.auth.header, scheme: config.auth.scheme, valuesPrinted: false },
};
}
function agentRunLaneRestBridgeMetadata(config: AgentRunClientConfig, target: AgentRunRestTarget, method: AgentRunHttpMethod, pathValue: string): Record<string, unknown> {
return {
mode: "lane-k8s-service-proxy",
clientRole: "render-only",
configPath: config.sourcePath,
laneConfigPath: target.configPath,
node: target.spec.nodeId,
lane: target.spec.lane,
kubeRoute: target.spec.nodeKubeRoute,
namespace: target.spec.runtime.namespace,
managerDeployment: target.spec.runtime.managerDeployment,
baseUrl: target.spec.runtime.internalBaseUrl,
request: { method, path: pathValue, timeoutMs: config.manager.timeoutMs },
auth: { source: "manager-pod-env", env: config.auth.env, fallbackEnv: "AGENTRUN_API_KEY", header: config.auth.header, scheme: config.auth.scheme, valuesPrinted: false },
valuesPrinted: false,
};
}
function addAgentRunNotFoundLookupHint(details: Record<string, unknown>, config: AgentRunClientConfig, method: AgentRunHttpMethod, pathValue: string, target: AgentRunRestTarget | null): Record<string, unknown> {
const resource = agentRunResourceFromPath(pathValue);
const laneConfig = readAgentRunLaneLookupConfig(config.sourcePath);
const candidateLanes = laneConfig?.lanes ?? [];
const nextCommands = candidateLanes
.filter((lane) => typeof lane.node === "string" && lane.node.length > 0 && typeof lane.lane === "string" && lane.lane.length > 0)
.slice(0, 4)
.map((lane) => `bun scripts/cli.ts agentrun control-plane status --node ${lane.node} --lane ${lane.lane}`);
return {
...details,
laneAwareLookup: {
kind: "agentrun-resource-not-found-on-current-endpoint",
summary: resource === null
? "The request returned 404 on the currently configured AgentRun manager endpoint; a resource from another lane will not be visible through this baseUrl."
: `${resource.kind}/${resource.id} returned 404 on the currently configured AgentRun manager endpoint; if it was created on another lane, inspect that lane before treating the resource as lost.`,
request: {
method,
path: pathValue,
...(resource === null ? {} : { resource }),
},
currentEndpoint: {
transport: target === null ? "direct-http" : "lane-k8s-service-proxy",
baseUrl: target === null ? config.manager.baseUrl : target.spec.runtime.internalBaseUrl,
configPath: config.sourcePath,
defaultNode: laneConfig?.defaultNode ?? null,
defaultLane: laneConfig?.defaultLane ?? null,
selectedNode: target?.spec.nodeId ?? null,
selectedLane: target?.spec.lane ?? null,
},
candidateLanes,
nextCommands,
note: target === null
? "Pass --node <node> --lane <lane> to query a YAML-declared non-default AgentRun lane."
: "The resource was not found on the selected AgentRun lane; verify the run/session id and the HWLAB runtime lane that created it.",
valuesPrinted: false,
},
};
}
function agentRunResourceFromPath(pathValue: string): Record<string, string> | null {
const run = pathValue.match(/\/runs\/([^/?#]+)/u);
if (run?.[1] !== undefined) return { kind: "run", id: decodeURIComponent(run[1]) };
const session = pathValue.match(/\/sessions\/([^/?#]+)/u);
if (session?.[1] !== undefined) return { kind: "session", id: decodeURIComponent(session[1]) };
const command = pathValue.match(/\/commands\/([^/?#]+)/u);
if (command?.[1] !== undefined) return { kind: "command", id: decodeURIComponent(command[1]) };
const task = pathValue.match(/\/queue\/tasks\/([^/?#]+)/u);
if (task?.[1] !== undefined) return { kind: "task", id: decodeURIComponent(task[1]) };
return null;
}
function readAgentRunLaneLookupConfig(configPath: string): { defaultNode: string | null; defaultLane: string | null; lanes: Record<string, unknown>[] } | null {
try {
const raw = readFileSync(configPath, "utf8");
const parsed = record(Bun.YAML.parse(raw) as unknown);
const controlPlane = record(parsed.controlPlane);
const defaultTarget = record(controlPlane.default);
const lanes = record(controlPlane.lanes);
return {
defaultNode: stringOrNull(defaultTarget.node),
defaultLane: stringOrNull(defaultTarget.lane),
lanes: Object.entries(lanes).map(([laneName, rawLane]) => {
const lane = record(rawLane);
const runtime = record(lane.runtime);
return {
lane: laneName,
node: stringOrNull(lane.node) ?? stringOrNull(lane.nodeId),
version: stringOrNull(lane.version),
namespace: stringOrNull(runtime.namespace),
internalBaseUrl: stringOrNull(runtime.internalBaseUrl),
isDefault: laneName === stringOrNull(defaultTarget.lane),
};
}),
};
} catch {
return null;
}
}
function agentRunDryRunPlan(action: string, pathValue: string, body: Record<string, unknown>, confirmCommand: string, method: AgentRunHttpMethod = "POST", extra: Record<string, unknown> = {}): Record<string, unknown> {
return {
ok: true,
action,
dryRun: true,
mutation: false,
transport: "direct-http",
clientRole: "render-only",
request: {
method,
path: pathValue,
bodyKeys: Object.keys(body),
valuesPrinted: false,
},
...extra,
next: { confirm: confirmCommand },
valuesPrinted: false,
};
}
function agentRunQuery(args: string[], names: string[]): string {
const params = new URLSearchParams();
const keyMap: Record<string, string> = {
"after-seq": "afterSeq",
"backend-profile": "backendProfile",
"command": "commandId",
"command-id": "commandId",
"reader-id": "readerId",
"run-id": "runId",
"updated-after": "updatedAfter",
};
for (const name of names) {
const value = agentRunOption(args, name);
if (value !== null) params.set(keyMap[name] ?? name, value);
}
const query = params.toString();
return query.length > 0 ? `?${query}` : "";
}
function agentRunOption(args: string[], name: string): string | null {
const flag = `--${name}`;
for (let index = 0; index < args.length; index += 1) {
const arg = args[index] ?? "";
if (arg === flag) {
const value = args[index + 1];
if (value === undefined || value.startsWith("--")) throw new AgentRunRestError("validation-failed", `${flag} requires a value`);
return value;
}
if (arg.startsWith(`${flag}=`)) return arg.slice(flag.length + 1);
}
return null;
}
function requiredAgentRunOption(args: string[], names: string[], message: string): string {
for (const name of names) {
const value = agentRunOption(args, name);
if (value !== null) return value;
}
throw new AgentRunRestError("validation-failed", message);
}
function agentRunHasFlag(args: string[], name: string): boolean {
const flag = `--${name}`;
return args.includes(flag) || args.some((arg) => arg.startsWith(`${flag}=`));
}
async function requiredJsonBody(args: string[], context: string): Promise<Record<string, unknown>> {
const body = await optionalJsonBody(args);
if (Object.keys(body).length === 0) throw new AgentRunRestError("validation-failed", `${context} requires --json-stdin or --json-file <path>`);
return body;
}
async function optionalJsonBody(args: string[]): Promise<Record<string, unknown>> {
const raw = readJsonTextFromArgs(args, "json");
if (raw === null) return {};
return record(JSON.parse(raw) as unknown);
}
async function optionalRunnerJsonBody(args: string[]): Promise<Record<string, unknown>> {
const raw = readJsonTextFromArgs(args, "runner-json");
if (raw === null) return {};
return record(JSON.parse(raw) as unknown);
}
function readJsonTextFromArgs(args: string[], prefix: "json" | "runner-json"): string | null {
if (agentRunHasFlag(args, `${prefix}-stdin`)) return readFileSync(0, "utf8");
const file = agentRunOption(args, `${prefix}-file`);
if (file !== null) return file === "-" ? readFileSync(0, "utf8") : readFileSync(file, "utf8");
return null;
}
function readYamlInputFromArgs(args: string[]): string {
if (agentRunHasFlag(args, "yaml-stdin")) return readFileSync(0, "utf8");
const file = agentRunOption(args, "yaml-file");
if (file !== null) return file === "-" ? readFileSync(0, "utf8") : readFileSync(file, "utf8");
throw new AgentRunRestError("validation-failed", "aipod-spec YAML input is required; use --yaml-stdin or --yaml-file <file>");
}
async function aipodRenderInputFromArgs(args: string[], trailingPromptStart: number, overrides: Record<string, unknown> = {}): Promise<Record<string, unknown>> {
const input = await optionalJsonBody(args);
const prompt = optionalPromptFromArgs(args, trailingPromptStart);
if (prompt !== null) input.prompt = prompt;
copyAgentRunOptions(args, input, ["tenant-id", "project-id", "queue", "lane", "title", "provider-id", "idempotency-key", "session-id"]);
const profile = agentRunOption(args, "profile") ?? agentRunOption(args, "backend-profile");
if (profile) input.backendProfile = profile;
const priority = agentRunOption(args, "priority");
if (priority) input.priority = Number(priority);
const workspaceRef = jsonObjectOption(args, "workspace-json");
if (workspaceRef !== null) input.workspaceRef = workspaceRef;
return { ...input, ...overrides };
}
function readPromptFromArgs(args: string[], trailingStart: number): string {
const prompt = optionalPromptFromArgs(args, trailingStart);
if (prompt === null) throw new AgentRunRestError("validation-failed", "prompt is required; use --prompt-stdin, --prompt-file, --prompt, or a trailing prompt");
return prompt;
}
function optionalPromptFromArgs(args: string[], trailingStart: number): string | null {
if (agentRunHasFlag(args, "prompt-stdin") || agentRunHasFlag(args, "stdin")) return readFileSync(0, "utf8");
const promptFile = agentRunOption(args, "prompt-file");
if (promptFile !== null) return promptFile === "-" ? readFileSync(0, "utf8") : readFileSync(promptFile, "utf8");
const prompt = agentRunOption(args, "prompt");
if (prompt !== null) return prompt;
const trailing = args.slice(trailingStart).filter((arg) => !arg.startsWith("-"));
return trailing.length > 0 ? trailing.join(" ") : null;
}
function queueDispatchBodyFromArgs(args: string[]): Record<string, unknown> {
const body = recordFromMaybeJson(args);
copyAgentRunOptions(args, body, ["idempotency-key", "image", "namespace", "attempt-id", "runner-id", "source-commit", "service-account-name"]);
const managerUrl = agentRunOption(args, "runner-manager-url");
if (managerUrl) body.managerUrl = managerUrl;
return body;
}
function recordFromMaybeJson(args: string[]): Record<string, unknown> {
const raw = readJsonTextFromArgs(args, "json");
if (raw === null) return {};
return record(JSON.parse(raw) as unknown);
}
function cancelBodyFromArgs(args: string[]): Record<string, unknown> {
const reason = agentRunOption(args, "reason");
return reason === null ? {} : { reason };
}
function copyAgentRunOptions(args: string[], target: Record<string, unknown>, flagNames: string[]): void {
for (const flagName of flagNames) {
const value = agentRunOption(args, flagName);
if (value === null) continue;
target[camelCaseFlag(flagName)] = value;
}
}
function camelCaseFlag(flagName: string): string {
return flagName.replace(/-([a-z])/gu, (_, letter: string) => String(letter).toUpperCase());
}
function jsonObjectOption(args: string[], flagName: string): Record<string, unknown> | null {
const value = agentRunOption(args, flagName);
if (value === null) return null;
return record(JSON.parse(value) as unknown);
}
function queueSubmitConfirmCommand(args: string[], aipod?: string): string {
const dryRunless = args.filter((arg) => arg !== "--dry-run").join(" ");
return `bun scripts/cli.ts agentrun queue submit${aipod ? ` --aipod ${aipod}` : ""}${dryRunless.length > 0 ? ` ${dryRunless}` : ""}`.trim();
}
function jsonInputDisclosureFromArgs(args: string[]): Record<string, unknown> {
return {
source: agentRunHasFlag(args, "json-stdin") ? "stdin" : agentRunOption(args, "json-file") ?? "<task.json>",
valuesPrinted: false,
};
}
function defaultAgentRunExecutionPolicy(profile: string, sessionPolicy: AgentRunSessionPolicyConfig = readAgentRunClientConfig().client.sessionPolicy): Record<string, unknown> {
const { spec } = resolveAgentRunLaneTarget({});
const credentials = agentRunProviderCredentialRefs(spec, profile);
if (credentials.length === 0) {
throw new AgentRunRestError("validation-failed", `config/agentrun.yaml has no providerCredential Secret binding for backendProfile=${profile} on default lane ${spec.nodeId}/${spec.lane}`);
}
const providerCredentials = credentials.map((credential) => {
if (credential.secretRef.namespace !== spec.runtime.namespace) {
throw new AgentRunRestError("validation-failed", `providerCredential ${profile} Secret ${credential.secretRef.name} is in namespace ${credential.secretRef.namespace}, expected default lane namespace ${spec.runtime.namespace}`);
}
return {
profile: credential.profile,
secretRef: {
name: credential.secretRef.name,
keys: credential.secretRef.keys,
},
};
});
const basePolicy = cloneJsonRecord(sessionPolicy.executionPolicy);
const secretScope = record(basePolicy.secretScope);
return {
...basePolicy,
secretScope: {
...secretScope,
providerCredentials,
},
};
}
function renderAgentRunSessionPolicyExplanation(): string {
const config = readAgentRunClientConfig();
const { spec } = resolveAgentRunLaneTarget({});
const sessionPolicy = config.client.sessionPolicy;
const credentials = agentRunProviderCredentialRefs(spec, sessionPolicy.backendProfile);
const execution = defaultAgentRunExecutionPolicy(sessionPolicy.backendProfile, sessionPolicy);
const credentialSources = credentials.map((credential) => ({
profile: credential.profile,
secretRef: credential.secretRef,
valuesPrinted: false,
}));
return [
"KIND: session-policy",
`CONFIG: ${config.sourcePath}`,
`DEFAULT LANE: ${spec.nodeId}/${spec.lane}`,
`DEFAULTS: tenantId=${sessionPolicy.tenantId} projectId=${sessionPolicy.projectId} providerId=${sessionPolicy.providerId} backendProfile=${sessionPolicy.backendProfile}`,
`WORKSPACE: ${JSON.stringify(sessionPolicy.workspaceRef)}`,
`EXECUTION: ${JSON.stringify(execution)}`,
`PROVIDER CREDENTIAL SOURCES: ${JSON.stringify(credentialSources)}`,
"VALUES: secret payloads are not printed",
].join("\n");
}
function safeAgentRunEnvelope(envelope: Record<string, unknown>): Record<string, unknown> {
return pickCompact(envelope, ["ok", "failureKind", "message", "code", "details", "valuesPrinted"]);
}
function normalizeAgentRunFailureKind(raw: string | null, httpStatus: number): AgentRunFailureKind {
if (raw === "auth-missing" || raw === "auth-failed" || raw === "agentrun-unreachable" || raw === "schema-mismatch" || raw === "unsupported-version" || raw === "validation-failed" || raw === "not-found") return raw;
if (httpStatus === 401 || httpStatus === 403) return "auth-failed";
if (httpStatus === 404) return "not-found";
return raw === "schema-invalid" ? "validation-failed" : "validation-failed";
}
function stringFieldFromRecord(obj: Record<string, unknown>, key: string, pathValue: string): string {
const value = obj[key];
if (typeof value !== "string" || value.trim().length === 0) throw new Error(`${pathValue}.${key} must be a non-empty string`);
return value.trim();
}
function numberFieldFromRecord(obj: Record<string, unknown>, key: string, pathValue: string, bounds: { min: number; max: number }): number {
const value = obj[key];
if (typeof value !== "number" || !Number.isFinite(value) || value < bounds.min || value > bounds.max) throw new Error(`${pathValue}.${key} must be a number between ${bounds.min} and ${bounds.max}`);
return value;
}
function positiveIntegerFieldFromRecord(obj: Record<string, unknown>, key: string, pathValue: string): number {
const value = obj[key];
if (!Number.isInteger(value) || Number(value) <= 0) throw new Error(`${pathValue}.${key} must be a positive integer`);
return Number(value);
}
function booleanFieldFromRecord(obj: Record<string, unknown>, key: string, pathValue: string): boolean {
const value = obj[key];
if (typeof value !== "boolean") throw new Error(`${pathValue}.${key} must be a boolean`);
return value;
}
function cloneJsonRecord(value: Record<string, unknown>): Record<string, unknown> {
return JSON.parse(JSON.stringify(value)) as Record<string, unknown>;
}
interface AgentRunClientConfig {
sourcePath: string;
manager: {
baseUrl: string;
timeoutMs: number;
};
auth: {
env: string;
file: string;
header: string;
scheme: string;
};
client: {
role: string;
transport: string;
sessionPolicy: AgentRunSessionPolicyConfig;
};
publicExposure: AgentRunPublicExposure | null;
}
interface AgentRunSessionPolicyConfig {
sourcePath: string;
tenantId: string;
projectId: string;
providerId: string;
backendProfile: string;
workspaceRef: Record<string, unknown>;
executionPolicy: Record<string, unknown>;
}
interface AgentRunPublicExposure {
enabled: true;
proxyName: string;
remotePort: number;
publicBaseUrl: string;
masterBaseUrl: string;
masterFrps: {
configPath: string;
containerName: string;
};
masterCaddy: {
enabled: boolean;
domain: string;
configPath: string;
serviceName: string;
upstreamBaseUrl: string;
responseHeaderTimeoutSeconds: number;
};
}
interface AgentRunResolvedAuth {
source: "env" | "file";
value: string;
}
type AgentRunHttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
type AgentRunFailureKind = "auth-missing" | "auth-failed" | "agentrun-unreachable" | "schema-mismatch" | "unsupported-version" | "validation-failed" | "not-found";
type AgentRunRestCompatGroup = "queue" | "sessions" | "aipod-specs" | "aipods" | "runs" | "commands" | "runner";
type AgentRunResourceVerb = "get" | "describe" | "events" | "logs" | "result" | "ack" | "cancel" | "dispatch" | "create" | "apply" | "send" | "explain";
type AgentRunResourceKind = "task" | "run" | "command" | "runnerjob" | "session" | "aipodspec";
type AgentRunOutputMode = "human" | "wide" | "name" | "json" | "yaml";
interface AgentRunResourceRef {
kind: AgentRunResourceKind;
name: string;
}
interface AgentRunResourceOptions {
output: AgentRunOutputMode;
full: boolean;
raw: boolean;
debug: boolean;
limit: number;
queue: string | null;
state: string | null;
unread: boolean;
readerId: string;
taskId: string | null;
runId: string | null;
commandId: string | null;
sessionId: string | null;
afterSeq: number | null;
tail: number | null;
fullText: boolean;
reason: string | null;
dryRun: boolean;
file: string | null;
aipod: string | null;
idempotencyKey: string | null;
promptStdin: boolean;
node: string | null;
lane: string | null;
passthroughArgs: string[];
}
interface AgentRunRestTargetOptions {
node: string | null;
lane: string | null;
}
interface AgentRunRestTarget {
config: UniDeskConfig;
configPath: string;
spec: AgentRunLaneSpec;
}
let activeAgentRunRestTarget: AgentRunRestTarget | null = null;
type AgentRunBridgeCaptureBackend = "local-backend-core-broker" | "remote-frontend-websocket";
interface LocalBackendCoreStatus {
dockerExecutable: boolean;
backendCoreContainer: boolean;
error: string | null;
}
interface AgentRunBridgeCapturePlan {
backend: AgentRunBridgeCaptureBackend;
route: string;
reason: string;
remoteHost: string | null;
localBackendCore: LocalBackendCoreStatus;
}
type AgentRunBridgeCaptureResult = SshCaptureResult & { bridgeExecution?: AgentRunBridgeCapturePlan };
let localBackendCoreStatusCache: LocalBackendCoreStatus | null = null;
async function capture(config: UniDeskConfig, target: string, args: string[]): Promise<AgentRunBridgeCaptureResult> {
const plan = agentRunBridgeCapturePlan(config, target);
if (plan.backend === "remote-frontend-websocket" && plan.remoteHost !== null) {
return await captureRemote(config, plan, target, args);
}
const local = attachBridgeExecution(await runSshCommandCapture(config, target, args), plan);
const fallbackHost = agentRunBridgeRemoteHost(config);
if (local.exitCode !== 0 && fallbackHost !== null && bridgeExecutionFailureKind(local)?.degradedReason === "capture-backend-unavailable") {
const fallbackPlan: AgentRunBridgeCapturePlan = {
...plan,
backend: "remote-frontend-websocket",
remoteHost: fallbackHost,
reason: "local-capture-backend-unavailable",
};
return await captureRemote(config, fallbackPlan, target, args);
}
return local;
}
async function captureRemote(config: UniDeskConfig, plan: AgentRunBridgeCapturePlan, target: string, args: string[]): Promise<AgentRunBridgeCaptureResult> {
if (plan.remoteHost === null) return attachBridgeExecution(remoteBridgeCaptureFailure(new Error("remote host is not configured")), plan);
try {
return attachBridgeExecution(await runRemoteSshCommandCapture(config, plan.remoteHost, target, args), plan);
} catch (error) {
return attachBridgeExecution(remoteBridgeCaptureFailure(error), plan);
}
}
function remoteBridgeCaptureFailure(error: unknown): SshCaptureResult {
const message = error instanceof Error ? `${error.name}: ${error.message}` : String(error);
return {
exitCode: 255,
stdout: "",
stderr: `unidesk remote frontend ssh bridge failed: ${message}\n`,
};
}
function attachBridgeExecution(result: SshCaptureResult, plan: AgentRunBridgeCapturePlan): AgentRunBridgeCaptureResult {
return { ...result, bridgeExecution: plan };
}
function agentRunBridgeCapturePlan(config: UniDeskConfig, target: string, env: NodeJS.ProcessEnv = process.env): AgentRunBridgeCapturePlan {
const localBackendCore = detectLocalBackendCoreStatus();
const remoteHost = agentRunBridgeRemoteHost(config, env);
const runnerEnv = isAgentRunRunnerEnvironment(env);
if (runnerEnv && remoteHost !== null) {
return { backend: "remote-frontend-websocket", route: target, reason: "runner-environment", remoteHost, localBackendCore };
}
if (!localBackendCore.backendCoreContainer && remoteHost !== null) {
return { backend: "remote-frontend-websocket", route: target, reason: "local-backend-core-unavailable", remoteHost, localBackendCore };
}
return { backend: "local-backend-core-broker", route: target, reason: "main-server-local-backend-core", remoteHost: null, localBackendCore };
}
function detectLocalBackendCoreStatus(): LocalBackendCoreStatus {
if (localBackendCoreStatusCache !== null) return localBackendCoreStatusCache;
const result = spawnSync("docker", ["ps", "--format", "{{.Names}}"], { encoding: "utf8", timeout: 2000 });
if (result.error !== undefined) {
localBackendCoreStatusCache = {
dockerExecutable: false,
backendCoreContainer: false,
error: result.error.message,
};
return localBackendCoreStatusCache;
}
const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`.trim();
localBackendCoreStatusCache = {
dockerExecutable: result.status === 0,
backendCoreContainer: result.status === 0 && String(result.stdout ?? "").split(/\r?\n/u).includes("unidesk-backend-core"),
error: result.status === 0 ? null : output || `docker ps exited ${result.status ?? "unknown"}`,
};
return localBackendCoreStatusCache;
}
function isAgentRunRunnerEnvironment(env: NodeJS.ProcessEnv): boolean {
return Boolean(
env.AGENTRUN_BOOT_MODE
|| env.AGENTRUN_RUN_ID
|| env.AGENTRUN_K8S_JOB_NAME
|| env.CODE_QUEUE_SERVICE_ROLE
|| env.CODE_QUEUE_INSTANCE_ID
|| env.KUBERNETES_SERVICE_HOST,
);
}
function agentRunBridgeRemoteHost(config: UniDeskConfig, env: NodeJS.ProcessEnv = process.env): string | null {
return normalizeRemoteHostHint(env.UNIDESK_MAIN_SERVER_IP)
?? normalizeRemoteHostHint(env.UNIDESK_MAIN_SERVER_HOST)
?? normalizeRemoteHostHint(env.CODE_QUEUE_DEV_CONTAINER_MASTER_HOST)
?? normalizeRemoteHostHint(config.network.publicHost);
}
function normalizeRemoteHostHint(raw: string | undefined): string | null {
const value = raw?.trim() ?? "";
if (value.length === 0 || value === "localhost" || value === "127.0.0.1" || value === "::1") return null;
return value.replace(/\/+$/u, "");
}
function captureBridgeMetadata(result: SshCaptureResult): Record<string, unknown> | null {
const bridgeExecution = (result as AgentRunBridgeCaptureResult).bridgeExecution;
if (bridgeExecution === undefined) return null;
return {
backend: bridgeExecution.backend,
route: bridgeExecution.route,
reason: bridgeExecution.reason,
remoteHost: bridgeExecution.remoteHost,
localBackendCore: bridgeExecution.localBackendCore,
};
}
function bridgeExecutionFailureKind(result: SshCaptureResult): { degradedReason: string; failureKind: string; recoveryActions: string[] } | null {
if (result.exitCode === 0) return null;
const stderr = result.stderr;
if (/No such container: unidesk-backend-core|failed to start broker|Executable not found.*"docker"|docker: not found|Cannot connect to the Docker daemon/iu.test(stderr)) {
return {
degradedReason: "capture-backend-unavailable",
failureKind: "bridge-execution-environment",
recoveryActions: [
"Run the same command from a healthy UniDesk main-server CLI, or provide UNIDESK_MAIN_SERVER_IP/UNIDESK_MAIN_SERVER_HOST so the bridge can use the frontend WebSocket SSH backend.",
"For AgentRun runner jobs, request the unidesk-ssh tool credential SecretRef instead of embedding secret values in prompts or payloads.",
"After restoring the bridge backend, retry the same bun scripts/cli.ts agentrun ... command; do not use direct kubectl or GitHub API bypasses.",
],
};
}
if (/frontend login failed|remote frontend ssh bridge|websocket error|timed out waiting for provider session|route-not-allowed/iu.test(stderr)) {
return {
degradedReason: "bridge-execution-environment-unavailable",
failureKind: "bridge-execution-environment",
recoveryActions: [
"Verify the UniDesk frontend/backend-core SSH bridge is reachable from this runner and that UNIDESK_MAIN_SERVER_IP or UNIDESK_MAIN_SERVER_HOST points at the main server.",
"Verify scoped UNIDESK_SSH_CLIENT_TOKEN route allowlist includes the requested AgentRun route, without printing token values.",
"Retry with the same bun scripts/cli.ts agentrun ... command after the controlled bridge path is restored.",
],
};
}
return null;
}
function compactCapture(result: SshCaptureResult, options: { full?: boolean; stdoutTailChars?: number; stderrTailChars?: number } = {}): Record<string, unknown> {
const stdoutTailChars = options.stdoutTailChars ?? 8000;
const stderrTailChars = options.stderrTailChars ?? 4000;
const full = options.full ?? true;
const payload: Record<string, unknown> = {
exitCode: result.exitCode,
stdoutBytes: Buffer.byteLength(result.stdout, "utf8"),
stderrBytes: Buffer.byteLength(result.stderr, "utf8"),
stdoutTailOmitted: !full && result.exitCode === 0,
stderrTailOmitted: !full && result.exitCode === 0,
};
const bridgeExecution = captureBridgeMetadata(result);
if (bridgeExecution !== null) payload.bridgeExecution = bridgeExecution;
const failureKind = bridgeExecutionFailureKind(result);
if (failureKind !== null) {
payload.degradedReason = failureKind.degradedReason;
payload.failureKind = failureKind.failureKind;
payload.recoveryActions = failureKind.recoveryActions;
}
if (full || result.exitCode !== 0) {
payload.stdoutTail = tail(result.stdout, stdoutTailChars);
payload.stderrTail = tail(result.stderr, stderrTailChars);
payload.stdoutTruncated = result.stdout.length > stdoutTailChars;
payload.stderrTruncated = result.stderr.length > stderrTailChars;
}
return payload;
}
async function timedStatusStage<T>(stage: string, action: () => Promise<T>): Promise<TimedValue<T>> {
const startedAtMs = Date.now();
progressEvent("agentrun.control-plane.status.progress", { stage, status: "started" });
try {
const value = await action();
const exitCode = isCaptureResult(value) ? value.exitCode : typeof record(value).ok === "boolean" && record(value).ok !== true ? 1 : 0;
progressEvent("agentrun.control-plane.status.progress", {
stage,
status: exitCode === 0 ? "succeeded" : "failed",
exitCode,
elapsedMs: Date.now() - startedAtMs,
});
return { value, elapsedMs: Date.now() - startedAtMs };
} catch (error) {
progressEvent("agentrun.control-plane.status.progress", {
stage,
status: "failed",
elapsedMs: Date.now() - startedAtMs,
error: error instanceof Error ? error.message : String(error),
});
throw error;
}
}
function progressEvent(event: string, payload: Record<string, unknown>): void {
const stage = displayValue(payload.stage ?? event);
const status = displayValue(payload.status ?? "-");
const details = [
payload.exitCode === undefined ? null : `exit=${displayValue(payload.exitCode)}`,
payload.elapsedMs === undefined ? null : `elapsed=${displayValue(payload.elapsedMs)}ms`,
payload.error === undefined ? null : `error=${truncateOneLine(displayValue(payload.error), 160)}`,
].filter((item): item is string => item !== null);
process.stderr.write(`status ${stage} ${status}${details.length === 0 ? "" : ` ${details.join(" ")}`}\n`);
}
function isCaptureResult(value: unknown): value is SshCaptureResult {
return typeof value === "object" && value !== null && "exitCode" in value && typeof (value as { exitCode?: unknown }).exitCode === "number";
}
function isAgentRunPipelineRunName(value: string): boolean {
return /^agentrun-v[0-9]+-ci-[0-9a-f]{12,40}(?:-[a-z0-9-]+)?$/u.test(value);
}
function captureJsonPayload(result: SshCaptureResult): Record<string, unknown> {
const trimmed = result.stdout.trim();
if (trimmed.length === 0) return {};
try {
return record(JSON.parse(trimmed) as unknown);
} catch {
const lastJsonLine = trimmed.split(/\r?\n/u).reverse().find((line) => line.trim().startsWith("{") && line.trim().endsWith("}"));
if (lastJsonLine === undefined) return {};
try {
return record(JSON.parse(lastJsonLine) as unknown);
} catch {
return {};
}
}
}
function record(value: unknown): Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : {};
}
function stringOrNull(value: unknown): string | null {
return typeof value === "string" && value.length > 0 ? value : null;
}
function nonNegativeIntegerOrNull(value: unknown): number | null {
if (typeof value === "number" && Number.isInteger(value) && value >= 0) return value;
if (typeof value === "string" && /^\d+$/u.test(value)) return Number(value);
return null;
}
function isGitSha(value: string): boolean {
return /^[0-9a-f]{40}$/u.test(value);
}
function tail(text: string, maxChars: number): string {
return text.length > maxChars ? text.slice(-maxChars) : text;
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function shQuote(value: string): string {
return `'${value.replace(/'/gu, "'\\''")}'`;
}
function unsupported(args: string[]): RenderedCliResult {
const command = `agentrun ${args.join(" ")}`.trim();
return renderedCliResult(false, command, [
`Error: unsupported AgentRun command: ${command}`,
"",
"Supported resource commands:",
" agentrun get|describe|events|logs|result|ack|cancel|dispatch|create|apply|send",
"",
"Compatibility bridge groups:",
" agentrun aipod-specs|queue|runs|commands|runner|sessions",
"",
"Operations:",
" agentrun control-plane status|trigger-current|refresh",
" agentrun git-mirror status|sync|flush",
].join("\n"));
}