From d1f852441af2157d78dc96c84d955674c4666918 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 15 Jul 2026 10:28:29 +0200 Subject: [PATCH 1/7] =?UTF-8?q?docs:=20=E7=99=BB=E8=AE=B0=20PaC=20?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E5=8F=AF=E8=A7=81=E6=80=A7=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/MDTODO/pr-merge-driven-automatic-delivery.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/MDTODO/pr-merge-driven-automatic-delivery.md b/docs/MDTODO/pr-merge-driven-automatic-delivery.md index 08b872a7..a54c1526 100644 --- a/docs/MDTODO/pr-merge-driven-automatic-delivery.md +++ b/docs/MDTODO/pr-merge-driven-automatic-delivery.md @@ -109,3 +109,7 @@ ### R7.2 [in_progress] 建立 [UniDesk #2010](https://github.com/pikasTech/unidesk/issues/2010):跟随 release branch 的独立 production PaC/Tekton/GitOps/Argo、namespace、host PostgreSQL database/role/Secret/migration ledger、Kafka consumer group 与 https://hwlab.pikapython.com YAML publicExposure;release 只从完成 P0 Kafka 纠偏并验证的 v0.3 commit 初始化,完成任务后将详细报告写入[任务报告](./details/pr-merge-driven-automatic-delivery/R7.2_Task_Report.md)。 + +## R8 [in_progress] + +解决 [UniDesk #2165](https://github.com/pikasTech/unidesk/issues/2165):修复 PaC `status/history/debug-step` 与 AgentRun control-plane 状态命令在远端 timeout、空 payload、解析失败或异常时以空 stdout/不明 exit 结束的问题;保持目标 Kubernetes/Tekton/Argo/runtime 为唯一状态 authority,所有成功、partial、timeout 和 failure 都输出有界 typed 摘要、耗时、first break 与只读下钻,禁止第二状态源、无界 dump、交付 mutation、PK01 修改、合同/租约/围栏和 Vitest,完成任务后将详细报告写入[任务报告](./details/pr-merge-driven-automatic-delivery/R8_Task_Report.md)。 From fe23d35c6e2cee352cd29682a1e89a2de9ffa347 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 15 Jul 2026 10:52:15 +0200 Subject: [PATCH 2/7] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20PaC=20=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E5=91=BD=E4=BB=A4=E7=A9=BA=E8=BE=93=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/src/agentrun/control-plane.ts | 20 ++- scripts/src/agentrun/public-exposure.ts | 15 +- scripts/src/agentrun/utils.ts | 4 +- ...latform-infra-pac-failure-evidence.test.ts | 31 +++- .../src/platform-infra-pipelines-as-code.ts | 163 ++++++++++++++++-- 5 files changed, 208 insertions(+), 25 deletions(-) diff --git a/scripts/src/agentrun/control-plane.ts b/scripts/src/agentrun/control-plane.ts index 25a76ffc..7ecde0e8 100644 --- a/scripts/src/agentrun/control-plane.ts +++ b/scripts/src/agentrun/control-plane.ts @@ -47,6 +47,8 @@ import { compactAgentRunLaneStatusTarget, compactLaneSecretsStatus } from "./tri import { capture, captureJsonPayload, compactCapture, record, stringOrNull, timedStatusStage } from "./utils"; import { yamlLaneK3sSourceStatusScript, yamlLaneRuntimeStatusScript, yamlLaneSourceStatusScript } from "./yaml-lane"; +const agentRunStatusCaptureTimeoutMs = 20_000; + export function parseSecretSyncOptions(args: string[]): SecretSyncOptions { const base = parseConfirmOptions(args); let node: string | null = null; @@ -409,13 +411,15 @@ function giteaMirrorStatusCommand(spec: AgentRunLaneSpec): string { export async function statusPipelinesAsCodeLane(config: UniDeskConfig, options: StatusOptions, target: { configPath: string; spec: AgentRunLaneSpec }, authority: Extract): Promise> { const spec = target.spec; const consumerId = authority.consumer.consumerId; - const pacStatus = record(await runPlatformInfraPipelinesAsCodeCommand(config, ["status", "--target", spec.nodeId, "--consumer", consumerId, "--full"])); + const pacProbe = await timedStatusStage("pac", () => runPlatformInfraPipelinesAsCodeCommand(config, ["status", "--target", spec.nodeId, "--consumer", consumerId, "--full"])); + const pacStatus = record(pacProbe.value); const pacSummary = record(pacStatus.summary); + const pacReadObservation = record(pacStatus.observation); const latestPipelineRun = record(pacSummary.latestPipelineRun); const artifact = record(pacSummary.artifact); const pacArgo = record(pacSummary.argo); const pipelineRunName = options.pipelineRun ?? stringOrNull(latestPipelineRun.name); - const runtimeProbe = await timedStatusStage("runtime", () => capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneRuntimeStatusScript(spec, pipelineRunName)])); + const runtimeProbe = await timedStatusStage("runtime", () => capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneRuntimeStatusScript(spec, pipelineRunName)], { runtimeTimeoutMs: agentRunStatusCaptureTimeoutMs })); const runtimePayload = captureJsonPayload(runtimeProbe.value); const manager = record(runtimePayload.manager); const database = record(runtimePayload.database); @@ -479,6 +483,7 @@ export async function statusPipelinesAsCodeLane(config: UniDeskConfig, options: pac: { ready: pacSummary.ready ?? false, webhookCount: pacSummary.webhookCount ?? null, + readObservation: pacReadObservation, latestPipelineRun: { name: pipelineRunName, status: latestPipelineRun.status ?? null, @@ -539,8 +544,9 @@ export async function statusPipelinesAsCodeLane(config: UniDeskConfig, options: migration, }, timings: { + pacMs: pacProbe.elapsedMs, runtimeMs: runtimeProbe.elapsedMs, - totalMs: runtimeProbe.elapsedMs, + totalMs: pacProbe.elapsedMs + runtimeProbe.elapsedMs, }, disclosure: { output: options.full || options.raw ? "full" : "compact-summary", @@ -636,8 +642,8 @@ export async function statusYamlLane(config: UniDeskConfig, options: StatusOptio if (deliveryAuthority.kind === "pac-pr-merge") return await statusPipelinesAsCodeLane(config, options, target, deliveryAuthority); if (deliveryAuthority.kind === "unknown") return unknownDeliveryAuthorityStatus(options, target, deliveryAuthority); const sourceProbe = await timedStatusStage("source", () => spec.source.statusMode === "k3s-git-mirror" - ? capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneK3sSourceStatusScript(spec)]) - : capture(config, `${spec.nodeRoute}:${spec.source.workspace}`, ["sh", "--", yamlLaneSourceStatusScript(spec)])); + ? capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneK3sSourceStatusScript(spec)], { runtimeTimeoutMs: agentRunStatusCaptureTimeoutMs }) + : capture(config, `${spec.nodeRoute}:${spec.source.workspace}`, ["sh", "--", yamlLaneSourceStatusScript(spec)], { runtimeTimeoutMs: agentRunStatusCaptureTimeoutMs })); const sourcePayload = captureJsonPayload(sourceProbe.value); const branchTipCommit = stringOrNull(sourcePayload.remoteBranchCommit) ?? stringOrNull(sourcePayload.localHead); const initialSourceCommit = options.sourceCommit @@ -645,8 +651,8 @@ export async function statusYamlLane(config: UniDeskConfig, options: StatusOptio ?? stringOrNull(sourcePayload.localHead); const pipelineRunName = options.pipelineRun ?? (initialSourceCommit ? agentRunPipelineRunName(spec, initialSourceCommit) : null); const [runtimeProbe, mirrorProbe] = await Promise.all([ - timedStatusStage("runtime", () => capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneRuntimeStatusScript(spec, pipelineRunName)])), - timedStatusStage("git-mirror", () => capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneGitMirrorStatusScript(spec)])), + timedStatusStage("runtime", () => capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneRuntimeStatusScript(spec, pipelineRunName)], { runtimeTimeoutMs: agentRunStatusCaptureTimeoutMs })), + timedStatusStage("git-mirror", () => capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneGitMirrorStatusScript(spec)], { runtimeTimeoutMs: agentRunStatusCaptureTimeoutMs })), ]); const runtimePayload = captureJsonPayload(runtimeProbe.value); const mirrorPayload = captureJsonPayload(mirrorProbe.value); diff --git a/scripts/src/agentrun/public-exposure.ts b/scripts/src/agentrun/public-exposure.ts index 7c3feb81..2a203f1b 100644 --- a/scripts/src/agentrun/public-exposure.ts +++ b/scripts/src/agentrun/public-exposure.ts @@ -80,6 +80,8 @@ export function renderAgentRunControlPlaneStatusSummary(result: Record `- ${warning}`)].join("\n"), "", "NEXT", @@ -127,7 +140,7 @@ export function renderAgentRunControlPlaneStatusSummary(result: Record line !== null); return renderedCliResult(result.ok !== false, "agentrun control-plane status", `${lines.join("\n")}\n`); } diff --git a/scripts/src/agentrun/utils.ts b/scripts/src/agentrun/utils.ts index b22068c7..83d5a770 100644 --- a/scripts/src/agentrun/utils.ts +++ b/scripts/src/agentrun/utils.ts @@ -137,8 +137,8 @@ export type AgentRunBridgeCaptureResult = SshCaptureResult & { bridgeExecution?: export let localBackendCoreStatusCache: LocalBackendCoreStatus | null = null; -export async function capture(config: UniDeskConfig, target: string, args: string[]): Promise { - const result = await runSshCommandCapture(config, target, args); +export async function capture(config: UniDeskConfig, target: string, args: string[], options: { runtimeTimeoutMs?: number } = {}): Promise { + const result = await runSshCommandCapture(config, target, args, undefined, options); const plan = agentRunBridgeCapturePlan(config, target); return attachBridgeExecution(result, { ...plan, reason: `unified-ssh-capture:${plan.reason}` }); } diff --git a/scripts/src/platform-infra-pac-failure-evidence.test.ts b/scripts/src/platform-infra-pac-failure-evidence.test.ts index 63c28769..bf2a89f5 100644 --- a/scripts/src/platform-infra-pac-failure-evidence.test.ts +++ b/scripts/src/platform-infra-pac-failure-evidence.test.ts @@ -5,7 +5,7 @@ import { resolve } from "node:path"; import { createRequire } from "node:module"; import { sentinelPublishImageBuildShell } from "./hwlab-node-web-sentinel-cicd-jobs"; import type { SentinelCicdState } from "./hwlab-node-web-sentinel-cicd-shared"; -import { compactHistoryJson, compactStatusSummary, renderDebugStep, renderHistory, renderStatus } from "./platform-infra-pipelines-as-code"; +import { compactHistoryJson, compactStatusSummary, pacReadOnlyCaptureObservation, renderDebugStep, renderHistory, renderStatus } from "./platform-infra-pipelines-as-code"; const require = createRequire(import.meta.url); const { parsePacLogRecords } = require("../native/cicd/pac-status-evaluator.cjs") as { @@ -55,6 +55,35 @@ describe("PaC 失败证据合同", () => { expect(compactStatus).toMatchObject({ artifact: { traceId: row.traceId, firstBreak: row.firstBreak, error: row.error } }); }); + test("只读 capture timeout 保留阶段、预算、首断点与下一步", () => { + const observation = pacReadOnlyCaptureObservation({ + action: "history", + targetId: "NC01", + consumerId: "agentrun-nc01-v02", + result: { exitCode: 124, stdout: "", stderr: "UNIDESK_SSH_RUNTIME_TIMEOUT" }, + parsed: null, + elapsedMs: 25001, + timeoutMs: 25000, + next: "bun scripts/cli.ts platform-infra pipelines-as-code history --target NC01 --consumer agentrun-nc01-v02 --limit 5", + }); + expect(observation).toMatchObject({ + ok: false, + status: "timeout", + target: "NC01", + consumer: "agentrun-nc01-v02", + stage: "history-remote-observe", + elapsedMs: 25001, + timeoutMs: 25000, + exitCode: 124, + firstBreak: { code: "pac-read-timeout", phase: "remote-read" }, + }); + const output = renderHistory({ rows: [], config: {}, historyErrors: [], deliveryBudget: {}, observation, next: {} }).renderedText; + expect(output).toContain("READ OBSERVATION"); + expect(output).toContain("history-remote-observe"); + expect(output).toContain("pac-read-timeout"); + expect(output).toContain("next-read-only:"); + }); + test("feature-config warning 在 status、history、debug-step 同构有界投影", () => { const observation = extractPacSourceObservation([ { diff --git a/scripts/src/platform-infra-pipelines-as-code.ts b/scripts/src/platform-infra-pipelines-as-code.ts index d04a0d42..8219e4a1 100644 --- a/scripts/src/platform-infra-pipelines-as-code.ts +++ b/scripts/src/platform-infra-pipelines-as-code.ts @@ -52,6 +52,7 @@ const remoteScriptFile = rootPath("scripts", "src", "platform-infra-pipelines-as const evaluatorFile = rootPath("scripts", "native", "cicd", "pac-status-evaluator.cjs"); const sourceArtifactRuntimeObserverFile = rootPath("scripts", "native", "cicd", "pac-source-artifact-runtime.mjs"); const fieldManager = "unidesk-platform-infra-pipelines-as-code"; +const pacReadOnlyCaptureTimeoutMs = 25_000; const y = createYamlFieldReader(configLabel); function kubernetesLabelValue(value: string): string { @@ -293,6 +294,25 @@ interface SecretMaterial { webhookPath: string; } +type PacReadOnlyAction = "status" | "history" | "debug-step"; + +interface PacReadOnlyCaptureObservation { + readonly ok: boolean; + readonly status: "succeeded" | "timeout" | "remote-failure" | "empty-response" | "parse-failure"; + readonly target: string; + readonly consumer: string; + readonly stage: string; + readonly elapsedMs: number; + readonly timeoutMs: number; + readonly exitCode: number; + readonly stdoutBytes: number; + readonly stderrBytes: number; + readonly partial: boolean; + readonly firstBreak: { readonly code: string; readonly phase: string }; + readonly next: string; + readonly valuesPrinted: false; +} + export async function runPlatformInfraPipelinesAsCodeCommand(config: UniDeskConfig, args: string[]): Promise | RenderedCliResult> { const [action = "plan"] = args; if (action === "help" || action === "--help") return help(args[1] ?? null); @@ -1350,14 +1370,15 @@ async function status(config: UniDeskConfig, options: CommonOptions): Promise record(observation.warning)) .filter((warning) => Object.keys(warning).length > 0); return { - ok: result.exitCode === 0 && parsed?.ok !== false && historyErrors.length === 0, + ok: observation.ok && parsed?.ok !== false && historyErrors.length === 0, action: "platform-infra-pipelines-as-code-history", mutation: false, target: targetSummary(target), @@ -1633,6 +1658,7 @@ async function history(config: UniDeskConfig, options: HistoryOptions): Promise< detailId: options.detailId, rows, deliveryBudget, + observation, historyErrors, warnings: [...pac.validationWarnings, ...deliveryWarnings], controlPlane: parsed === null ? undefined : { @@ -1646,9 +1672,7 @@ async function history(config: UniDeskConfig, options: HistoryOptions): Promise< valuesPrinted: false, }, remote: parsed === null || options.raw ? remote : undefined, - next: selection.nextConsumerId === null - ? pacNodeReadOnlyNext(target.id) - : pacReadOnlyNext(target.id, selection.nextConsumerId), + next, }; } @@ -1698,12 +1722,13 @@ async function debugStep(config: UniDeskConfig, options: HistoryOptions): Promis } const consumer = resolveConsumer(pac, selection.selectedConsumerIds[0] ?? null); const repository = resolveRepository(pac, consumer.repositoryRef); - const result = await capture(config, target.route, ["sh"], remoteScript("debug-step", pac, target, repository, consumer, { ...options, confirm: false, dryRun: true, wait: false }, emptySecretMaterial(), "")); - const parsed = parseJsonOutput(result.stdout); + const next = nextCommands(target.id, consumer.id, pac.defaults.consumerId); + const observed = await capturePacReadOnly(config, "debug-step", target, consumer, remoteScript("debug-step", pac, target, repository, consumer, { ...options, confirm: false, dryRun: true, wait: false }, emptySecretMaterial(), ""), stringValue(next.status)); + const { result, parsed, observation } = observed; const fixtureChecks = parsed === null ? [] : arrayRecords(parsed.checks); const fixtureDisclosure = pacDebugFixtureDisclosure(fixtureChecks, options.detailId); return { - ok: result.exitCode === 0 && parsed?.ok === true, + ok: observation.ok && parsed?.ok === true, action: "platform-infra-pipelines-as-code-debug-step", mutation: false, target: targetSummary(target), @@ -1713,12 +1738,97 @@ async function debugStep(config: UniDeskConfig, options: HistoryOptions): Promis fixtureGate: { ...fixtureDisclosure.fixtureGate, ok: parsed !== null && fixtureDisclosure.fixtureGate.ok === true }, checks: fixtureDisclosure.checks, realRun: parsed === null ? null : parsed.realRun ?? null, + observation, remote: parsed === null || options.raw ? parsed ?? compactCapture(result, { full: true }) : undefined, - next: nextCommands(target.id, consumer.id, pac.defaults.consumerId), + next, valuesPrinted: false, }; } +async function capturePacReadOnly( + config: UniDeskConfig, + action: PacReadOnlyAction, + target: PacTarget, + consumer: PacConsumer, + script: string, + next: string, +): Promise<{ readonly result: Awaited>; readonly parsed: Record | null; readonly observation: PacReadOnlyCaptureObservation }> { + const stage = `${action}-remote-observe`; + const startedAt = Date.now(); + printPacReadOnlyProgress(target, consumer, stage, "started", startedAt, { timeoutMs: pacReadOnlyCaptureTimeoutMs }); + const result = await capture(config, target.route, ["sh"], script, { runtimeTimeoutMs: pacReadOnlyCaptureTimeoutMs }); + const parsed = parseJsonOutput(result.stdout); + const observation = pacReadOnlyCaptureObservation({ + action, + targetId: target.id, + consumerId: consumer.id, + result, + parsed, + elapsedMs: Date.now() - startedAt, + timeoutMs: pacReadOnlyCaptureTimeoutMs, + next, + }); + printPacReadOnlyProgress(target, consumer, stage, observation.status, startedAt, { + exitCode: result.exitCode, + firstBreak: observation.firstBreak.code, + }); + return { result, parsed, observation }; +} + +export function pacReadOnlyCaptureObservation(options: { + readonly action: PacReadOnlyAction; + readonly targetId: string; + readonly consumerId: string; + readonly result: { readonly exitCode: number; readonly stdout: string; readonly stderr: string }; + readonly parsed: Record | null; + readonly elapsedMs: number; + readonly timeoutMs: number; + readonly next: string; +}): PacReadOnlyCaptureObservation { + const stdoutBytes = Buffer.byteLength(options.result.stdout, "utf8"); + const stderrBytes = Buffer.byteLength(options.result.stderr, "utf8"); + const timedOut = options.result.exitCode === 124 || /ssh-runtime-timeout|top-level runtime limit/iu.test(options.result.stderr); + const status = timedOut + ? "timeout" + : options.result.exitCode !== 0 + ? "remote-failure" + : options.result.stdout.trim().length === 0 + ? "empty-response" + : options.parsed === null + ? "parse-failure" + : "succeeded"; + const firstBreakCode = status === "succeeded" ? "none" : `pac-read-${status}`; + return { + ok: status === "succeeded", + status, + target: options.targetId, + consumer: options.consumerId, + stage: `${options.action}-remote-observe`, + elapsedMs: options.elapsedMs, + timeoutMs: options.timeoutMs, + exitCode: options.result.exitCode, + stdoutBytes, + stderrBytes, + partial: stdoutBytes > 0 || stderrBytes > 0, + firstBreak: { code: firstBreakCode, phase: "remote-read" }, + next: options.next, + valuesPrinted: false, + }; +} + +function printPacReadOnlyProgress(target: PacTarget, consumer: PacConsumer, stage: string, status: string, startedAt: number, detail: Record): void { + process.stderr.write(`${JSON.stringify({ + event: "platform-infra.pipelines-as-code.read.progress", + target: target.id, + consumer: consumer.id, + stage, + status, + elapsedMs: Date.now() - startedAt, + ...detail, + valuesPrinted: false, + })}\n`); +} + export function pacDebugFixtureDisclosure(fixtureChecks: readonly Record[], detailId: string | null): { readonly fixtureGate: Record; readonly checks: readonly Record[]; @@ -2718,6 +2828,7 @@ export function renderStatus(result: Record): RenderedCliResult ]]), ` hint: ${compactLine(stringValue(diagnostics.hint))}`, ` pipeline-run: ${latest.name === undefined ? "-" : `bun scripts/cli.ts platform-infra pipelines-as-code history --target ${stringValue(record(result.target).id)} --id ${stringValue(latest.name)}`}`, + ...renderPacReadObservation(result), "", "NEXT", ` full: ${stringValue(record(result.next).status)} --full`, @@ -2912,6 +3023,7 @@ export function renderHistory(result: Record): RenderedCliResul ]; }))), ...(detailId === "-" || rows.length !== 1 ? [] : renderHistoryDetail(rows[0] ?? {})), + ...renderPacReadObservation(result), "", "NEXT", rows.length > 0 && detailId === "-" ? ` detail: ${stringValue(record(result.next).history)} --id ${stringValue(rows[0]?.id ?? rows[0]?.pipelineRun)}` : null, @@ -2987,6 +3099,7 @@ export function renderDebugStep(result: Record): RenderedCliRes `${stringValue(item.expectedOk)}/${stringValue(item.expectedCode)}`, `${stringValue(item.actualOk)}/${stringValue(item.actualCode)}`, ]))), + ...renderPacReadObservation(result), "", "NEXT", ` status: ${stringValue(record(result.next).status)}`, @@ -2994,6 +3107,28 @@ export function renderDebugStep(result: Record): RenderedCliRes return rendered(result, "platform-infra pipelines-as-code debug-step", lines); } +function renderPacReadObservation(result: Record): string[] { + const observation = record(result.observation); + if (Object.keys(observation).length === 0) return []; + const firstBreak = record(observation.firstBreak); + return [ + "", + "READ OBSERVATION", + ...table(["TARGET", "CONSUMER", "STAGE", "STATUS", "ELAPSED_MS", "TIMEOUT_MS", "EXIT", "PARTIAL", "FIRST_BREAK"], [[ + stringValue(observation.target), + stringValue(observation.consumer), + stringValue(observation.stage), + stringValue(observation.status), + stringValue(observation.elapsedMs), + stringValue(observation.timeoutMs), + stringValue(observation.exitCode), + boolText(observation.partial), + stringValue(firstBreak.code), + ]]), + ` next-read-only: ${stringValue(observation.next)}`, + ]; +} + function parseApplyOptions(args: string[]): ApplyOptions { const commonArgs: string[] = []; let confirm = false; From 18d0bd89b2443d3c1652e8e589a847fbc2876602 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 15 Jul 2026 11:25:24 +0200 Subject: [PATCH 3/7] =?UTF-8?q?fix:=20=E5=B0=86=E5=8F=AA=E8=AF=BB=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E9=A2=84=E7=AE=97=E6=94=B6=E6=95=9B=E5=88=B0=20YAML?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/agentrun.yaml | 3 + config/platform-infra/pipelines-as-code.yaml | 2 + .../R8_Task_Report.md | 79 +++++++++++++++++++ scripts/src/agentrun-lanes.ts | 14 ++++ scripts/src/agentrun/control-plane.ts | 14 ++-- ...latform-infra-pac-failure-evidence.test.ts | 3 + .../src/platform-infra-pipelines-as-code.ts | 30 +++++-- 7 files changed, 131 insertions(+), 14 deletions(-) create mode 100644 docs/MDTODO/details/pr-merge-driven-automatic-delivery/R8_Task_Report.md diff --git a/config/agentrun.yaml b/config/agentrun.yaml index 6df6b161..944b1089 100644 --- a/config/agentrun.yaml +++ b/config/agentrun.yaml @@ -59,6 +59,9 @@ controlPlane: unideskWorkspace: /root/unidesk lanes: nc01-v02: + status: + readOnlyCapture: + timeoutMs: 20000 deployment: format: unidesk-yaml-only gitopsRoot: deploy/gitops/node/nc01 diff --git a/config/platform-infra/pipelines-as-code.yaml b/config/platform-infra/pipelines-as-code.yaml index edb859cb..1206b01e 100644 --- a/config/platform-infra/pipelines-as-code.yaml +++ b/config/platform-infra/pipelines-as-code.yaml @@ -23,6 +23,8 @@ deliveryTiming: endToEndBudgetSeconds: 120 observability: configRef: config/platform-infra/observability.yaml + readOnlyCapture: + timeoutMs: 25000 tracesEndpoint: http://otel-collector.platform-infra.svc.cluster.local:4318/v1/traces serviceName: unidesk-cicd propagation: diff --git a/docs/MDTODO/details/pr-merge-driven-automatic-delivery/R8_Task_Report.md b/docs/MDTODO/details/pr-merge-driven-automatic-delivery/R8_Task_Report.md new file mode 100644 index 00000000..6545ff78 --- /dev/null +++ b/docs/MDTODO/details/pr-merge-driven-automatic-delivery/R8_Task_Report.md @@ -0,0 +1,79 @@ +# R8 任务报告 + +## 任务定位 + +- MDTODO:`R8`。 +- Issue:`pikasTech/unidesk#2165`。 +- PR:`pikasTech/unidesk#2168`。 +- 本轮范围:仅处理主代理 blocker comment `#issuecomment-4978743309`。 +- Target:`NC01`。 +- targetWorkspace:`/root/unidesk/.worktree/2165-pac-status-visible`。 +- ref:`fix/2165-pac-status-visible`。 + +## 根因 + +- 首轮修复已建立稳定的 typed observation,使 PaC `status/history/debug-step` 与 AgentRun `control-plane status` 在 timeout、remote failure、empty response 和 parse failure 时不再以空 stdout 结束。 +- 首轮仍把 PaC 只读 capture 预算 `25000ms` 与 AgentRun status capture 预算 `20000ms` 写成 TypeScript 常量。 +- 这些预算属于可调运维事实;源码常量绕过 owning YAML,无法披露来源路径,也无法在选中对象缺失预算时 fail-closed。 + +## 官方最佳实践 + +- 遵循仓库 `AGENTS.md` 与 `docs/reference/yaml-first-ops.md`:owning YAML 是可调运维事实的唯一真相,parser 只做结构与类型校验,代码只消费已验证值。 +- 遵循 `docs/reference/cli.md`:远端 timeout、空响应和解析失败必须形成有界 typed 输出,并保留稳定退出语义与只读 drill-down。 +- 遵循 Issue #2165 的 P0 SPEC:Kubernetes、Tekton、Argo 和既有 runtime 聚合器继续作为状态 authority;预算配置不新增状态源、缓存 authority 或交付 mutation。 + +## Current Control Flow + +1. CLI 读取 PaC 或 AgentRun YAML。 +2. parser 解析目标、consumer 或 lane。 +3. capture 调用使用 TypeScript 文件内固定的 timeout 常量。 +4. typed observation 披露实际 timeout 数值,但无法证明该数值的 owning config path。 +5. YAML 缺失预算时仍可继续执行,不能按当前选中对象 fail-closed。 + +## Desired Control Flow + +1. PaC parser 必填读取 `config/platform-infra/pipelines-as-code.yaml#observability.readOnlyCapture.timeoutMs`。 +2. AgentRun lane parser 必填读取 `config/agentrun.yaml#controlPlane.lanes..status.readOnlyCapture.timeoutMs`。 +3. parser 将预算值与精确 config path 放入 typed spec,不提供源码默认值。 +4. capture 只使用已验证的 YAML 预算。 +5. PaC text/JSON observation 与 AgentRun status text/JSON 摘要同时披露 config path 和实际 timeout。 +6. 缺失字段时 parser 在发起远端读取前失败,并指出缺失 YAML 路径。 + +## 修改 + +- `config/platform-infra/pipelines-as-code.yaml` + - 新增 `observability.readOnlyCapture.timeoutMs: 25000`。 +- `scripts/src/platform-infra-pipelines-as-code.ts` + - 删除 `pacReadOnlyCaptureTimeoutMs` 源码常量。 + - parser 必填校验预算并记录精确 config path。 + - `status/history/debug-step` capture 使用 YAML 预算。 + - typed observation 与默认 text 输出披露预算路径和实际数值。 +- `config/agentrun.yaml` + - 在 `controlPlane.lanes.nc01-v02.status.readOnlyCapture.timeoutMs` 声明 `20000`。 +- `scripts/src/agentrun-lanes.ts` + - lane parser 必填解析 status capture 预算与精确 config path。 +- `scripts/src/agentrun/control-plane.ts` + - 删除 `agentRunStatusCaptureTimeoutMs` 源码常量。 + - source、runtime 和 git-mirror 只读 capture 使用选中 lane 的 YAML 预算。 + - status 结果披露 `readCaptureBudget`。 + +## 验证证据 + +- 定向 Bun 测试验证 PaC typed observation 保留 timeout、config path、首断点与只读 Next。 +- TypeScript 语法检查覆盖修改的 parser、PaC 与 AgentRun control-plane 文件。 +- YAML 读取和原 CLI 入口验证证明预算由选中配置解析,并在 text/JSON 中可见。 +- `git diff --check` 验证补丁格式。 +- 所有目标源码、Git、验证和原入口观察均通过 `trans NC01:/root/unidesk/.worktree/2165-pac-status-visible ...` 执行。 + +## 权限与变更边界 + +- 保留现有 typed observation、即时 progress、只读 Next 和非 mutation 行为。 +- 未增加 env、ConfigMap、数据库、缓存或第二状态源 authority。 +- 未增加合同、租约、安全围栏或锁。 +- 未执行 Vitest、PK01 操作、人工 PipelineRun、Argo、mirror 或其他交付 mutation。 +- 不自行合并 PR;管理性结论由主代理审核。 + +## 剩余边界 + +- Issue #2167 不属于 R8 当前 blocker 修复范围,本轮不扩展其 authority、实现或验收。 +- PR 合并与后续交付由主代理审核后决定。 diff --git a/scripts/src/agentrun-lanes.ts b/scripts/src/agentrun-lanes.ts index 8fa8d13b..dbfc0828 100644 --- a/scripts/src/agentrun-lanes.ts +++ b/scripts/src/agentrun-lanes.ts @@ -173,6 +173,12 @@ export interface AgentRunLaneSpec { readonly nodeKubeRoute: string; readonly nodeUnideskWorkspace: string; readonly version: string; + readonly status: { + readonly readOnlyCapture: { + readonly timeoutMs: number; + readonly configPath: string; + }; + }; readonly source: { readonly statusMode: "host-worktree" | "k3s-git-mirror"; readonly repository: string; @@ -677,6 +683,8 @@ function parseLanes(input: Record, nodes: Record, configPath: string): AgentRunLaneSpec { const path = `${configPath}.controlPlane.lanes.${lane}`; + const status = recordField(input, "status", path); + const readOnlyCapture = recordField(status, "readOnlyCapture", `${path}.status`); const source = recordField(input, "source", path); const runtime = recordField(input, "runtime", path); const ci = recordField(input, "ci", path); @@ -694,6 +702,12 @@ function parseLane(lane: string, node: AgentRunNodeSpec, input: Record capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneRuntimeStatusScript(spec, pipelineRunName)], { runtimeTimeoutMs: agentRunStatusCaptureTimeoutMs })); + const runtimeProbe = await timedStatusStage("runtime", () => capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneRuntimeStatusScript(spec, pipelineRunName)], { runtimeTimeoutMs: spec.status.readOnlyCapture.timeoutMs })); const runtimePayload = captureJsonPayload(runtimeProbe.value); const manager = record(runtimePayload.manager); const database = record(runtimePayload.database); @@ -530,6 +528,7 @@ export async function statusPipelinesAsCodeLane(config: UniDeskConfig, options: command: "agentrun control-plane status", mode: "pipelines-as-code-migrated-lane", configPath: target.configPath, + readCaptureBudget: spec.status.readOnlyCapture, target: options.full || options.raw ? agentRunLaneSummary(spec) : compactAgentRunLaneStatusTarget(spec), summary, alignment: { @@ -642,8 +641,8 @@ export async function statusYamlLane(config: UniDeskConfig, options: StatusOptio if (deliveryAuthority.kind === "pac-pr-merge") return await statusPipelinesAsCodeLane(config, options, target, deliveryAuthority); if (deliveryAuthority.kind === "unknown") return unknownDeliveryAuthorityStatus(options, target, deliveryAuthority); const sourceProbe = await timedStatusStage("source", () => spec.source.statusMode === "k3s-git-mirror" - ? capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneK3sSourceStatusScript(spec)], { runtimeTimeoutMs: agentRunStatusCaptureTimeoutMs }) - : capture(config, `${spec.nodeRoute}:${spec.source.workspace}`, ["sh", "--", yamlLaneSourceStatusScript(spec)], { runtimeTimeoutMs: agentRunStatusCaptureTimeoutMs })); + ? capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneK3sSourceStatusScript(spec)], { runtimeTimeoutMs: spec.status.readOnlyCapture.timeoutMs }) + : capture(config, `${spec.nodeRoute}:${spec.source.workspace}`, ["sh", "--", yamlLaneSourceStatusScript(spec)], { runtimeTimeoutMs: spec.status.readOnlyCapture.timeoutMs })); const sourcePayload = captureJsonPayload(sourceProbe.value); const branchTipCommit = stringOrNull(sourcePayload.remoteBranchCommit) ?? stringOrNull(sourcePayload.localHead); const initialSourceCommit = options.sourceCommit @@ -651,8 +650,8 @@ export async function statusYamlLane(config: UniDeskConfig, options: StatusOptio ?? stringOrNull(sourcePayload.localHead); const pipelineRunName = options.pipelineRun ?? (initialSourceCommit ? agentRunPipelineRunName(spec, initialSourceCommit) : null); const [runtimeProbe, mirrorProbe] = await Promise.all([ - timedStatusStage("runtime", () => capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneRuntimeStatusScript(spec, pipelineRunName)], { runtimeTimeoutMs: agentRunStatusCaptureTimeoutMs })), - timedStatusStage("git-mirror", () => capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneGitMirrorStatusScript(spec)], { runtimeTimeoutMs: agentRunStatusCaptureTimeoutMs })), + timedStatusStage("runtime", () => capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneRuntimeStatusScript(spec, pipelineRunName)], { runtimeTimeoutMs: spec.status.readOnlyCapture.timeoutMs })), + timedStatusStage("git-mirror", () => capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneGitMirrorStatusScript(spec)], { runtimeTimeoutMs: spec.status.readOnlyCapture.timeoutMs })), ]); const runtimePayload = captureJsonPayload(runtimeProbe.value); const mirrorPayload = captureJsonPayload(mirrorProbe.value); @@ -904,6 +903,7 @@ export async function statusYamlLane(config: UniDeskConfig, options: StatusOptio command: "agentrun control-plane status", mode: "yaml-declared-node-lane", configPath: target.configPath, + readCaptureBudget: spec.status.readOnlyCapture, target: options.full || options.raw ? agentRunLaneSummary(spec) : compactAgentRunLaneStatusTarget(spec), summary: options.full || options.raw ? detailedSummary : commanderSummary, alignment: { diff --git a/scripts/src/platform-infra-pac-failure-evidence.test.ts b/scripts/src/platform-infra-pac-failure-evidence.test.ts index bf2a89f5..0c4846be 100644 --- a/scripts/src/platform-infra-pac-failure-evidence.test.ts +++ b/scripts/src/platform-infra-pac-failure-evidence.test.ts @@ -64,6 +64,7 @@ describe("PaC 失败证据合同", () => { parsed: null, elapsedMs: 25001, timeoutMs: 25000, + configPath: "config/platform-infra/pipelines-as-code.yaml#observability.readOnlyCapture.timeoutMs", next: "bun scripts/cli.ts platform-infra pipelines-as-code history --target NC01 --consumer agentrun-nc01-v02 --limit 5", }); expect(observation).toMatchObject({ @@ -74,6 +75,7 @@ describe("PaC 失败证据合同", () => { stage: "history-remote-observe", elapsedMs: 25001, timeoutMs: 25000, + configPath: "config/platform-infra/pipelines-as-code.yaml#observability.readOnlyCapture.timeoutMs", exitCode: 124, firstBreak: { code: "pac-read-timeout", phase: "remote-read" }, }); @@ -81,6 +83,7 @@ describe("PaC 失败证据合同", () => { expect(output).toContain("READ OBSERVATION"); expect(output).toContain("history-remote-observe"); expect(output).toContain("pac-read-timeout"); + expect(output).toContain("config/platform-infra/pipelines-as-code.yaml#observability.readOnlyCapture.timeoutMs"); expect(output).toContain("next-read-only:"); }); diff --git a/scripts/src/platform-infra-pipelines-as-code.ts b/scripts/src/platform-infra-pipelines-as-code.ts index 8219e4a1..f889a6e5 100644 --- a/scripts/src/platform-infra-pipelines-as-code.ts +++ b/scripts/src/platform-infra-pipelines-as-code.ts @@ -52,7 +52,6 @@ const remoteScriptFile = rootPath("scripts", "src", "platform-infra-pipelines-as const evaluatorFile = rootPath("scripts", "native", "cicd", "pac-status-evaluator.cjs"); const sourceArtifactRuntimeObserverFile = rootPath("scripts", "native", "cicd", "pac-source-artifact-runtime.mjs"); const fieldManager = "unidesk-platform-infra-pipelines-as-code"; -const pacReadOnlyCaptureTimeoutMs = 25_000; const y = createYamlFieldReader(configLabel); function kubernetesLabelValue(value: string): string { @@ -111,6 +110,10 @@ export interface PacConfig { deliveryTiming: PacDeliveryTimingPolicy; observability: { configRef: string; + readOnlyCapture: { + timeoutMs: number; + configPath: string; + }; tracesEndpoint: string; serviceName: string; propagation: string[]; @@ -530,6 +533,7 @@ export function parsePacConfigDocument( const display = y.objectField(root, "display", ""); const deliveryTiming = y.objectField(root, "deliveryTiming", ""); const observability = y.objectField(root, "observability", ""); + const readOnlyCapture = y.objectField(observability, "readOnlyCapture", "observability"); const observabilitySampling = y.objectField(observability, "sampling", "observability"); const exporterFailure = y.objectField(observability, "exporterFailure", "observability"); const admin = y.objectField(gitea, "admin", "gitea"); @@ -569,6 +573,10 @@ export function parsePacConfigDocument( }, observability: { configRef: y.stringField(observability, "configRef", "observability"), + readOnlyCapture: { + timeoutMs: integerInRange(readOnlyCapture.timeoutMs, 1_000, 120_000, "observability.readOnlyCapture.timeoutMs"), + configPath: `${configLabel}#observability.readOnlyCapture.timeoutMs`, + }, tracesEndpoint: urlField(observability, "tracesEndpoint", "observability"), serviceName: y.stringField(observability, "serviceName", "observability"), propagation: y.stringArrayField(observability, "propagation", "observability"), @@ -1371,7 +1379,7 @@ async function status(config: UniDeskConfig, options: CommonOptions): Promise>; readonly parsed: Record | null; readonly observation: PacReadOnlyCaptureObservation }> { const stage = `${action}-remote-observe`; const startedAt = Date.now(); - printPacReadOnlyProgress(target, consumer, stage, "started", startedAt, { timeoutMs: pacReadOnlyCaptureTimeoutMs }); - const result = await capture(config, target.route, ["sh"], script, { runtimeTimeoutMs: pacReadOnlyCaptureTimeoutMs }); + const captureBudget = pac.observability.readOnlyCapture; + printPacReadOnlyProgress(target, consumer, stage, "started", startedAt, captureBudget); + const result = await capture(config, target.route, ["sh"], script, { runtimeTimeoutMs: captureBudget.timeoutMs }); const parsed = parseJsonOutput(result.stdout); const observation = pacReadOnlyCaptureObservation({ action, @@ -1765,7 +1775,8 @@ async function capturePacReadOnly( result, parsed, elapsedMs: Date.now() - startedAt, - timeoutMs: pacReadOnlyCaptureTimeoutMs, + timeoutMs: captureBudget.timeoutMs, + configPath: captureBudget.configPath, next, }); printPacReadOnlyProgress(target, consumer, stage, observation.status, startedAt, { @@ -1783,6 +1794,7 @@ export function pacReadOnlyCaptureObservation(options: { readonly parsed: Record | null; readonly elapsedMs: number; readonly timeoutMs: number; + readonly configPath: string; readonly next: string; }): PacReadOnlyCaptureObservation { const stdoutBytes = Buffer.byteLength(options.result.stdout, "utf8"); @@ -1806,6 +1818,7 @@ export function pacReadOnlyCaptureObservation(options: { stage: `${options.action}-remote-observe`, elapsedMs: options.elapsedMs, timeoutMs: options.timeoutMs, + configPath: options.configPath, exitCode: options.result.exitCode, stdoutBytes, stderrBytes, @@ -2452,6 +2465,7 @@ function compactStatusJson(result: Record): Record, includeTaskD consumers: result.consumers, detailId: result.detailId, deliveryBudget: result.deliveryBudget, + observation: result.observation, rows: arrayRecords(result.rows).map((row) => { const taskRuns = record(row.taskRuns); const artifact = record(row.artifact); @@ -3125,6 +3140,7 @@ function renderPacReadObservation(result: Record): string[] { boolText(observation.partial), stringValue(firstBreak.code), ]]), + ` budget-config: ${stringValue(observation.configPath)}`, ` next-read-only: ${stringValue(observation.next)}`, ]; } From b82b4ecfc5b30b47614e4a81bdb52be5e3d5995b Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 15 Jul 2026 11:50:25 +0200 Subject: [PATCH 4/7] =?UTF-8?q?fix:=20=E5=AE=8C=E6=88=90=E5=8F=AA=E8=AF=BB?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E9=A2=84=E7=AE=97=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/agentrun.yaml | 3 ++ .../R8_Task_Report.md | 37 ++++++++++++++----- scripts/src/agentrun/control-plane.ts | 2 + scripts/src/agentrun/public-exposure.ts | 14 +++++++ ...latform-infra-pac-failure-evidence.test.ts | 36 ++++++++++++++++++ .../src/platform-infra-pipelines-as-code.ts | 1 + 6 files changed, 84 insertions(+), 9 deletions(-) diff --git a/config/agentrun.yaml b/config/agentrun.yaml index 944b1089..68195dbf 100644 --- a/config/agentrun.yaml +++ b/config/agentrun.yaml @@ -500,6 +500,9 @@ controlPlane: gitopsPath: deploy/gitops/node/nc01/runtime-v02 argoApplication: agentrun-nc01-v02 nc01-release: + status: + readOnlyCapture: + timeoutMs: 20000 deployment: format: unidesk-yaml-only gitopsRoot: deploy/gitops/node/nc01 diff --git a/docs/MDTODO/details/pr-merge-driven-automatic-delivery/R8_Task_Report.md b/docs/MDTODO/details/pr-merge-driven-automatic-delivery/R8_Task_Report.md index 6545ff78..9f72d009 100644 --- a/docs/MDTODO/details/pr-merge-driven-automatic-delivery/R8_Task_Report.md +++ b/docs/MDTODO/details/pr-merge-driven-automatic-delivery/R8_Task_Report.md @@ -15,6 +15,8 @@ - 首轮修复已建立稳定的 typed observation,使 PaC `status/history/debug-step` 与 AgentRun `control-plane status` 在 timeout、remote failure、empty response 和 parse failure 时不再以空 stdout 结束。 - 首轮仍把 PaC 只读 capture 预算 `25000ms` 与 AgentRun status capture 预算 `20000ms` 写成 TypeScript 常量。 - 这些预算属于可调运维事实;源码常量绕过 owning YAML,无法披露来源路径,也无法在选中对象缺失预算时 fail-closed。 +- `18d0bd89` 已把预算迁入 YAML,但只给默认 `nc01-v02` lane 声明 AgentRun 预算;parser 会校验 `controlPlane.lanes` 的全部对象,因此 `nc01-release` 缺失 `status.readOnlyCapture` 时,`agentrun control-plane status` 在任何目标选择前即失败。 +- AgentRun 默认文本只显示嵌套 PaC timeout,未披露选中 lane 的 budget path;PaC observation 实际返回 `configPath`,其 TypeScript 接口却漏声明该字段。 ## 官方最佳实践 @@ -25,19 +27,20 @@ ## Current Control Flow 1. CLI 读取 PaC 或 AgentRun YAML。 -2. parser 解析目标、consumer 或 lane。 -3. capture 调用使用 TypeScript 文件内固定的 timeout 常量。 -4. typed observation 披露实际 timeout 数值,但无法证明该数值的 owning config path。 -5. YAML 缺失预算时仍可继续执行,不能按当前选中对象 fail-closed。 +2. parser 解析目标、consumer 或全部 lane。 +3. `18d0bd89` 后 capture 已使用 YAML timeout,但 AgentRun 只有一个 lane 声明预算,导致 parser 在发起只读 capture 前失败。 +4. PaC text/JSON 已披露 timeout 与 config path;AgentRun JSON 有 `readCaptureBudget`,默认文本未披露该字段。 +5. 嵌套 PaC 与 runtime capture 串行执行;当前有效预算为 `25000ms + 20000ms`,需要用原入口实测其是否与外层 `trans` 60 秒短连接竞争。 ## Desired Control Flow 1. PaC parser 必填读取 `config/platform-infra/pipelines-as-code.yaml#observability.readOnlyCapture.timeoutMs`。 -2. AgentRun lane parser 必填读取 `config/agentrun.yaml#controlPlane.lanes..status.readOnlyCapture.timeoutMs`。 +2. AgentRun 每个 owning lane 都必须声明 `config/agentrun.yaml#controlPlane.lanes..status.readOnlyCapture.timeoutMs`,parser 对全部 lane 必填校验且不补默认值。 3. parser 将预算值与精确 config path 放入 typed spec,不提供源码默认值。 4. capture 只使用已验证的 YAML 预算。 5. PaC text/JSON observation 与 AgentRun status text/JSON 摘要同时披露 config path 和实际 timeout。 6. 缺失字段时 parser 在发起远端读取前失败,并指出缺失 YAML 路径。 +7. status JSON 显式声明 `mutation: false`;失败保持非空 typed 结果,不触发 PipelineRun、Argo、mirror 或 runtime 写操作。 ## 修改 @@ -49,22 +52,38 @@ - `status/history/debug-step` capture 使用 YAML 预算。 - typed observation 与默认 text 输出披露预算路径和实际数值。 - `config/agentrun.yaml` - - 在 `controlPlane.lanes.nc01-v02.status.readOnlyCapture.timeoutMs` 声明 `20000`。 + - 在 `controlPlane.lanes.nc01-v02.status.readOnlyCapture.timeoutMs` 与 `controlPlane.lanes.nc01-release.status.readOnlyCapture.timeoutMs` 分别声明 `20000`,保证所有 owning lane 通过必填 parser。 - `scripts/src/agentrun-lanes.ts` - lane parser 必填解析 status capture 预算与精确 config path。 - `scripts/src/agentrun/control-plane.ts` - 删除 `agentRunStatusCaptureTimeoutMs` 源码常量。 - source、runtime 和 git-mirror 只读 capture 使用选中 lane 的 YAML 预算。 - status 结果披露 `readCaptureBudget`。 + - status 结果显式披露 `mutation: false`。 +- `scripts/src/agentrun/public-exposure.ts` + - 默认文本新增 `AGENTRUN READ CAPTURE BUDGET`,披露选中 lane 的实际 timeout 与 config path。 + - 嵌套 `PAC READ OBSERVATION` 同时披露 PaC budget config path。 +- `scripts/src/platform-infra-pipelines-as-code.ts` + - 补齐 `PacReadOnlyCaptureObservation.configPath` 类型声明,使 typed JSON 与 TypeScript 合同一致。 ## 验证证据 -- 定向 Bun 测试验证 PaC typed observation 保留 timeout、config path、首断点与只读 Next。 -- TypeScript 语法检查覆盖修改的 parser、PaC 与 AgentRun control-plane 文件。 -- YAML 读取和原 CLI 入口验证证明预算由选中配置解析,并在 text/JSON 中可见。 +- 定向 Bun 测试:`7 pass / 0 fail`,覆盖 PaC typed observation 与 AgentRun text 两层 YAML budget path。 +- `bun --check` 覆盖修改的 parser、PaC、AgentRun control-plane、renderer 与 capture helper。 +- PaC status JSON:约 `28s` 返回 `status=timeout`、`timeoutMs=25000`、PaC config path、`mutation=false`,exit `1`,stdout 非空。 +- PaC history text:约 `29s` 返回 `pac-read-timeout`、PaC config path 与只读 Next,exit `1`,stdout 非空。 +- PaC debug-step text:约 `3s` 成功,`34/34` fixtures,通过同一 YAML budget path,exit `0`。 +- AgentRun status text:内部 PaC `25609ms`、runtime `20150ms`、total `45759ms`;外层 `trans` 总耗时约 `47.273s`,在 60 秒短连接预算内稳定返回,披露 PaC `25000ms` 与 lane `20000ms` 两层 config path,exit `1`,stdout 非空。 +- AgentRun `--raw` JSON:约 `45.242s` 返回;完整 JSON 的 `readCaptureBudget` 与 `summary.pac.readObservation` 分别披露 AgentRun/PaC config path,且 status 结果声明 `mutation=false`。 - `git diff --check` 验证补丁格式。 - 所有目标源码、Git、验证和原入口观察均通过 `trans NC01:/root/unidesk/.worktree/2165-pac-status-visible ...` 执行。 +## 取消与超时结论 + +- 上次任务取消不能归因于当前 CLI 嵌套预算:修正 YAML 完整性后,最慢 AgentRun 原入口内部总耗时约 `45.759s`,外层 `trans` 约 `47.273s`,未触发 60 秒短连接 timeout。 +- 当前链路确实串行消费 PaC `25000ms` 与 runtime `20000ms`,但仍保留约 14 秒外层余量;本轮没有证据支持新增 job、锁、租约、第二状态库或额外 authority。 +- 取消更符合 Artificer/session 外部终止,而不是 CLI 预算嵌套 first break;本报告只记录可复现证据,不猜测未观测的调度原因。 + ## 权限与变更边界 - 保留现有 typed observation、即时 progress、只读 Next 和非 mutation 行为。 diff --git a/scripts/src/agentrun/control-plane.ts b/scripts/src/agentrun/control-plane.ts index 02770ef8..01f21fe8 100644 --- a/scripts/src/agentrun/control-plane.ts +++ b/scripts/src/agentrun/control-plane.ts @@ -527,6 +527,7 @@ export async function statusPipelinesAsCodeLane(config: UniDeskConfig, options: ok: runtimeProbe.value.exitCode === 0 && pacStatus.ok === true && blockers.length === 0, command: "agentrun control-plane status", mode: "pipelines-as-code-migrated-lane", + mutation: false, configPath: target.configPath, readCaptureBudget: spec.status.readOnlyCapture, target: options.full || options.raw ? agentRunLaneSummary(spec) : compactAgentRunLaneStatusTarget(spec), @@ -902,6 +903,7 @@ export async function statusYamlLane(config: UniDeskConfig, options: StatusOptio ok: sourceProbe.value.exitCode === 0 && runtimeProbe.value.exitCode === 0 && mirrorProbe.value.exitCode === 0 && blockers.length === 0, command: "agentrun control-plane status", mode: "yaml-declared-node-lane", + mutation: false, configPath: target.configPath, readCaptureBudget: spec.status.readOnlyCapture, target: options.full || options.raw ? agentRunLaneSummary(spec) : compactAgentRunLaneStatusTarget(spec), diff --git a/scripts/src/agentrun/public-exposure.ts b/scripts/src/agentrun/public-exposure.ts index 2a203f1b..8877bfad 100644 --- a/scripts/src/agentrun/public-exposure.ts +++ b/scripts/src/agentrun/public-exposure.ts @@ -75,6 +75,7 @@ export function renderAgentRunControlPlaneStatusSummary(result: Record `- ${warning}`)].join("\n"), "", "NEXT", @@ -176,6 +184,12 @@ export function renderAgentRunControlPlaneStatusSummary(result: Record `- ${warning}`)].join("\n"), "", "NEXT", diff --git a/scripts/src/platform-infra-pac-failure-evidence.test.ts b/scripts/src/platform-infra-pac-failure-evidence.test.ts index 0c4846be..d877dc70 100644 --- a/scripts/src/platform-infra-pac-failure-evidence.test.ts +++ b/scripts/src/platform-infra-pac-failure-evidence.test.ts @@ -5,6 +5,7 @@ import { resolve } from "node:path"; import { createRequire } from "node:module"; import { sentinelPublishImageBuildShell } from "./hwlab-node-web-sentinel-cicd-jobs"; import type { SentinelCicdState } from "./hwlab-node-web-sentinel-cicd-shared"; +import { renderAgentRunControlPlaneStatusSummary } from "./agentrun/public-exposure"; import { compactHistoryJson, compactStatusSummary, pacReadOnlyCaptureObservation, renderDebugStep, renderHistory, renderStatus } from "./platform-infra-pipelines-as-code"; const require = createRequire(import.meta.url); @@ -87,6 +88,41 @@ describe("PaC 失败证据合同", () => { expect(output).toContain("next-read-only:"); }); + test("AgentRun status 文本披露 lane 与 PaC 两层 YAML capture 预算", () => { + const output = renderAgentRunControlPlaneStatusSummary({ + ok: false, + target: { node: { id: "NC01" }, lane: "nc01-v02" }, + summary: { + migration: { migrated: true, sourceAuthority: "gitea-pipelines-as-code" }, + pac: { + readObservation: { + stage: "status-remote-observe", + status: "timeout", + elapsedMs: 25001, + timeoutMs: 25000, + configPath: "config/platform-infra/pipelines-as-code.yaml#observability.readOnlyCapture.timeoutMs", + exitCode: 124, + firstBreak: { code: "pac-read-timeout" }, + next: "bun scripts/cli.ts platform-infra pipelines-as-code status --full", + }, + }, + blockers: ["pac-not-ready"], + warnings: [], + nextAction: {}, + }, + readCaptureBudget: { + timeoutMs: 20000, + configPath: "config/agentrun.yaml.controlPlane.lanes.nc01-v02.status.readOnlyCapture.timeoutMs", + }, + next: {}, + timings: {}, + }).renderedText; + expect(output).toContain("PAC READ OBSERVATION"); + expect(output).toContain("config/platform-infra/pipelines-as-code.yaml#observability.readOnlyCapture.timeoutMs"); + expect(output).toContain("AGENTRUN READ CAPTURE BUDGET"); + expect(output).toContain("config/agentrun.yaml.controlPlane.lanes.nc01-v02.status.readOnlyCapture.timeoutMs"); + }); + test("feature-config warning 在 status、history、debug-step 同构有界投影", () => { const observation = extractPacSourceObservation([ { diff --git a/scripts/src/platform-infra-pipelines-as-code.ts b/scripts/src/platform-infra-pipelines-as-code.ts index f889a6e5..612835b4 100644 --- a/scripts/src/platform-infra-pipelines-as-code.ts +++ b/scripts/src/platform-infra-pipelines-as-code.ts @@ -307,6 +307,7 @@ interface PacReadOnlyCaptureObservation { readonly stage: string; readonly elapsedMs: number; readonly timeoutMs: number; + readonly configPath: string; readonly exitCode: number; readonly stdoutBytes: number; readonly stderrBytes: number; From cd2affa9a1f9590c60bea58f6d4ab4295313ec10 Mon Sep 17 00:00:00 2001 From: Lyon <88232613+pikasTech@users.noreply.github.com> Date: Wed, 15 Jul 2026 18:50:15 +0800 Subject: [PATCH 5/7] =?UTF-8?q?feat:=20=E9=85=8D=E7=BD=AE=E5=BC=80?= =?UTF-8?q?=E5=8F=91=E9=9B=86=E7=BE=A4=20CosyVoice3=20=E7=AB=AF=E7=82=B9?= =?UTF-8?q?=20(#2194)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Codex --- config/selfmedia.yaml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/config/selfmedia.yaml b/config/selfmedia.yaml index c1d32448..8a62a07f 100644 --- a/config/selfmedia.yaml +++ b/config/selfmedia.yaml @@ -95,6 +95,8 @@ delivery: replicas: 1 serviceAccountName: selfmedia imagePullPolicy: IfNotPresent + environment: + SELFMEDIA_TTS_BASE_URL: http://152.53.229.148:24317 command: - /usr/bin/tini - -- @@ -234,6 +236,10 @@ delivery: - 192.168.0.0/16 - 169.254.169.254/32 - 152.53.229.148/32 + allowedTcpEndpoints: + - cidr: 152.53.229.148/32 + ports: + - 24317 codex: package: "@openai/codex" version: 0.144.1 @@ -281,6 +287,8 @@ delivery: namespace: selfmedia-production runtime: serviceAccountName: selfmedia-production + environment: + SELFMEDIA_TTS_BASE_URL: "" service: name: selfmedia-production publicExposure: @@ -306,6 +314,8 @@ delivery: secretName: selfmedia-production-codex codex: supervisorStatePath: /home/codex/.codex/state/selfmedia-production-supervisor.json + networkPolicy: + allowedTcpEndpoints: [] cutover: targets: NC01: From 0b82cfd146baff6df8259efb3391052ade88eefe Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 15 Jul 2026 12:51:50 +0200 Subject: [PATCH 6/7] =?UTF-8?q?fix:=20=E7=A7=BB=E9=99=A4=E5=8F=AA=E8=AF=BB?= =?UTF-8?q?=E9=A2=84=E7=AE=97=E4=BB=A3=E7=A0=81=E4=B8=8A=E9=99=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pr-merge-driven-automatic-delivery/R8_Task_Report.md | 7 ++++++- scripts/src/platform-infra-pipelines-as-code.ts | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/MDTODO/details/pr-merge-driven-automatic-delivery/R8_Task_Report.md b/docs/MDTODO/details/pr-merge-driven-automatic-delivery/R8_Task_Report.md index 9f72d009..186f6179 100644 --- a/docs/MDTODO/details/pr-merge-driven-automatic-delivery/R8_Task_Report.md +++ b/docs/MDTODO/details/pr-merge-driven-automatic-delivery/R8_Task_Report.md @@ -17,6 +17,8 @@ - 这些预算属于可调运维事实;源码常量绕过 owning YAML,无法披露来源路径,也无法在选中对象缺失预算时 fail-closed。 - `18d0bd89` 已把预算迁入 YAML,但只给默认 `nc01-v02` lane 声明 AgentRun 预算;parser 会校验 `controlPlane.lanes` 的全部对象,因此 `nc01-release` 缺失 `status.readOnlyCapture` 时,`agentrun control-plane status` 在任何目标选择前即失败。 - AgentRun 默认文本只显示嵌套 PaC timeout,未披露选中 lane 的 budget path;PaC observation 实际返回 `configPath`,其 TypeScript 接口却漏声明该字段。 +- `b82b4ecf` 虽已完成 YAML 预算迁移和原入口复测,但 PaC parser 仍用 `integerInRange(..., 1000, 120000)` 限制 `observability.readOnlyCapture.timeoutMs`。 +- 该 `120000ms` 上限仍是源码中的运维策略,会拒绝 owning YAML 明确声明的更大正整数预算,因此 YAML 尚未成为完整 authority。 ## 官方最佳实践 @@ -36,7 +38,7 @@ 1. PaC parser 必填读取 `config/platform-infra/pipelines-as-code.yaml#observability.readOnlyCapture.timeoutMs`。 2. AgentRun 每个 owning lane 都必须声明 `config/agentrun.yaml#controlPlane.lanes..status.readOnlyCapture.timeoutMs`,parser 对全部 lane 必填校验且不补默认值。 -3. parser 将预算值与精确 config path 放入 typed spec,不提供源码默认值。 +3. parser 只校验预算字段结构和正整数类型,将预算值与精确 config path 放入 typed spec,不提供源码默认值或上限。 4. capture 只使用已验证的 YAML 预算。 5. PaC text/JSON observation 与 AgentRun status text/JSON 摘要同时披露 config path 和实际 timeout。 6. 缺失字段时 parser 在发起远端读取前失败,并指出缺失 YAML 路径。 @@ -65,6 +67,7 @@ - 嵌套 `PAC READ OBSERVATION` 同时披露 PaC budget config path。 - `scripts/src/platform-infra-pipelines-as-code.ts` - 补齐 `PacReadOnlyCaptureObservation.configPath` 类型声明,使 typed JSON 与 TypeScript 合同一致。 + - 删除 `observability.readOnlyCapture.timeoutMs` 的 `120000ms` 代码上限,只保留 owning YAML 字段的正整数与结构校验。 ## 验证证据 @@ -76,6 +79,8 @@ - AgentRun status text:内部 PaC `25609ms`、runtime `20150ms`、total `45759ms`;外层 `trans` 总耗时约 `47.273s`,在 60 秒短连接预算内稳定返回,披露 PaC `25000ms` 与 lane `20000ms` 两层 config path,exit `1`,stdout 非空。 - AgentRun `--raw` JSON:约 `45.242s` 返回;完整 JSON 的 `readCaptureBudget` 与 `summary.pac.readObservation` 分别披露 AgentRun/PaC config path,且 status 结果声明 `mutation=false`。 - `git diff --check` 验证补丁格式。 +- 不落盘 parser 探针确认 `120001ms` 被接受,`0` 仍以 `must be positive` 拒绝,证明预算上限完全由 owning YAML 决定。 +- 复核 PaC `status/history/debug-step` typed observation 与 AgentRun control-plane/RunnerJob 观察路径:timeout、remote failure、empty response、parse failure 和对象缺失均保留有界摘要、elapsed/first break、只读 Next 或对象存在性证据,结果继续声明 `mutation=false`。 - 所有目标源码、Git、验证和原入口观察均通过 `trans NC01:/root/unidesk/.worktree/2165-pac-status-visible ...` 执行。 ## 取消与超时结论 diff --git a/scripts/src/platform-infra-pipelines-as-code.ts b/scripts/src/platform-infra-pipelines-as-code.ts index 612835b4..44f97944 100644 --- a/scripts/src/platform-infra-pipelines-as-code.ts +++ b/scripts/src/platform-infra-pipelines-as-code.ts @@ -575,7 +575,7 @@ export function parsePacConfigDocument( observability: { configRef: y.stringField(observability, "configRef", "observability"), readOnlyCapture: { - timeoutMs: integerInRange(readOnlyCapture.timeoutMs, 1_000, 120_000, "observability.readOnlyCapture.timeoutMs"), + timeoutMs: positiveInteger(readOnlyCapture, "timeoutMs", "observability.readOnlyCapture"), configPath: `${configLabel}#observability.readOnlyCapture.timeoutMs`, }, tracesEndpoint: urlField(observability, "tracesEndpoint", "observability"), From a246251442d60dc1c7cad9e17e64e4c127376516 Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 15 Jul 2026 12:56:04 +0200 Subject: [PATCH 7/7] =?UTF-8?q?docs:=20=E5=AE=8C=E6=88=90=20PaC=20?= =?UTF-8?q?=E5=8F=AF=E8=A7=81=E6=80=A7=20R8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/MDTODO/pr-merge-driven-automatic-delivery.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/MDTODO/pr-merge-driven-automatic-delivery.md b/docs/MDTODO/pr-merge-driven-automatic-delivery.md index a54c1526..caed2362 100644 --- a/docs/MDTODO/pr-merge-driven-automatic-delivery.md +++ b/docs/MDTODO/pr-merge-driven-automatic-delivery.md @@ -110,6 +110,6 @@ 建立 [UniDesk #2010](https://github.com/pikasTech/unidesk/issues/2010):跟随 release branch 的独立 production PaC/Tekton/GitOps/Argo、namespace、host PostgreSQL database/role/Secret/migration ledger、Kafka consumer group 与 https://hwlab.pikapython.com YAML publicExposure;release 只从完成 P0 Kafka 纠偏并验证的 v0.3 commit 初始化,完成任务后将详细报告写入[任务报告](./details/pr-merge-driven-automatic-delivery/R7.2_Task_Report.md)。 -## R8 [in_progress] +## R8 [completed] 解决 [UniDesk #2165](https://github.com/pikasTech/unidesk/issues/2165):修复 PaC `status/history/debug-step` 与 AgentRun control-plane 状态命令在远端 timeout、空 payload、解析失败或异常时以空 stdout/不明 exit 结束的问题;保持目标 Kubernetes/Tekton/Argo/runtime 为唯一状态 authority,所有成功、partial、timeout 和 failure 都输出有界 typed 摘要、耗时、first break 与只读下钻,禁止第二状态源、无界 dump、交付 mutation、PK01 修改、合同/租约/围栏和 Vitest,完成任务后将详细报告写入[任务报告](./details/pr-merge-driven-automatic-delivery/R8_Task_Report.md)。