// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. entry module for scripts/src/agentrun.ts. // Moved mechanically from scripts/src/agentrun.ts:1-302 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 { AgentRunResourceVerb, AgentRunRestCompatGroup } from "./utils"; import { controlPlaneApply, controlPlanePlan, parseCleanupReleasedPvOptions, parseCleanupRunnersOptions, parseCleanupRunsOptions, parseConfirmOptions, parseGitMirrorOptions, parseLaneConfirmOptions, parseRefreshOptions, parseSecretSyncOptions, status } from "./control-plane"; import { gitMirrorStatus } from "./git-mirror"; import { agentRunExplain, isRecord, parseGitMirrorStatusOptions, parseStatusOptions, parseTriggerOptions } from "./options"; import { renderAgentRunControlPlaneActionSummary, renderAgentRunControlPlanePlanSummary, renderAgentRunControlPlaneStatusSummary } from "./public-exposure"; import { renderedCliResult } from "./render"; import { agentRunGetKindHelp, runAgentRunResourceCommand } from "./resource-actions"; import { runAgentRunRestCompatCommand, runGitMirrorJob, startAsyncAgentRunJob } from "./rest-bridge"; import { exposeAgentRun, restartYamlLane, secretSync, triggerCurrent } from "./trigger"; import { unsupported } from "./utils"; import { cleanupReleasedPvs, cleanupRunners, cleanupRuns, refresh } from "./yaml-lane"; export function agentRunHelp(): unknown { return { command: "agentrun get|describe|events|logs|result|ack|cancel|dispatch|create|apply|send|explain", output: "human by default; use -o json|yaml or --raw for machine/debug output", usage: [ "bun scripts/cli.ts agentrun get tasks --queue commander --limit 20", "bun scripts/cli.ts agentrun describe run/ --node D601 --lane v02", "bun scripts/cli.ts agentrun get tasks -o wide", "bun scripts/cli.ts agentrun get tasks -o json", "bun scripts/cli.ts agentrun describe task/", "bun scripts/cli.ts agentrun events run/ --after-seq 0 --limit 100", "bun scripts/cli.ts agentrun logs session/ --tail 100", "bun scripts/cli.ts agentrun result run/ --command ", "bun scripts/cli.ts agentrun ack task/ --reader-id cli", "bun scripts/cli.ts agentrun cancel task/ --reason --dry-run", "bun scripts/cli.ts agentrun dispatch task/", "bun scripts/cli.ts agentrun create task --aipod Artificer --prompt-stdin --idempotency-key ", "bun scripts/cli.ts agentrun apply -f - --dry-run", "bun scripts/cli.ts agentrun send session/ --aipod Artificer --prompt-stdin", "bun scripts/cli.ts agentrun explain task", "bun scripts/cli.ts agentrun explain session-policy", "bun scripts/cli.ts agentrun control-plane plan --node D601 --lane v02", "bun scripts/cli.ts agentrun control-plane apply --node D601 --lane v02 --dry-run", "bun scripts/cli.ts agentrun control-plane status --node D601 --lane v02", "bun scripts/cli.ts agentrun control-plane secret-sync --node D601 --lane v02 --dry-run", "bun scripts/cli.ts agentrun control-plane secret-sync --node D601 --lane v02 --confirm", "bun scripts/cli.ts agentrun control-plane restart --node D601 --lane v02 --dry-run", "bun scripts/cli.ts agentrun control-plane restart --node D601 --lane v02 --confirm", "bun scripts/cli.ts agentrun control-plane trigger-current --node D601 --lane v02 --dry-run", "bun scripts/cli.ts agentrun control-plane trigger-current --node D601 --lane v02 --confirm", "bun scripts/cli.ts agentrun control-plane status", "bun scripts/cli.ts agentrun control-plane status --full", "bun scripts/cli.ts agentrun control-plane status --pipeline-run ", "bun scripts/cli.ts agentrun control-plane status --source-commit ", "bun scripts/cli.ts agentrun control-plane expose --dry-run", "bun scripts/cli.ts agentrun control-plane expose --confirm", "bun scripts/cli.ts agentrun control-plane trigger-current --dry-run", "bun scripts/cli.ts agentrun control-plane trigger-current --confirm", "bun scripts/cli.ts agentrun control-plane refresh --dry-run", "bun scripts/cli.ts agentrun control-plane refresh --confirm", "bun scripts/cli.ts agentrun control-plane cleanup-runners --node D601 --lane v02 --dry-run", "bun scripts/cli.ts agentrun control-plane cleanup-runners --node D601 --lane v02 --confirm", "bun scripts/cli.ts agentrun control-plane cleanup-runs --min-age-minutes 30 --limit 200 --dry-run", "bun scripts/cli.ts agentrun control-plane cleanup-runs --min-age-minutes 30 --limit 200 --confirm", "bun scripts/cli.ts agentrun control-plane cleanup-released-pvs --limit 200 --dry-run", "bun scripts/cli.ts agentrun control-plane cleanup-released-pvs --limit 200 --confirm", "bun scripts/cli.ts agentrun git-mirror status", "bun scripts/cli.ts agentrun git-mirror status --full", "bun scripts/cli.ts agentrun git-mirror sync --confirm", "bun scripts/cli.ts agentrun git-mirror flush --confirm", ], resources: ["task/qt", "run", "command/cmd", "runnerjob/rjob", "session/ses", "aipodspec/aps"], description: "Operate AgentRun through Kubernetes-style resource verbs. Human output is compact by default; -o json|yaml returns the UniDesk render-only client schema, --raw exposes the REST envelope, and --node/--lane targets a YAML-declared runtime lane.", legacyCompatibility: "queue/runs/commands/runner/sessions/aipod-specs remain as compatibility groups backed by direct HTTP; new commander work should use get/describe/events/logs/result/ack/cancel/dispatch/create/apply/send. sessions turn/steer are removed; use send only.", }; } export async function runAgentRunCommand(config: UniDeskConfig | null, args: string[]): Promise | RenderedCliResult> { const route = normalizeAgentRunCommandArgs(args); const { group, action, actionArgs } = route; if (group === undefined || isHelpArg(group)) return renderAgentRunHelp(route.canonicalArgs); if (isHelpArg(action) || actionArgs.some(isHelpArg)) return renderAgentRunHelp(route.canonicalArgs); if (isResourceVerb(group)) return await runAgentRunResourceCommand(config, group, action, actionArgs, route.canonicalArgs); if (isAgentRunRestCompatGroup(group)) { return await runAgentRunRestCompatCommand(config, group, route.forwardArgs, route.canonicalArgs); } if (config === null) throw new Error("agentrun control-plane and git-mirror commands require UniDesk config"); if (group === "control-plane") { if (action === "plan") { const options = parseStatusOptions(actionArgs); const result = await controlPlanePlan(config, options); return options.full || options.raw ? result : renderAgentRunControlPlanePlanSummary(result); } if (action === "apply") return await controlPlaneApply(config, parseLaneConfirmOptions(actionArgs)); if (action === "status") { const options = parseStatusOptions(actionArgs); const result = await status(config, options); return options.full || options.raw ? result : renderAgentRunControlPlaneStatusSummary(result); } if (action === "secret-sync") return await secretSync(config, parseSecretSyncOptions(actionArgs)); if (action === "restart") return await restartYamlLane(config, parseLaneConfirmOptions(actionArgs)); if (action === "expose") return await exposeAgentRun(config, parseConfirmOptions(actionArgs)); if (action === "trigger-current") { const options = parseTriggerOptions(actionArgs); const result = await triggerCurrent(config, options); return options.full || options.raw || !isRecord(result.target) ? result : renderAgentRunControlPlaneActionSummary(result, "AGENTRUN TRIGGER CURRENT"); } if (action === "refresh") { const options = parseRefreshOptions(actionArgs); const result = await refresh(config, options); return options.full || options.raw ? result : renderAgentRunControlPlaneActionSummary(result, "AGENTRUN CONTROL-PLANE REFRESH"); } if (action === "cleanup-runners") { const options = parseCleanupRunnersOptions(actionArgs); const result = await cleanupRunners(config, options); return options.full || options.raw ? result : renderAgentRunControlPlaneActionSummary(result, "AGENTRUN RUNNER CLEANUP"); } if (action === "cleanup-runs") return await cleanupRuns(config, parseCleanupRunsOptions(actionArgs)); if (action === "cleanup-released-pvs") return await cleanupReleasedPvs(config, parseCleanupReleasedPvOptions(actionArgs)); } if (group === "git-mirror") { if (action === "status") return await gitMirrorStatus(config, parseGitMirrorStatusOptions(actionArgs)); if (action === "sync" || action === "flush") { const options = parseGitMirrorOptions(actionArgs); const { spec } = resolveAgentRunLaneTarget(options); if (options.confirm && !options.wait) { return startAsyncAgentRunJob( `agentrun_${spec.lane}_git_mirror_${action}`, ["bun", "scripts/cli.ts", "agentrun", "git-mirror", action, "--node", spec.nodeId, "--lane", spec.lane, "--confirm", "--wait", "--timeout-seconds", String(options.timeoutSeconds)], `Run AgentRun ${spec.version} git mirror ${action} on ${spec.nodeId}`, ); } return await runGitMirrorJob(config, action, options); } } return unsupported(route.canonicalArgs); } export function isAgentRunRestCompatGroup(group: string | undefined): group is AgentRunRestCompatGroup { return group === "queue" || group === "sessions" || group === "aipod-specs" || group === "aipods" || group === "runs" || group === "commands" || group === "runner"; } export interface AgentRunCommandRoute { canonicalArgs: string[]; group: string | undefined; action: string | undefined; actionArgs: string[]; forwardArgs: string[]; } export function normalizeAgentRunCommandArgs(args: string[]): AgentRunCommandRoute { const canonicalArgs = args[0] === "v01" ? args.slice(1) : args; const [group, action, ...actionArgs] = canonicalArgs; return { canonicalArgs, group, action, actionArgs, forwardArgs: canonicalArgs.slice(1), }; } export function isHelpArg(value: string | undefined): boolean { return value === "help" || value === "--help" || value === "-h"; } export function isResourceVerb(value: string | undefined): value is AgentRunResourceVerb { return value === "get" || value === "describe" || value === "events" || value === "logs" || value === "result" || value === "ack" || value === "cancel" || value === "dispatch" || value === "create" || value === "apply" || value === "send" || value === "explain"; } export function renderAgentRunHelp(args: string[]): RenderedCliResult { const text = agentRunHelpText(args); return renderedCliResult(true, "agentrun help", text); } export function agentRunHelpText(args: string[]): string { const [verb, kind] = args.filter((arg) => !isHelpArg(arg)); if (verb === "get" && kind !== undefined) return agentRunGetKindHelp(kind); if (verb === "get") { return [ "Usage: bun scripts/cli.ts agentrun get [options]", "", "Resources: tasks|sessions|runs|commands|runnerjobs|aipodspecs", "Output: table by default; use -o wide|name|json|yaml.", "", "Examples:", " bun scripts/cli.ts agentrun get tasks --queue commander --limit 20", " bun scripts/cli.ts agentrun get sessions --limit 20", " bun scripts/cli.ts agentrun get tasks -o json", ].join("\n"); } if (verb === "describe") { return [ "Usage: bun scripts/cli.ts agentrun describe [--node --lane ] [--full] [-o json|yaml] [--raw]", "", "Kinds: task|run|command|runnerjob|session|aipodspec", "Examples:", " bun scripts/cli.ts agentrun describe task/qt_...", " bun scripts/cli.ts agentrun describe run/ --node D601 --lane v02", " bun scripts/cli.ts agentrun describe command/cmd_... --run ", " bun scripts/cli.ts agentrun describe session/ --full", ].join("\n"); } if (verb === "events") { return "Usage: bun scripts/cli.ts agentrun events run/ [--after-seq N] [--limit 100] [-o json|yaml] [--raw]"; } if (verb === "logs") { return "Usage: bun scripts/cli.ts agentrun logs session/ [--tail 100|--after-seq N] [--limit 100] [--full-text] [-o json|yaml] [--raw]"; } if (verb === "result") { return "Usage: bun scripts/cli.ts agentrun result run/ --command | agentrun result command/ --run "; } if (verb === "ack") { return "Usage: bun scripts/cli.ts agentrun ack task/|session/ [--reader-id cli]"; } if (verb === "cancel") { return "Usage: bun scripts/cli.ts agentrun cancel task/|session/|run/|command/ --reason [--run ] [--dry-run]"; } if (verb === "dispatch") { return "Usage: bun scripts/cli.ts agentrun dispatch task/"; } if (verb === "create") { return "Usage: bun scripts/cli.ts agentrun create task --aipod Artificer --prompt-stdin [--idempotency-key ] [--dry-run]"; } if (verb === "apply") { return "Usage: bun scripts/cli.ts agentrun apply -f task.yaml|json|- [--dry-run]\nTask manifests use kind: Task and spec: ."; } if (verb === "send") { return "Usage: bun scripts/cli.ts agentrun send session/ --aipod Artificer --prompt-stdin\nThe server decides whether this becomes an internal steer or a new turn from session state."; } if (verb === "explain") return agentRunExplain(kind ?? "task"); if (verb === "control-plane") { return [ "Usage: bun scripts/cli.ts agentrun control-plane [options]", "", "Actions: plan, apply, status, secret-sync, expose, trigger-current, refresh, cleanup-runners, cleanup-runs, cleanup-released-pvs", "Examples:", " bun scripts/cli.ts agentrun control-plane plan --node D601 --lane v02", " bun scripts/cli.ts agentrun control-plane apply --node D601 --lane v02 --dry-run", " bun scripts/cli.ts agentrun control-plane status --node D601 --lane v02", " bun scripts/cli.ts agentrun control-plane secret-sync --node D601 --lane v02 --dry-run", " bun scripts/cli.ts agentrun control-plane trigger-current --node D601 --lane v02 --dry-run", " bun scripts/cli.ts agentrun control-plane status", " bun scripts/cli.ts agentrun control-plane status --pipeline-run ", " bun scripts/cli.ts agentrun control-plane expose --dry-run", " bun scripts/cli.ts agentrun control-plane trigger-current --dry-run", " bun scripts/cli.ts agentrun control-plane cleanup-runners --node D601 --lane v02 --dry-run", " bun scripts/cli.ts agentrun control-plane cleanup-runs --min-age-minutes 30 --limit 200 --dry-run", ].join("\n"); } if (verb === "git-mirror") { return [ "Usage: bun scripts/cli.ts agentrun git-mirror [--full|--raw|--confirm]", "", "Confirmed sync/flush returns an async job unless --wait is set.", ].join("\n"); } if (verb !== undefined && isAgentRunRestCompatGroup(verb)) { return [ `Compatibility group: agentrun ${verb} ...`, "", "Use resource primitives for daily commander work:", " bun scripts/cli.ts agentrun get tasks --queue commander --limit 20", " bun scripts/cli.ts agentrun describe task/", " bun scripts/cli.ts agentrun describe run/ --node D601 --lane v02", " bun scripts/cli.ts agentrun events run/ --after-seq 0 --limit 100", " bun scripts/cli.ts agentrun logs session/ --tail 100", "", "Use --raw on a resource command when you need the direct AgentRun REST envelope.", ].join("\n"); } return [ "Usage: bun scripts/cli.ts agentrun [options]", "", "Verbs: get, describe, events, logs, result, ack, cancel, dispatch, create, apply, send, explain", "Resources: task/qt, run, command/cmd, runnerjob/rjob, session/ses, aipodspec/aps", "", "Common:", " bun scripts/cli.ts agentrun get tasks --queue commander --limit 20", " bun scripts/cli.ts agentrun describe task/", " bun scripts/cli.ts agentrun describe run/ --node D601 --lane v02", " bun scripts/cli.ts agentrun events run/ --after-seq 0 --limit 100", " bun scripts/cli.ts agentrun logs session/ --tail 100", " bun scripts/cli.ts agentrun create task --aipod Artificer --prompt-stdin", "", "Machine/debug output:", " -o json|yaml emits a stable UniDesk resource object without the global JSON envelope.", " --raw emits the direct AgentRun REST envelope.", " --node/--lane targets a YAML-declared AgentRun runtime lane; default remains the configured public manager.", ].join("\n"); }