319 lines
16 KiB
TypeScript
319 lines
16 KiB
TypeScript
// 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<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 deploymentReplicaSummary(manager: Record<string, unknown>): 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<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 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<string, unknown>): 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<string, unknown>, 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`);
|
|
}
|