fix: 完成只读状态预算验证

This commit is contained in:
Codex
2026-07-15 11:50:25 +02:00
parent 18d0bd89b2
commit b82b4ecfc5
6 changed files with 84 additions and 9 deletions
+3
View File
@@ -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
@@ -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 pathPaC 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 pathAgentRun 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.<lane>.status.readOnlyCapture.timeoutMs`
2. AgentRun 每个 owning lane 都必须声明 `config/agentrun.yaml#controlPlane.lanes.<lane>.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、PaCAgentRun control-plane 文件
- YAML 读取和原 CLI 入口验证证明预算由选中配置解析,并在 text/JSON 中可见
- 定向 Bun 测试`7 pass / 0 fail`,覆盖 PaC typed observation 与 AgentRun text 两层 YAML budget path
- `bun --check` 覆盖修改的 parser、PaCAgentRun 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 与只读 Nextexit `1`stdout 非空。
- PaC debug-step text:约 `3s` 成功,`34/34` fixtures,通过同一 YAML budget pathexit `0`
- AgentRun status text:内部 PaC `25609ms`、runtime `20150ms`、total `45759ms`;外层 `trans` 总耗时约 `47.273s`,在 60 秒短连接预算内稳定返回,披露 PaC `25000ms` 与 lane `20000ms` 两层 config pathexit `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 行为。
+2
View File
@@ -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),
+14
View File
@@ -75,6 +75,7 @@ export function renderAgentRunControlPlaneStatusSummary(result: Record<string, u
const next = record(result.next);
const disclosure = record(result.disclosure);
const timings = record(result.timings);
const readCaptureBudget = record(result.readCaptureBudget);
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);
@@ -130,8 +131,15 @@ export function renderAgentRunControlPlaneStatusSummary(result: Record<string, u
displayValue(pacRead.exitCode),
displayValue(pacFirstBreak.code),
]]),
` budget-config: ${displayValue(pacRead.configPath)}`,
` next-read-only: ${displayValue(pacRead.next)}`,
"",
"AGENTRUN READ CAPTURE BUDGET",
renderTable(["TIMEOUT_MS", "CONFIG_PATH"], [[
displayValue(readCaptureBudget.timeoutMs),
displayValue(readCaptureBudget.configPath),
]]),
"",
warnings.length === 0 ? "WARNINGS\n-" : ["WARNINGS", ...warnings.map((warning) => `- ${warning}`)].join("\n"),
"",
"NEXT",
@@ -176,6 +184,12 @@ export function renderAgentRunControlPlaneStatusSummary(result: Record<string, u
],
),
"",
"AGENTRUN READ CAPTURE BUDGET",
renderTable(["TIMEOUT_MS", "CONFIG_PATH"], [[
displayValue(readCaptureBudget.timeoutMs),
displayValue(readCaptureBudget.configPath),
]]),
"",
warnings.length === 0 ? "WARNINGS\n-" : ["WARNINGS", ...warnings.map((warning) => `- ${warning}`)].join("\n"),
"",
"NEXT",
@@ -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([
{
@@ -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;