// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. public-exposure module for scripts/src/agentrun.ts. // Moved mechanically from scripts/src/agentrun.ts:2593-2746 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 { StatusOptions } from "./options"; import { displayValue, renderTable } from "./options"; import { renderedCliResult } from "./render"; import { renderNextObjectLines, shortSha, yesNo } from "./trigger"; import { record } from "./utils"; export function agentRunControlPlaneStatusCommand(spec: AgentRunLaneSpec, options: Pick, 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 deploymentReplicaSummary(manager: Record): string { const replicas = record(manager.replicas); const desired = displayValue(replicas.desired); const ready = displayValue(replicas.ready); const available = displayValue(replicas.available); return `${ready}/${desired} available=${available}`; } export function renderAgentRunControlPlaneStatusSummary(result: Record): 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 migration = record(summary.migration); if (migration.migrated === true && migration.sourceAuthority === "gitea-pipelines-as-code") { const pac = record(summary.pac); const latestPipelineRun = record(pac.latestPipelineRun); const artifact = record(pac.artifact); const taskRuns = Array.isArray(pac.taskRuns) ? pac.taskRuns.map(record) : []; const taskRows = taskRuns.slice(0, 8).map((taskRun) => [ displayValue(taskRun.name), displayValue(taskRun.status), displayValue(taskRun.reason), displayValue(taskRun.durationSeconds), ]); const lines = [ "AGENTRUN CONTROL-PLANE STATUS", renderTable( ["NODE", "LANE", "MODE", "SOURCE", "PIPELINE", "ALIGNED", "RUNTIME", "BLOCKERS"], [[ displayValue(node.id ?? target.node ?? "-"), displayValue(target.lane ?? "-"), "PaC/Gitea", shortSha(summary.sourceCommit), displayValue(summary.expectedPipelineRun ?? "-"), yesNo(summary.aligned), yesNo(summary.runtimeAligned), blockers.length === 0 ? "-" : blockers.join(","), ]], ), "", renderTable( ["COMPONENT", "STATUS", "DETAIL"], [ ["trigger", yesNo(pac.ready), `webhooks=${displayValue(pac.webhookCount)} path=${displayValue(migration.triggerPath)}`], ["pipelinerun", yesNo(latestPipelineRun.status === "True"), `run=${displayValue(latestPipelineRun.name)} status=${displayValue(latestPipelineRun.status)} reason=${displayValue(latestPipelineRun.reason)} duration=${displayValue(latestPipelineRun.durationSeconds)}s`], ["image", yesNo(artifact.imageStatus === "reused" || artifact.imageStatus === "built"), `status=${displayValue(artifact.imageStatus)} env=${displayValue(artifact.envIdentity)} digest=${shortSha(artifact.digest)} gitops=${shortSha(artifact.gitopsCommit)}`], ["argo", yesNo(argo.syncedToGitops), `sync=${displayValue(argo.syncStatus)} health=${displayValue(argo.healthStatus)} revision=${shortSha(argo.revision)}`], ["runtime", yesNo(runtimeManager.ready === true && runtimeManager.sourceMatchesExpected === true), `manager=${shortSha(runtimeManager.sourceCommit)} ready=${yesNo(runtimeManager.ready)} replicas=${deploymentReplicaSummary(runtimeManager)} secrets=${yesNo(runtime.secretsReady)} dbSecret=${yesNo(runtime.databaseSecretPresent)} localPgAbsent=${yesNo(runtime.localPostgresAbsent)}`], ], ), "", "TASKRUN DURATIONS", taskRows.length === 0 ? "-" : renderTable(["TASKRUN", "STATUS", "REASON", "DURATION_S"], taskRows), "", warnings.length === 0 ? "WARNINGS\n-" : ["WARNINGS", ...warnings.map((warning) => `- ${warning}`)].join("\n"), "", "NEXT", ` action: ${displayValue(nextAction.summary ?? "-")}`, nextAction.command ? ` run: ${displayValue(nextAction.command)}` : null, ` pac: ${displayValue(next.pacStatus ?? disclosure.pacStatusCommand ?? "-")}`, ` full: ${displayValue(next.statusFull ?? disclosure.fullCommand ?? "bun scripts/cli.ts agentrun control-plane status --full")}`, "", `TIMINGS runtime=${displayValue(timings.runtimeMs)}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`); } const sourceSnapshotMode = source.sourceAuthority === "git-mirror-snapshot" || source.snapshotPresent !== null; const sourceStatus = sourceSnapshotMode ? yesNo(source.snapshotPresent) : yesNo(source.workspaceClean); const sourceDetail = sourceSnapshotMode ? `branch=${displayValue(source.branch)} commit=${shortSha(source.remoteBranchCommit)} snapshot=${yesNo(source.snapshotPresent)} stage=${displayValue(source.sourceStageRef ?? "-")}` : `branch=${displayValue(source.branch)} commit=${shortSha(source.remoteBranchCommit)} detached=${yesNo(source.workspaceDetached)}`; 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", sourceStatus, sourceDetail], ["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.ready === true && runtimeManager.sourceMatchesExpected === true), `manager=${shortSha(runtimeManager.sourceCommit)} ready=${yesNo(runtimeManager.ready)} replicas=${deploymentReplicaSummary(runtimeManager)} 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`); } export function renderAgentRunControlPlanePlanSummary(result: Record): RenderedCliResult { const target = record(result.target); const node = record(target.node); const runtime = record(target.runtime); const source = record(target.source); const checks = Array.isArray(result.plannedChecks) ? result.plannedChecks.map(String) : []; const lines = [ "AGENTRUN CONTROL-PLANE PLAN", renderTable( ["NODE", "LANE", "NAMESPACE", "SOURCE", "CHECKS", "MUTATION"], [[ displayValue(node.id ?? target.node ?? "-"), displayValue(target.lane ?? "-"), displayValue(runtime.namespace ?? "-"), displayValue(source.branch ?? "-"), String(checks.length), "false", ]], ), "", "CHECKS", ...(checks.length === 0 ? ["-"] : checks.slice(0, 12).map((check) => `- ${check}`)), "", "NEXT", ...renderNextObjectLines(record(result.next)), "", "DETAIL", " use --full for rendered target details; valuesPrinted=false", ]; return renderedCliResult(result.ok !== false, "agentrun control-plane plan", `${lines.join("\n")}\n`); } export function renderAgentRunControlPlaneActionSummary(result: Record, title: string): RenderedCliResult { const target = record(result.target); const node = record(target.node); const runtime = record(target.runtime); const summaryRows = [ ["ok", String(result.ok !== false)], ["mode", displayValue(result.mode ?? "-")], ["dryRun", displayValue(result.dryRun ?? "-")], ["mutation", displayValue(result.mutation ?? "-")], ["namespace", displayValue(result.namespace ?? runtime.namespace ?? "-")], ]; if (result.degradedReason !== undefined) summaryRows.push(["degradedReason", displayValue(result.degradedReason)]); const plan = record(result.plan); const secretRefs = record(plan.secretRefs); const providerCredentials = Array.isArray(plan.providerCredentials) ? plan.providerCredentials.map(record) : []; const planRows = [ ["deployment", displayValue(plan.deployment ?? "-")], ["annotation", displayValue(plan.annotation ?? "-")], ["patchType", displayValue(plan.patchType ?? "-")], ["reason", displayValue(plan.reason ?? "-")], ["secretRefCount", displayValue(secretRefs.count ?? "-")], ["providerCredentialCount", displayValue(providerCredentials.length === 0 ? "-" : providerCredentials.length)], ].filter((row) => row[1] !== "-"); const countKeys = [ "runnerJobCount", "overLimitCount", "inactiveCandidateCount", "managerRetentionCandidateCount", "stalePendingObservationCount", "selectedRunnerJobCount", "protectedActiveRunnerCount", "remainingAfterSelection", "deletedRunnerJobCount", "nonTerminalRunnerPodCount", "nonTerminalPodCount", "nextRunnerRequiresManagerRetention", ]; const countRows = countKeys .filter((key) => result[key] !== undefined) .map((key) => [key, displayValue(result[key])]); const capacity = record(result.capacity); const capacityBlocker = record(capacity.blocker); const capacityCurrent = capacity.runnerJobCount ?? result.runnerJobCount; const capacityLimit = capacity.maxRunners; const capacityRows = [ ["usage", capacityCurrent === undefined || capacityLimit === undefined ? "-" : `${String(capacityCurrent)}/${String(capacityLimit)}`], ["status", displayValue(capacity.status ?? "-")], ["availableSlots", displayValue(capacity.availableSlots ?? "-")], ["blockerType", displayValue(capacityBlocker.type ?? "-")], ["blockerCode", displayValue(capacityBlocker.code ?? "-")], ].filter((row) => row[1] !== "-"); const lines = [ title, renderTable( ["NODE", "LANE", "NAMESPACE", "MODE", "OK"], [[ displayValue(node.id ?? target.node ?? "-"), displayValue(target.lane ?? "-"), displayValue(result.namespace ?? runtime.namespace ?? "-"), displayValue(result.mode ?? "-"), String(result.ok !== false), ]], ), "", renderTable(["FIELD", "VALUE"], summaryRows), ]; if (planRows.length > 0) lines.push("", "PLAN", renderTable(["FIELD", "VALUE"], planRows)); if (providerCredentials.length > 0) { lines.push( "", "PROVIDER CREDENTIALS", renderTable(["PROFILE", "NAMESPACE", "SECRET", "KEYS", "VALUES"], providerCredentials.slice(0, 8).map((credential) => [ displayValue(credential.profile), displayValue(credential.namespace), displayValue(credential.name), Array.isArray(credential.keys) ? credential.keys.map(String).join(",") : "-", "false", ])), ); } if (capacityRows.length > 0) lines.push("", "CAPACITY", renderTable(["FIELD", "VALUE"], capacityRows)); if (countRows.length > 0) lines.push("", renderTable(["COUNT", "VALUE"], countRows)); const cleanupRows = (value: unknown): string[][] => (Array.isArray(value) ? value : []) .slice(0, 4) .map(record) .map((item) => { const drillDown = record(item.drillDown); return [ displayValue(item.runnerJobId ?? item.name ?? "-"), Array.isArray(item.podPhases) ? item.podPhases.map(String).join(",") || "-" : "-", Array.isArray(item.waitingReasons) ? item.waitingReasons.map(String).join(",") || "-" : "-", displayValue(item.lastActiveAgeMs ?? "-"), displayValue(item.classification ?? item.protectedReason ?? "-"), displayValue(drillDown.runnerJob ?? "-"), ]; }); const candidates = cleanupRows(result.candidates); const selected = cleanupRows(result.selected); if (candidates.length > 0) { lines.push("", `CANDIDATES (showing ${String(candidates.length)}, omitted=${displayValue(result.candidateOmittedCount ?? 0)})`, renderTable(["RUNNERJOB", "POD PHASE", "WAITING", "AGE MS", "CLASSIFICATION", "DRILL-DOWN"], candidates)); } if (selected.length > 0) { lines.push("", `SELECTED (showing ${String(selected.length)}, omitted=${displayValue(result.selectedOmittedCount ?? 0)})`, renderTable(["RUNNERJOB", "POD PHASE", "WAITING", "AGE MS", "CLASSIFICATION", "DRILL-DOWN"], selected)); } const nextLines = renderNextObjectLines(record(result.next ?? result.followUp)); if (nextLines.length > 0) lines.push("", "NEXT", ...nextLines); lines.push("", "DETAIL", " use --full for capture/probe details; valuesPrinted=false"); return renderedCliResult(result.ok !== false, String(result.command ?? "agentrun control-plane"), `${lines.join("\n")}\n`); }