// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. rest-bridge module for scripts/src/agentrun.ts. // Moved mechanically from scripts/src/agentrun.ts:5847-6915 for #903. // SPEC: PJ2026-01020108 cancel lifecycle + PJ2026-01020205 AipodSpec binding + PJ2026-01020302 session policy + PJ2026-01020305 cancel control + PJ2026-01060305/06 YAML execution policy and bounded output draft-2026-06-25-p0. // Exposes AgentRun lane-scoped policy, AipodSpec SecretRef binding, cancel lifecycle, and bounded default output in the UniDesk CLI. 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 AgentRunCancelLifecycleSpec, type AgentRunLaneSpec, } from "../agentrun-lanes"; import { agentRunImageArtifact, placeholderAgentRunImage, renderedFilesDigest, renderedObjectsDigest, renderAgentRunControlPlaneManifests, renderAgentRunGitopsFiles, type AgentRunArtifactService, } from "../agentrun-manifests"; import { sha256Fingerprint } from "../platform-infra-ops-library"; import type { AgentRunClientConfig } from "./config"; import type { GitMirrorOptions } from "./options"; import type { AgentRunFailureKind, AgentRunHttpMethod, AgentRunResourceOptions, AgentRunRestCompatGroup, AgentRunRestCompatOptions, AgentRunRestTarget, AgentRunRestTargetOptions } from "./utils"; import { addAgentRunNotFoundLookupHint, agentRunDryRunPlan, agentRunHasFlag, agentRunLaneRestBridgeMetadata, agentRunOption, agentRunQuery, agentRunRestBridgeMetadata, aipodRenderInputFromArgs, cancelBodyFromArgs, classifyAgentRunProxyFailureKind, cloneJsonRecord, copyAgentRunOptions, defaultAgentRunExecutionPolicy, defaultAgentRunProviderId, jsonInputDisclosureFromArgs, jsonObjectOption, normalizeAgentRunFailureKind, optionalJsonBody, optionalPromptFromArgs, optionalRunnerJsonBody, queueDispatchBodyFromArgs, queueSubmitConfirmCommand, readAgentRunClientConfig, readYamlInputFromArgs, requiredAgentRunOption, requiredJsonBody, resolveAgentRunAuth, resolveAgentRunSessionPolicyTarget, safeAgentRunEnvelope } from "./config"; import { status } from "./control-plane"; import { gitMirrorStatus } from "./git-mirror"; import { arrayRecords, displayValue, isRecord, pickCompact, renderTable, truncateOneLine } from "./options"; import { innerData, pathValue, renderMachine, renderedCliResult } from "./render"; import { parseResourceOptions, stripAgentRunResourceWrapperArgs } from "./resource-actions"; import { AGENTRUN_GIT_MIRROR_RETRY_MAX_ATTEMPTS, agentRunGitMirrorRetryDelayMs, agentRunGitMirrorRetrySummary, agentRunGitMirrorRetryableFailure, createYamlLaneJobScript, yamlLaneGitMirrorJobManifest, yamlLaneGitMirrorStatusScript, yamlLaneJobProbeScript } from "./secrets"; import { capture, captureJsonPayload, compactCapture, nonNegativeIntegerOrNull, progressEvent, record, shQuote, sleep, stringOrNull } from "./utils"; export async function readGitMirrorStatus(config: UniDeskConfig, target: { configPath: string; spec: AgentRunLaneSpec }): Promise & { result: SshCaptureResult; raw: string; summary: Record }> { 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, }; } export async function runGitMirrorJob(config: UniDeskConfig, action: "sync" | "flush", options: GitMirrorOptions): Promise> { 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[] = []; let lastCreated: SshCaptureResult | null = null; let lastWait: Record | 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}` }, }; } export async function waitForGitMirrorJob(config: UniDeskConfig, spec: AgentRunLaneSpec, action: "sync" | "flush", jobName: string, timeoutSeconds: number): Promise> { 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, }; } export function startAsyncAgentRunJob(name: string, command: string[], note: string): Record { 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}`, }, }; } export async function runAgentRunRestCompatCommand(config: UniDeskConfig | null, group: AgentRunRestCompatGroup, args: string[], canonicalArgs: string[]): Promise | RenderedCliResult> { const options = parseAgentRunRestCompatOptions(args); const command = `agentrun ${canonicalArgs.join(" ")}`.trim(); try { const raw = await withAgentRunRestTarget(resolveAgentRunRestTarget(config, options), async () => { return await runAgentRunRestCommand(config, group, stripAgentRunResourceWrapperArgs(args)); }); return renderAgentRunRestCompatResult(command, group, stripAgentRunResourceWrapperArgs(args), raw, options); } catch (error) { if (error instanceof AgentRunRestError) { if (options.raw || options.full || options.output === "json" || options.output === "yaml") { return renderMachine(command, error.toPayload(command), options.output === "yaml" ? "yaml" : "json", false); } return error.toPayload(command); } throw error; } } export function parseAgentRunRestCompatOptions(args: string[]): AgentRunRestCompatOptions { const resourceOptions = parseResourceOptions(args); return { node: resourceOptions.node, lane: resourceOptions.lane, output: resourceOptions.output, full: resourceOptions.full, raw: resourceOptions.raw, }; } export function renderAgentRunRestCompatResult(command: string, group: AgentRunRestCompatGroup, args: string[], raw: Record, options: AgentRunRestCompatOptions): Record | RenderedCliResult { if (options.raw || options.full) 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 compatGroup = group === "aipods" ? "aipod-specs" : group; const [action, id] = args; if (compatGroup === "aipod-specs" && action === "render" && id !== undefined) return renderAgentRunAipodSpecRenderSummary(command, raw, id); return raw; } export function renderAgentRunAipodSpecRenderSummary(command: string, raw: Record, requestedAipod: string): RenderedCliResult { const data = record(innerData(raw)); const aipod = record(data.aipod); const task = record(data.queueTask); const bridge = record(raw.bridge); const executionPolicy = record(task.executionPolicy); const secretScope = record(executionPolicy.secretScope); const resourceBundle = record(task.resourceBundleRef ?? aipod.resourceBundleRef); const dispatchDefaults = record(data.dispatchDefaults); const runnerJobDefaults = record(dispatchDefaults.runnerJob); const node = stringOrNull(bridge.node) ?? stringOrNull(task.providerId) ?? "-"; const lane = stringOrNull(bridge.lane) ?? stringOrNull(task.lane) ?? "-"; const namespace = stringOrNull(bridge.namespace) ?? stringOrNull(runnerJobDefaults.namespace) ?? "-"; const providerCredentials = agentRunRenderedProviderCredentials(secretScope, aipod); const toolCredentials = agentRunRenderedToolCredentials(secretScope, aipod); const bundleNames = agentRunResourceBundleNames(resourceBundle.bundles, "name"); const skillNames = agentRunResourceBundleNames(resourceBundle.requiredSkills, "name"); const targetArgs = agentRunCompatTargetCliArgs(node, lane); const lines = [ "AIPODSPEC RENDER", ` AipodSpec: ${displayValue(aipod.name ?? requestedAipod)} hash=${displayValue(aipod.specHash)}`, ` Target: node=${displayValue(node)} lane=${displayValue(lane)} namespace=${displayValue(namespace)} bridge=${displayValue(bridge.mode)}`, ` TaskPolicy: backendProfile=${displayValue(task.backendProfile)} providerId=${displayValue(task.providerId)} queue=${displayValue(task.queue)} lane=${displayValue(task.lane)}`, ` WorkspaceRef: ${agentRunCompactJson(task.workspaceRef, 140)}`, ` Execution: ${agentRunCompactJson(pickCompact(executionPolicy, ["sandbox", "approval", "timeoutMs", "network"]), 160)}`, " Values: secret payloads are not printed; valuesPrinted=false", "", "PROVIDER CREDENTIAL SECRETREFS", providerCredentials.length > 0 ? renderTable(["PROFILE", "NAMESPACE", "SECRET", "KEYS", "VALUES"], providerCredentials.map((credential) => [ displayValue(credential.profile), displayValue(credential.namespace ?? namespace), displayValue(credential.name), agentRunJoinNames(credential.keys, 5), "false", ])) : " (none)", "", "TOOL CREDENTIAL SECRETREFS", toolCredentials.length > 0 ? renderTable(["TOOL", "PURPOSE", "NAMESPACE", "SECRET", "KEYS", "PROJECTION", "VALUES"], toolCredentials.map((credential) => [ displayValue(credential.tool), displayValue(credential.purpose), displayValue(credential.namespace ?? namespace), displayValue(credential.name), agentRunJoinNames(credential.keys, 5), agentRunProjectionSummary(credential.projection), "false", ])) : " (none)", "", "RESOURCE BUNDLE", ` Kind: ${displayValue(resourceBundle.kind)} repo=${displayValue(resourceBundle.repoUrl)} ref=${displayValue(resourceBundle.ref)} commit=${displayValue(resourceBundle.commitId)}`, ` Bundles: ${agentRunJoinNames(bundleNames, 6)}`, ` RequiredSkills: ${agentRunJoinNames(skillNames, 8)}`, ` RunnerJob: namespace=${displayValue(runnerJobDefaults.namespace)} image=${agentRunCompactJson(runnerJobDefaults.imageRef, 120)}`, "", "DRILL-DOWN", ` bun scripts/cli.ts ${command} --full`, ` bun scripts/cli.ts ${command} -o json`, ` bun scripts/cli.ts agentrun create task --aipod ${requestedAipod}${targetArgs} --prompt-stdin`, ]; return renderedCliResult(raw.ok !== false, command, `${lines.join("\n")}\n`); } export function agentRunRenderedProviderCredentials(secretScope: Record, aipod: Record): Array<{ profile: unknown; namespace: unknown; name: unknown; keys: string[] }> { const scoped = arrayRecords(secretScope.providerCredentials).map((credential) => { const secretRef = record(credential.secretRef); return { profile: credential.profile, namespace: secretRef.namespace, name: secretRef.name, keys: agentRunStringList(secretRef.keys), }; }); if (scoped.length > 0) return scoped; return arrayRecords(record(aipod.providerCredentials).items).map((credential) => ({ profile: credential.profile, namespace: credential.namespace, name: credential.name, keys: agentRunStringList(credential.keys), })); } export function agentRunRenderedToolCredentials(secretScope: Record, aipod: Record): Array<{ tool: unknown; purpose: unknown; namespace: unknown; name: unknown; keys: string[]; projection: Record }> { const scoped = arrayRecords(secretScope.toolCredentials).map((credential) => { const secretRef = record(credential.secretRef); return { tool: credential.tool, purpose: credential.purpose, namespace: secretRef.namespace, name: secretRef.name, keys: agentRunStringList(secretRef.keys), projection: record(credential.projection), }; }); if (scoped.length > 0) return scoped; return arrayRecords(record(aipod.toolCredentials).items).map((credential) => ({ tool: credential.tool, purpose: credential.purpose, namespace: credential.namespace, name: credential.name, keys: agentRunStringList(credential.keys), projection: record(credential.projection), })); } export function agentRunResourceBundleNames(value: unknown, key: string): string[] { const direct = agentRunStringList(record(value).names); if (direct.length > 0) return direct; return arrayRecords(value).map((item) => stringOrNull(item[key]) ?? "").filter((item) => item.length > 0); } export function agentRunStringList(value: unknown): string[] { if (!Array.isArray(value)) return []; return value.map((item) => String(item)).filter((item) => item.length > 0); } export function agentRunJoinNames(values: string[], maxItems: number): string { if (values.length === 0) return "-"; const visible = values.slice(0, maxItems); const suffix = values.length > visible.length ? ` +${values.length - visible.length}` : ""; return `${visible.join(", ")}${suffix}`; } export function agentRunProjectionSummary(value: Record): string { const kind = stringOrNull(value.kind); if (kind === "env") return `env:${displayValue(value.envName)}<-${displayValue(value.secretKey)}`; if (kind === "volume") return `volume:${displayValue(value.mountPath)}`; if (Object.keys(value).length === 0) return "-"; return agentRunCompactJson(value, 80); } export function agentRunCompactJson(value: unknown, maxChars: number): string { if (value === undefined || value === null) return "-"; return truncateOneLine(JSON.stringify(value), maxChars); } export function agentRunCompatTargetCliArgs(node: string, lane: string): string { const parts: string[] = []; if (node !== "-") parts.push("--node", node); if (lane !== "-") parts.push("--lane", lane); return parts.length === 0 ? "" : ` ${parts.join(" ")}`; } export async function runAgentRunRestCommand(config: UniDeskConfig | null, group: AgentRunRestCompatGroup, args: string[]): Promise> { 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}`); } export async function runAgentRunQueueRest(action: string | undefined, id: string | undefined, args: string[]): Promise> { 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 === "attempts" && id) return await agentRunRestRequest("agentrun queue attempts", "GET", `/api/v1/queue/tasks/${encodeURIComponent(id)}/attempts${agentRunQuery(args, ["cursor", "limit"])}`); 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 === "retry" && id) return await retryQueueTaskRest(id, 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)"}`); } export async function runAgentRunSessionsRest(action: string | undefined, id: string | undefined, args: string[]): Promise> { 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)"}`); } export async function runAgentRunRunsRest(action: string | undefined, id: string | undefined, args: string[]): Promise> { 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)"}`); } export async function runAgentRunCommandsRest(action: string | undefined, id: string | undefined, args: string[]): Promise> { 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)"}`); } export async function runAgentRunRunnerRest(action: string | undefined, id: string | undefined, args: string[]): Promise> { 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)"}`); } export async function runAgentRunAipodSpecsRest(action: string | undefined, id: string | undefined, args: string[]): Promise> { 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)"}`); } export async function submitQueueTaskRest(args: string[]): Promise> { 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 = normalizeAipodRenderedQueueTask(record(record(innerData(rendered)).queueTask), args, aipod); 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 }, aipodBinding: agentRunAipodBindingDisclosure(body, aipod), }); } 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); } export async function mutateQueueTaskRest(action: string, taskId: string, suffix: string, body: Record, args: string[]): Promise> { 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); } export async function retryQueueTaskRest(taskId: string, args: string[]): Promise> { const idempotencyKey = requiredAgentRunOption(args, ["idempotency-key"], "queue retry requires --idempotency-key"); const reason = requiredAgentRunOption(args, ["reason"], "queue retry requires --reason"); return await agentRunRestRequest("agentrun queue retry", "POST", `/api/v1/queue/tasks/${encodeURIComponent(taskId)}/retry`, { idempotencyKey, reason, dryRun: agentRunHasFlag(args, "dry-run"), }); } export async function sessionSendRest(sessionId: string, args: string[]): Promise> { 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: { ...record(innerData(response)), sessionPolicy: agentRunSessionRunPolicyDisclosure(record(sendBody.run)), }, bridge: response.bridge, }; } export function sessionSendPayloadFromInput(input: Record, prompt: string | null): Record { 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 }; } export async function sessionSendBodyFromRunInput(sessionId: string, args: string[], input: Record, payload: Record): Promise> { 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 = 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; } export function isExplicitSessionSendBody(input: Record): boolean { return isRecord(input.run) || isRecord(input.runBase) || isRecord(input.payload) || isRecord(input.runnerJob) || input.createRunnerJob !== undefined || input.commandIdempotencyKey !== undefined; } export async function sessionRunBodyFromArgs(sessionId: string, args: string[], input: Record): Promise> { const existing = await fetchAgentRunSessionOrNull(sessionId, args); const sessionPolicy = readAgentRunClientConfig().client.sessionPolicy; const policyTarget = resolveAgentRunSessionPolicyTarget(); const profile = agentRunOption(args, "profile") ?? agentRunOption(args, "backend-profile") ?? stringOrNull(input.backendProfile) ?? stringOrNull(existing?.backendProfile) ?? sessionPolicy.backendProfile; const body: Record = { ...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) ?? defaultAgentRunProviderId(sessionPolicy, policyTarget); 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); const executionPolicy = jsonObjectOption(args, "execution-policy-json") ?? record(body.executionPolicy); body.executionPolicy = defaultAgentRunExecutionPolicy(profile, sessionPolicy, policyTarget, { basePolicy: Object.keys(executionPolicy).length === 0 ? sessionPolicy.executionPolicy : executionPolicy, }); 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; } export function normalizeAipodRenderedQueueTask(task: Record, args: string[], aipod: string): Record { if (Object.keys(task).length === 0) return task; const sessionPolicy = readAgentRunClientConfig().client.sessionPolicy; const policyTarget = resolveAgentRunSessionPolicyTarget(); const explicitProfile = agentRunOption(args, "profile") ?? agentRunOption(args, "backend-profile"); const renderedProfile = stringOrNull(task.backendProfile); const fallbackProfile = sessionPolicy.backendProfile; let backendProfile = explicitProfile ?? renderedProfile ?? fallbackProfile; if (agentRunProviderCredentialRefs(policyTarget.spec, backendProfile).length === 0) { if (explicitProfile !== null) { throw new AgentRunRestError("validation-failed", `${policyTarget.configPath} has no providerCredential Secret binding for explicit --backend-profile ${explicitProfile} on ${policyTarget.spec.nodeId}/${policyTarget.spec.lane}`); } backendProfile = fallbackProfile; } if (agentRunProviderCredentialRefs(policyTarget.spec, backendProfile).length === 0) { throw new AgentRunRestError("validation-failed", `${policyTarget.configPath} has no providerCredential Secret binding for AipodSpec ${aipod} backendProfile=${backendProfile} on ${policyTarget.spec.nodeId}/${policyTarget.spec.lane}`); } const basePolicy = record(task.executionPolicy); const normalized: Record = { ...task }; const explicitProviderId = agentRunOption(args, "provider-id"); normalized.tenantId = stringOrNull(normalized.tenantId) ?? sessionPolicy.tenantId; normalized.projectId = stringOrNull(normalized.projectId) ?? sessionPolicy.projectId; normalized.providerId = explicitProviderId ?? defaultAgentRunProviderId(sessionPolicy, policyTarget); normalized.backendProfile = backendProfile; if (!isRecord(normalized.workspaceRef)) normalized.workspaceRef = cloneJsonRecord(sessionPolicy.workspaceRef); normalized.executionPolicy = defaultAgentRunExecutionPolicy(backendProfile, sessionPolicy, policyTarget, { includeToolCredentials: true, basePolicy: Object.keys(basePolicy).length === 0 ? sessionPolicy.executionPolicy : basePolicy, }); return normalized; } export function agentRunAipodBindingDisclosure(task: Record, aipod: string): Record { const policyTarget = resolveAgentRunSessionPolicyTarget(); const executionPolicy = record(task.executionPolicy); const secretScope = record(executionPolicy.secretScope); const providerCredentials = arrayRecords(secretScope.providerCredentials).map((credential) => ({ profile: credential.profile ?? null, secretRef: { name: record(credential.secretRef).name ?? null, keys: Array.isArray(record(credential.secretRef).keys) ? record(credential.secretRef).keys : [], }, valuesPrinted: false, })); const toolCredentials = arrayRecords(secretScope.toolCredentials).map((credential) => ({ tool: credential.tool ?? null, purpose: credential.purpose ?? null, secretRef: { name: record(credential.secretRef).name ?? null, keys: Array.isArray(record(credential.secretRef).keys) ? record(credential.secretRef).keys : [], }, projection: record(credential.projection), valuesPrinted: false, })); return { aipod, node: policyTarget.spec.nodeId, lane: policyTarget.spec.lane, namespace: policyTarget.spec.runtime.namespace, policySource: policyTarget.source, providerId: task.providerId ?? null, backendProfile: task.backendProfile ?? null, workspaceRef: task.workspaceRef ?? null, executionPolicy: pickCompact(executionPolicy, ["sandbox", "approval", "timeoutMs", "network"]), providerCredentials, toolCredentials, valuesPrinted: false, }; } export function agentRunSessionRunPolicyDisclosure(runBody: Record): Record { const policyTarget = resolveAgentRunSessionPolicyTarget(); const executionPolicy = record(runBody.executionPolicy); const secretScope = record(executionPolicy.secretScope); const providerCredentials = arrayRecords(secretScope.providerCredentials).map((credential) => ({ profile: credential.profile ?? null, secretRef: { name: record(credential.secretRef).name ?? null, keys: Array.isArray(record(credential.secretRef).keys) ? record(credential.secretRef).keys : [], }, valuesPrinted: false, })); return { node: policyTarget.spec.nodeId, lane: policyTarget.spec.lane, namespace: policyTarget.spec.runtime.namespace, policySource: policyTarget.source, providerId: runBody.providerId ?? null, backendProfile: runBody.backendProfile ?? null, workspaceRef: runBody.workspaceRef ?? null, executionPolicy: pickCompact(executionPolicy, ["sandbox", "approval", "timeoutMs", "network"]), providerCredentials, valuesPrinted: false, }; } export async function sessionSendWithAipodRest(sessionId: string, aipod: string, args: string[]): Promise> { 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 = normalizeAipodRenderedQueueTask(record(renderedData.queueTask), args, aipod); 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 policyTarget = resolveAgentRunSessionPolicyTarget(); const backendProfile = stringOrNull(task.backendProfile) ?? sessionPolicy.backendProfile; const executionPolicy = record(task.executionPolicy); const runBody: Record = { tenantId: stringOrNull(task.tenantId) ?? sessionPolicy.tenantId, projectId: stringOrNull(task.projectId) ?? sessionPolicy.projectId, providerId: stringOrNull(task.providerId) ?? defaultAgentRunProviderId(sessionPolicy, policyTarget), backendProfile, workspaceRef: task.workspaceRef ?? cloneJsonRecord(sessionPolicy.workspaceRef), sessionRef: { ...sessionRef, sessionId, metadata }, executionPolicy: Object.keys(executionPolicy).length === 0 ? defaultAgentRunExecutionPolicy(backendProfile, sessionPolicy, policyTarget, { includeToolCredentials: true }) : defaultAgentRunExecutionPolicy(backendProfile, sessionPolicy, policyTarget, { includeToolCredentials: true, basePolicy: executionPolicy }), resourceBundleRef: task.resourceBundleRef, traceSink: { kind: "aipod-session", aipod, sessionId, valuesPrinted: false }, }; const runnerDefaults = record(record(renderedData.dispatchDefaults).runnerJob); const sendBody: Record = { 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 ?? ""), sessionPolicy: agentRunSessionRunPolicyDisclosure(runBody), aipodBinding: agentRunAipodBindingDisclosure(task, aipod), valuesPrinted: false, }, bridge: response.bridge, }; } export async function fetchAgentRunSessionOrNull(sessionId: string, args: string[]): Promise | 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; } } export async function sessionRunnerJobBody(args: string[], defaults: Record = {}): Promise> { 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; } export async function agentRunRestRequest(command: string, method: AgentRunHttpMethod, pathValue: string, body?: unknown): Promise> { 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 = { [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) { const timedOut = isAbortLikeError(error); throw new AgentRunRestError(timedOut ? "agentrun-timeout" : "agentrun-connect-failed", timedOut ? `AgentRun server request timed out for ${method} ${pathValue} after ${clientConfig.manager.timeoutMs}ms` : `AgentRun server connection failed 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; 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, }; } export function isAbortLikeError(error: unknown): boolean { if (!(error instanceof Error)) return false; return error.name === "AbortError" || /abort|timed out|timeout/iu.test(error.message); } export async function agentRunLaneRestRequest(command: string, method: AgentRunHttpMethod, pathValue: string, body: unknown, clientConfig: AgentRunClientConfig, target: AgentRunRestTarget): Promise> { const bridgeBase = agentRunLaneRestBridgeMetadata(clientConfig, target, method, pathValue); const startedAt = Date.now(); const proxyBaseUrl = `http://127.0.0.1:${target.spec.runtime.managerPort}`; const request = { method, path: pathValue, body: body ?? null, baseUrl: proxyBaseUrl, serviceBaseUrl: 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-proxy-exec-failed", `AgentRun lane ${target.spec.nodeId}/${target.spec.lane} manager proxy execution failed for ${method} ${pathValue}`, { bridge }); } const proxy = captureJsonPayload(captureResult); if (proxy.ok !== true) { const proxyFailureKind = stringOrNull(proxy.failureKind); const failureKind = classifyAgentRunProxyFailureKind(proxyFailureKind); throw new AgentRunRestError(failureKind, stringOrNull(proxy.message) ?? `AgentRun lane ${target.spec.nodeId}/${target.spec.lane} manager proxy failed`, { bridge: { ...bridge, proxy }, details: pickCompact(proxy, ["ok", "failureKind", "message", "elapsedMs", "path", "baseUrl", "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; 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, }; } export function agentRunLaneRestProxyScript(spec: AgentRunLaneSpec, request: Record): string { const requestB64 = Buffer.from(JSON.stringify(request), "utf8").toString("base64"); const evalScript = String.raw` const startedAt = Date.now(); let input = {}; let method = "GET"; let path = "/"; let baseUrl = "http://127.0.0.1:8080"; let timeoutMs = 15000; try { input = JSON.parse(Buffer.from(process.env.AGENTRUN_LANE_REST_REQUEST_B64 || "", "base64").toString("utf8")); method = String(input.method || "GET"); path = String(input.path || "/"); baseUrl = String(input.baseUrl || "http://127.0.0.1:8080"); timeoutMs = Number(input.timeoutMs || 15000); 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 timeout = setTimeout(() => controller.abort(), timeoutMs); let response; try { response = await fetch(new URL(path, baseUrl).toString(), { method, 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) { const name = error instanceof Error ? error.name : ""; const message = error instanceof Error ? error.message : String(error); const timedOut = name === "AbortError" || /abort|timed out|timeout/iu.test(message); console.log(JSON.stringify({ ok: false, failureKind: timedOut ? "manager-pod-fetch-timeout" : "manager-pod-fetch-failed", message: timedOut ? "AgentRun manager request timed out for " + method + " " + path + " after " + timeoutMs + "ms" : message, elapsedMs: Date.now() - startedAt, path, baseUrl, 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"); } export function resolveAgentRunRestTarget(config: UniDeskConfig | null, options: AgentRunRestTargetOptions): AgentRunRestTarget | null { if (config === null) { if (options.node === null && options.lane === null) return null; throw new AgentRunRestError("validation-failed", "--node/--lane resource queries require UniDesk config"); } const { configPath, spec } = resolveAgentRunLaneTarget(options); return { config, configPath, spec }; } export async function withAgentRunRestTarget(target: AgentRunRestTarget | null, action: () => Promise): Promise { const previous = activeAgentRunRestTarget; activeAgentRunRestTarget = target; try { return await action(); } finally { activeAgentRunRestTarget = previous; } } export function renderAgentRunRestError(command: string, error: AgentRunRestError, options: AgentRunResourceOptions): RenderedCliResult { const rawPayload = error.toPayload(command); if (options.raw) return renderMachine(command, rawPayload, "json", false); const payload = boundedAgentRunRestErrorPayload(rawPayload); if (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 validationDetails = record(payload.agentrun); const recoveryActions = Array.isArray(validationDetails.recoveryActions) ? validationDetails.recoveryActions.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 validationHelp = `bun scripts/cli.ts agentrun ${command.split(/\s+/u)[1] ?? "--help"} --help`; const nextLines = nextCommands.length > 0 ? nextCommands : recoveryActions.length > 0 ? recoveryActions : error.failureKind === "validation-failed" ? [ "Review the AgentRun validation message and correct the request; do not bypass the formal resource API.", validationHelp, ] : [ "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")); } export function boundedAgentRunRestErrorPayload(payload: Record): Record { return boundedAgentRunRestErrorValue(payload, 0) as Record; } function boundedAgentRunRestErrorValue(value: unknown, depth: number): unknown { if (typeof value === "string") return truncateOneLine(value, 400); if (value === null || typeof value === "number" || typeof value === "boolean") return value; if (Array.isArray(value)) return value.slice(0, 12).map((item) => boundedAgentRunRestErrorValue(item, depth + 1)); if (typeof value !== "object") return String(value); if (depth >= 5) return "[nested details omitted]"; const entries = Object.entries(value as Record); const result: Record = {}; for (const [key, item] of entries.slice(0, 20)) result[key] = boundedAgentRunRestErrorValue(item, depth + 1); if (entries.length > 20) result.omittedFieldCount = entries.length - 20; return result; } export class AgentRunRestError extends Error { readonly failureKind: AgentRunFailureKind; readonly bridge: Record | null; readonly httpStatus: number | null; readonly details: Record | null; constructor(failureKind: AgentRunFailureKind, message: string, options: { bridge?: Record; httpStatus?: number; details?: Record } = {}) { 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 { 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, }; } } export let activeAgentRunRestTarget: AgentRunRestTarget | null = null;