// SPEC: PJ2026-01060508 Web哨兵 draft-2026-06-25-p0-web-probe-sentinel. // Responsibility: YAML-first CI/CD, image, GitOps and Argo command plan for the web-probe sentinel. import { createHash, randomUUID } from "node:crypto"; import { existsSync, readFileSync } from "node:fs"; import { join } from "node:path"; import { repoRoot, rootPath } from "./config"; import { runCommand, type CommandResult } from "./command"; import { startJob } from "./jobs"; import { webProbeSentinelConfigPlan, withWebProbeSentinelConfigRendered } from "./hwlab-node-web-sentinel-config"; import type { HwlabRuntimeLaneSpec } from "./hwlab-node-lanes"; import type { RenderedCliResult } from "./output"; export type WebProbeSentinelConfigAction = "plan" | "status"; export type WebProbeSentinelImageAction = "status" | "build"; export type WebProbeSentinelControlPlaneAction = "plan" | "apply" | "status" | "trigger-current"; export type WebProbeSentinelMaintenanceAction = "status" | "start" | "stop"; export type WebProbeSentinelReportView = "summary" | "turn-summary" | "findings" | "trace-frame"; export type WebProbeSentinelOptions = | { readonly kind: "config"; readonly action: WebProbeSentinelConfigAction; readonly node: string; readonly lane: string; readonly dryRun: boolean; } | { readonly kind: "image"; readonly action: WebProbeSentinelImageAction; readonly node: string; readonly lane: string; readonly dryRun: boolean; readonly confirm: boolean; readonly wait: boolean; readonly timeoutSeconds: number; } | { readonly kind: "control-plane"; readonly action: WebProbeSentinelControlPlaneAction; readonly node: string; readonly lane: string; readonly dryRun: boolean; readonly confirm: boolean; readonly wait: boolean; readonly timeoutSeconds: number; } | { readonly kind: "maintenance"; readonly action: WebProbeSentinelMaintenanceAction; readonly node: string; readonly lane: string; readonly dryRun: boolean; readonly confirm: boolean; readonly wait: boolean; readonly timeoutSeconds: number; readonly releaseId: string | null; readonly reason: string | null; readonly quickVerify: boolean; } | { readonly kind: "validate"; readonly action: "validate"; readonly node: string; readonly lane: string; readonly dryRun: boolean; readonly confirm: boolean; readonly wait: boolean; readonly timeoutSeconds: number; readonly quickVerify: boolean; } | { readonly kind: "report"; readonly action: "report"; readonly node: string; readonly lane: string; readonly view: WebProbeSentinelReportView; readonly runId: string | null; readonly latest: boolean; readonly traceId: string | null; readonly sampleSeq: number | null; readonly raw: boolean; readonly timeoutSeconds: number; }; interface SentinelCicdState { readonly spec: HwlabRuntimeLaneSpec; readonly configReady: boolean; readonly runtime: Record; readonly cicd: Record; readonly publicExposure: Record; readonly controlPlaneTarget: Record; readonly controlPlaneNode: Record; readonly sourceHead: SourceHead; readonly image: SentinelImagePlan; readonly manifests: readonly Record[]; readonly manifestSha256: string; readonly valuesRedacted: true; } interface SourceHead { readonly ok: boolean; readonly repository: string; readonly branch: string; readonly commit: string | null; readonly localHead: string | null; readonly result: CompactCommandResult; } interface SentinelImagePlan { readonly repository: string; readonly tag: string; readonly ref: string; readonly digestRef: string | null; readonly baseImage: string; readonly buildContext: string; readonly entrypoint: string; readonly dockerfileSha256: string; readonly dockerfilePreview: string; } interface SentinelObservedStatus { readonly sourceMirror: Record; readonly registry: Record; readonly gitMirror: Record; readonly gitops: Record; readonly argo: Record; readonly runtime: Record; } interface SentinelObservedExpectation { readonly gitopsRevision: string | null; readonly runtimeImage: string | null; } interface SentinelRemoteJobResult { readonly ok: boolean; readonly phase: string; readonly jobName: string; readonly payload: Record; readonly polls?: number; readonly elapsedMs?: number; readonly create?: Record; readonly probe?: Record; readonly valuesRedacted: true; } interface CompactCommandResult { readonly exitCode: number | null; readonly timedOut: boolean; readonly stdoutBytes: number; readonly stderrBytes: number; readonly stdoutPreview: string; readonly stderrPreview: string; } interface ChildCliResult { readonly ok: boolean; readonly parsed: Record | null; readonly result: CompactCommandResult & { stdoutTail: string; stderrTail: string }; } const SPEC_REF = "PJ2026-01060508 Web哨兵 draft-2026-06-25-p0-web-probe-sentinel"; export function runWebProbeSentinelCommand(spec: HwlabRuntimeLaneSpec, options: WebProbeSentinelOptions): RenderedCliResult { if (options.kind === "config") return withWebProbeSentinelConfigRendered(webProbeSentinelConfigPlan(spec, options.action)); const state = loadSentinelCicdState(spec, options.timeoutSeconds); if (options.kind === "image") return runSentinelImage(state, options); if (options.kind === "control-plane") return runSentinelControlPlane(state, options); if (options.kind === "maintenance") return runSentinelMaintenance(state, options); if (options.kind === "validate") return runSentinelValidate(state, options); return runSentinelReport(state, options); } function runSentinelImage(state: SentinelCicdState, options: Extract): RenderedCliResult { const command = `web-probe sentinel image ${options.action}`; if (options.action === "build" && options.confirm) { if (!options.wait) return renderAsyncSentinelJob(state, "image", "build", options.timeoutSeconds); return runSentinelImageBuildConfirmed(state, options); } const registry = options.action === "status" ? probeImageRegistry(state, options.timeoutSeconds) : null; const registryReady = options.action !== "status" || record(registry?.probe).present === true; const result = { ok: state.configReady && state.sourceHead.ok && registryReady, command, node: state.spec.nodeId, lane: state.spec.lane, mode: options.action === "status" ? "status" : options.confirm ? "confirm" : "dry-run", mutation: false, specRef: SPEC_REF, source: state.sourceHead, image: state.image, registry, blocker: null, next: { status: `bun scripts/cli.ts web-probe sentinel image status --node ${state.spec.nodeId} --lane ${state.spec.lane}`, dryRun: `bun scripts/cli.ts web-probe sentinel image build --node ${state.spec.nodeId} --lane ${state.spec.lane} --dry-run`, confirm: `bun scripts/cli.ts web-probe sentinel image build --node ${state.spec.nodeId} --lane ${state.spec.lane} --confirm`, controlPlanePlan: `bun scripts/cli.ts web-probe sentinel control-plane plan --node ${state.spec.nodeId} --lane ${state.spec.lane} --dry-run`, }, valuesRedacted: true, }; return rendered(result.ok, command, renderImageResult(result)); } function runSentinelControlPlane(state: SentinelCicdState, options: Extract): RenderedCliResult { const command = `web-probe sentinel control-plane ${options.action}`; const mutationAction = options.action === "apply" || options.action === "trigger-current"; if (options.confirm && mutationAction) { if (!options.wait) return renderAsyncSentinelJob(state, "control-plane", options.action, options.timeoutSeconds); return runSentinelControlPlaneConfirmed(state, options); } const observed = options.action === "status" ? collectSentinelObservedStatus(state, options.timeoutSeconds) : null; const observedReady = options.action !== "status" || sentinelObservedReady(record(observed)); const pipelineRun = sentinelPipelineRunName(state); const result = { ok: state.configReady && state.sourceHead.ok && observedReady, command, node: state.spec.nodeId, lane: state.spec.lane, mode: options.action === "status" ? "status" : options.confirm ? "confirm" : "dry-run", mutation: false, specRef: SPEC_REF, source: state.sourceHead, image: state.image, pipelineRun, gitops: { path: stringField(state.cicd, "gitopsPath"), targetRevision: stringAt(state.cicd, "argo.targetRevision"), manifestObjects: state.manifests.length, manifestSha256: state.manifestSha256, }, argo: { namespace: stringAt(state.cicd, "argo.namespace"), projectName: stringAt(state.cicd, "argo.projectName"), applicationName: stringAt(state.cicd, "argo.applicationName"), }, maintenance: { startCommand: stringAt(state.cicd, "maintenance.startCommand"), stopCommand: stringAt(state.cicd, "maintenance.stopCommand"), serviceUnavailablePolicy: stringAt(state.cicd, "targetValidation.serviceUnavailablePolicy"), }, validation: { scenarioId: stringAt(state.cicd, "targetValidation.scenarioId"), maxSeconds: numberAt(state.cicd, "targetValidation.maxSeconds"), automaticSecondPath: false, }, manifests: { objects: manifestObjectSummary(state.manifests), sha256: state.manifestSha256, }, observed, blocker: null, next: controlPlaneNext(state, options.action), valuesRedacted: true, }; return rendered(result.ok, command, renderControlPlaneResult(result)); } function loadSentinelCicdState(spec: HwlabRuntimeLaneSpec, timeoutSeconds: number): SentinelCicdState { const sentinel = spec.observability.webProbe?.sentinel; if (sentinel === undefined) throw new Error(`config/hwlab-node-lanes.yaml#lanes.${spec.lane}.targets.${spec.nodeId}.observability.webProbe.sentinel is missing`); const configPlan = webProbeSentinelConfigPlan(spec, "status"); const runtime = recordTarget(readConfigRefTarget(sentinel.configRefs.runtime), sentinel.configRefs.runtime); const cicd = recordTarget(readConfigRefTarget(sentinel.configRefs.cicd), sentinel.configRefs.cicd); const publicExposure = recordTarget(readConfigRefTarget(sentinel.configRefs.publicExposure), sentinel.configRefs.publicExposure); const controlPlaneRef = stringField(cicd, "controlPlaneConfigRef"); const controlPlaneTarget = recordTarget(readConfigRefTarget(controlPlaneRef), controlPlaneRef); const controlPlaneConfig = recordTarget(readConfigFile(configRefFile(controlPlaneRef)), configRefFile(controlPlaneRef)); const nodeId = stringField(controlPlaneTarget, "node"); const controlPlaneNode = recordTarget(valueAtPath(controlPlaneConfig, `nodes.${nodeId}`), `${configRefFile(controlPlaneRef)}#nodes.${nodeId}`); const sourceHead = resolveSourceHead(cicd, timeoutSeconds); const image = sentinelImagePlan(cicd, sourceHead); const manifests = renderSentinelManifests(spec, runtime, cicd, publicExposure, image); const manifestYaml = `${manifests.map((item) => Bun.YAML.stringify(item).trim()).join("\n---\n")}\n`; return { spec, configReady: configPlan.ok, runtime, cicd, publicExposure, controlPlaneTarget, controlPlaneNode, sourceHead, image, manifests, manifestSha256: sha256(manifestYaml), valuesRedacted: true, }; } function resolveSourceHead(cicd: Record, timeoutSeconds: number): SourceHead { const repository = stringAt(cicd, "source.repository"); const branch = stringAt(cicd, "source.branch"); const remote = runCommand(["git", "ls-remote", "origin", `refs/heads/${branch}`], repoRoot, { timeoutMs: Math.min(timeoutSeconds, 60) * 1000 }); const local = runCommand(["git", "rev-parse", "HEAD"], repoRoot, { timeoutMs: 10_000 }); const commit = /^[0-9a-f]{40}\b/iu.exec(remote.stdout.trim())?.[0].toLowerCase() ?? null; const localHead = /^[0-9a-f]{40}$/iu.test(local.stdout.trim()) ? local.stdout.trim().toLowerCase() : null; return { ok: remote.exitCode === 0 && commit !== null, repository, branch, commit, localHead, result: compactCommand(remote), }; } function sentinelImagePlan(cicd: Record, sourceHead: SourceHead): SentinelImagePlan { const repository = stringAt(cicd, "image.repository"); const tag = sourceHead.commit === null ? "source-unresolved" : sourceHead.commit.slice(0, 12); const baseImageRef = stringAt(cicd, "image.baseImageRef"); const baseImage = stringTarget(readConfigRefTarget(baseImageRef), baseImageRef); const entrypoint = stringAt(cicd, "source.entrypoint"); const dockerfile = sentinelDockerfile(baseImage, entrypoint); return { repository, tag, ref: `${repository}:${tag}`, digestRef: null, baseImage, buildContext: stringAt(cicd, "source.buildContext"), entrypoint, dockerfileSha256: sha256(dockerfile), dockerfilePreview: dockerfile, }; } function sentinelDockerfile(baseImage: string, entrypoint: string): string { return [ `FROM ${baseImage}`, "WORKDIR /app", "COPY . /app", "RUN if [ -d /opt/hwlab-ci-node-deps/node_modules ]; then mkdir -p /app/node_modules; for dep in /opt/hwlab-ci-node-deps/node_modules/*; do ln -sf \"$dep\" \"/app/node_modules/$(basename \"$dep\")\"; done; fi", "ENV NODE_ENV=production", `ENTRYPOINT ["bun", "${entrypoint}"]`, "", ].join("\n"); } function renderSentinelManifests( spec: HwlabRuntimeLaneSpec, runtime: Record, cicd: Record, publicExposure: Record, image: SentinelImagePlan, ): readonly Record[] { const namespace = stringAt(runtime, "namespace"); const labels = { "app.kubernetes.io/name": stringAt(runtime, "deploymentName"), "app.kubernetes.io/part-of": "hwlab-web-probe-sentinel", "app.kubernetes.io/managed-by": "unidesk", "unidesk.ai/spec-ref": "PJ2026-01060508", "unidesk.ai/node": spec.nodeId, "unidesk.ai/lane": spec.lane, }; const deploymentName = stringAt(runtime, "deploymentName"); const serviceName = stringAt(runtime, "serviceName"); const servicePort = numberAt(runtime, "servicePort"); const pvcStorage = stringAt(runtime, "pvcStorage"); const stateRoot = stringAt(runtime, "stateRoot"); return [ { apiVersion: "v1", kind: "ServiceAccount", metadata: { name: stringAt(runtime, "serviceAccountName"), namespace, labels }, }, { apiVersion: "v1", kind: "PersistentVolumeClaim", metadata: { name: stringAt(runtime, "pvcName"), namespace, labels }, spec: { accessModes: ["ReadWriteOnce"], resources: { requests: { storage: pvcStorage } } }, }, { apiVersion: "v1", kind: "ConfigMap", metadata: { name: `${deploymentName}-config`, namespace, labels }, data: { "config-summary.json": JSON.stringify({ specRef: SPEC_REF, node: spec.nodeId, lane: spec.lane, publicBaseUrl: stringAt(publicExposure, "publicBaseUrl"), gitopsPath: stringAt(cicd, "gitopsPath"), valuesRedacted: true, }, null, 2), }, }, { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: deploymentName, namespace, labels }, spec: { replicas: numberAt(runtime, "replicas"), selector: { matchLabels: { "app.kubernetes.io/name": deploymentName } }, template: { metadata: { labels }, spec: { serviceAccountName: stringAt(runtime, "serviceAccountName"), containers: [{ name: "sentinel", image: image.ref, imagePullPolicy: "IfNotPresent", args: [ "--node", spec.nodeId, "--lane", spec.lane, "--state-root", stateRoot, "--host", stringAt(runtime, "listenHost"), "--port", String(servicePort), ], ports: [{ name: "http", containerPort: servicePort }], readinessProbe: { httpGet: { path: stringAt(runtime, "healthPath"), port: "http" } }, livenessProbe: { httpGet: { path: stringAt(runtime, "healthPath"), port: "http" } }, volumeMounts: [{ name: "state", mountPath: stateRoot }], }], volumes: [{ name: "state", persistentVolumeClaim: { claimName: stringAt(runtime, "pvcName") } }], }, }, }, }, { apiVersion: "v1", kind: "Service", metadata: { name: serviceName, namespace, labels }, spec: { type: "ClusterIP", selector: { "app.kubernetes.io/name": deploymentName }, ports: [{ name: "http", port: servicePort, targetPort: "http" }] }, }, { apiVersion: "apps/v1", kind: "Deployment", metadata: { name: stringAt(publicExposure, "frpc.deploymentName"), namespace, labels: { ...labels, "app.kubernetes.io/component": "tunnel" } }, spec: { replicas: 1, selector: { matchLabels: { "app.kubernetes.io/name": stringAt(publicExposure, "frpc.deploymentName") } }, template: { metadata: { labels: { ...labels, "app.kubernetes.io/name": stringAt(publicExposure, "frpc.deploymentName"), "app.kubernetes.io/component": "tunnel" }, annotations: { "unidesk.ai/public-base-url": stringAt(publicExposure, "publicBaseUrl"), "unidesk.ai/frp-server": `${stringAt(publicExposure, "frpc.serverAddr")}:${numberAt(publicExposure, "frpc.serverPort")}`, "unidesk.ai/frp-remote-port": String(numberAt(publicExposure, "frpc.httpProxy.remotePort")), }, }, spec: { containers: [{ name: "frpc", image: stringAt(publicExposure, "frpc.image"), imagePullPolicy: "IfNotPresent", args: ["-c", "/etc/frp/frpc.toml"], volumeMounts: [{ name: "frpc-config", mountPath: "/etc/frp/frpc.toml", subPath: stringAt(publicExposure, "frpc.secretKey"), readOnly: true }], }], volumes: [{ name: "frpc-config", secret: { secretName: stringAt(publicExposure, "frpc.secretName") } }], }, }, }, }, { apiVersion: "networking.k8s.io/v1", kind: "NetworkPolicy", metadata: { name: `${deploymentName}-egress`, namespace, labels }, spec: { podSelector: { matchLabels: { "app.kubernetes.io/name": deploymentName } }, policyTypes: ["Ingress", "Egress"], ingress: [{ from: [{ namespaceSelector: {} }], ports: [{ protocol: "TCP", port: servicePort }] }], egress: [{ to: [{ namespaceSelector: {} }] }], }, }, { apiVersion: "argoproj.io/v1alpha1", kind: "Application", metadata: { name: stringAt(cicd, "argo.applicationName"), namespace: stringAt(cicd, "argo.namespace"), labels }, spec: { project: stringAt(cicd, "argo.projectName"), source: { repoURL: stringAt(cicd, "argo.repoURL"), targetRevision: stringAt(cicd, "argo.targetRevision"), path: stringAt(cicd, "gitopsPath"), }, destination: { server: "https://kubernetes.default.svc", namespace }, syncPolicy: { automated: { prune: true, selfHeal: true } }, }, }, ]; } function probeImageRegistry(state: SentinelCicdState, timeoutSeconds: number): Record { const endpoint = stringAt(state.controlPlaneNode, "registry.endpoint"); const repoTag = state.image.ref.replace(`${endpoint}/`, ""); const repo = repoTag.slice(0, repoTag.lastIndexOf(":")); const tag = repoTag.slice(repoTag.lastIndexOf(":") + 1); const route = stringAt(state.controlPlaneNode, "route"); const script = [ "set +e", `url=${shellQuote(`http://${endpoint}/v2/${repo}/manifests/${tag}`)}`, "headers=$(mktemp)", "if command -v curl >/dev/null 2>&1 && curl -fsSI --max-time 5 \"$url\" >\"$headers\" 2>/tmp/web-probe-sentinel-image.err; then present=true; else present=false; fi", "digest=$(awk 'BEGIN{IGNORECASE=1} /^docker-content-digest:/ {gsub(/\\r/,\"\",$2); print $2; exit}' \"$headers\" 2>/dev/null)", "python3 - \"$present\" \"$digest\" \"$url\" <<'PY'", "import json, sys", "print(json.dumps({'present': sys.argv[1] == 'true', 'digest': sys.argv[2] or None, 'url': sys.argv[3], 'valuesRedacted': True}))", "PY", ].join("\n"); const result = runCommand(["trans", route, "sh", "--", script], repoRoot, { timeoutMs: Math.min(timeoutSeconds, 60) * 1000 }); return { ok: result.exitCode === 0, probe: parseJsonObject(result.stdout), result: compactCommand(result), }; } function runSentinelImageBuildConfirmed(state: SentinelCicdState, options: Extract): RenderedCliResult { const command = "web-probe sentinel image build"; const publish = runSentinelPublishJob(state, false, options.timeoutSeconds); const registry = probeImageRegistry(state, options.timeoutSeconds); const registryReady = record(registry.probe).present === true; const result = { ok: state.configReady && state.sourceHead.ok && publish.ok === true && registryReady, command, node: state.spec.nodeId, lane: state.spec.lane, mode: "confirm-wait", mutation: true, specRef: SPEC_REF, source: state.sourceHead, image: state.image, registry, publish, warnings: sentinelElapsedWarnings(record(publish).elapsedMs), blocker: null, next: { status: `bun scripts/cli.ts web-probe sentinel image status --node ${state.spec.nodeId} --lane ${state.spec.lane}`, controlPlaneTrigger: `bun scripts/cli.ts web-probe sentinel control-plane trigger-current --node ${state.spec.nodeId} --lane ${state.spec.lane} --confirm`, }, valuesRedacted: true, }; return rendered(result.ok, command, renderImageResult(result)); } function runSentinelControlPlaneConfirmed(state: SentinelCicdState, options: Extract): RenderedCliResult { const command = `web-probe sentinel control-plane ${options.action}`; const applyOnly = options.action === "apply"; const publish = applyOnly ? null : runSentinelPublishJob(state, true, options.timeoutSeconds); const flush = !applyOnly && record(publish).ok === true ? runChildCli(["hwlab", "nodes", "git-mirror", "flush", "--node", state.spec.nodeId, "--lane", state.spec.lane, "--confirm", "--wait"], options.timeoutSeconds) : null; const publicExposureApply = applySentinelPublicExposure(state, options.timeoutSeconds); const argoApply = applySentinelArgoApplication(state, options.timeoutSeconds); const observed = waitForSentinelObservedStatus(state, options.timeoutSeconds); const observedReady = sentinelObservedReady(observed); const targetValidation = applyOnly ? null : observedReady ? runSentinelQuickVerify(state, "control-plane-target-validation", options.timeoutSeconds) : { ok: false, status: "blocked", scenarioId: stringAt(state.cicd, "targetValidation.scenarioId"), reason: "runtime-not-ready", valuesRedacted: true, }; const targetValidationOk = applyOnly || record(targetValidation).ok === true; const ok = state.configReady && state.sourceHead.ok && (applyOnly || record(publish).ok === true) && (applyOnly || record(flush).ok === true) && record(publicExposureApply).ok === true && record(argoApply).ok === true && observedReady && targetValidationOk; const blocker = ok ? null : { code: targetValidationOk ? "sentinel-control-plane-not-ready" : "sentinel-target-validation-failed", reason: targetValidationOk ? "one or more publish, publicExposure, Argo or runtime observation checks did not pass" : text(record(targetValidation).failure ?? record(targetValidation).reason ?? "quick verify did not pass"), }; const result = { ok, command, node: state.spec.nodeId, lane: state.spec.lane, mode: "confirm-wait", mutation: true, specRef: SPEC_REF, source: state.sourceHead, image: state.image, pipelineRun: sentinelPipelineRunName(state), gitops: { path: stringAt(state.cicd, "gitopsPath"), targetRevision: stringAt(state.cicd, "argo.targetRevision"), manifestObjects: state.manifests.length, manifestSha256: state.manifestSha256, }, argo: { namespace: stringAt(state.cicd, "argo.namespace"), projectName: stringAt(state.cicd, "argo.projectName"), applicationName: stringAt(state.cicd, "argo.applicationName"), }, validation: { scenarioId: stringAt(state.cicd, "targetValidation.scenarioId"), maxSeconds: numberAt(state.cicd, "targetValidation.maxSeconds"), automaticSecondPath: false, }, manifests: { objects: manifestObjectSummary(state.manifests), sha256: state.manifestSha256, }, publish, flush, publicExposureApply, argoApply, observed, targetValidation, warnings: Array.from(new Set([ ...sentinelElapsedWarnings(record(publish).elapsedMs), ...sentinelElapsedWarnings(record(flush).result === undefined ? null : record(record(flush).result).durationMs), ...(Array.isArray(record(targetValidation).warnings) ? record(targetValidation).warnings.map(text) : []), ])), blocker, next: controlPlaneNext(state, options.action), valuesRedacted: true, }; return rendered(ok, command, renderControlPlaneResult(result)); } function renderAsyncSentinelJob(state: SentinelCicdState, domain: "image" | "control-plane", action: string, timeoutSeconds: number): RenderedCliResult { const args = domain === "image" ? ["web-probe", "sentinel", "image", action, "--node", state.spec.nodeId, "--lane", state.spec.lane, "--confirm", "--wait", "--timeout-seconds", String(timeoutSeconds)] : ["web-probe", "sentinel", "control-plane", action, "--node", state.spec.nodeId, "--lane", state.spec.lane, "--confirm", "--wait", "--timeout-seconds", String(timeoutSeconds)]; const job = startJob(`hwlab_nodes_${state.spec.lane}_web_probe_sentinel_${domain}_${action}`, ["bun", "scripts/cli.ts", ...args], `Run HWLAB ${state.spec.lane} web-probe sentinel ${domain} ${action} for node ${state.spec.nodeId}`); const command = `web-probe sentinel ${domain} ${action}`; const result = { ok: true, command, node: state.spec.nodeId, lane: state.spec.lane, mode: "async-job", mutation: true, reason: "confirmed sentinel publish/build can exceed the short interactive window; use job status for bounded progress.", job, next: { status: `bun scripts/cli.ts job status ${job.id} --tail-bytes 12000`, wait: ["bun", "scripts/cli.ts", ...args].join(" "), }, valuesRedacted: true, }; return rendered(true, command, renderAsyncJobResult(result)); } function collectSentinelObservedStatus(state: SentinelCicdState, timeoutSeconds: number, expectation?: SentinelObservedExpectation): SentinelObservedStatus { const registry = probeImageRegistry(state, timeoutSeconds); const gitops = probeGitopsRuntimeManifest(state, timeoutSeconds); const effectiveExpectation = { gitopsRevision: expectation?.gitopsRevision ?? nonEmptyString(gitops.revision), runtimeImage: expectation?.runtimeImage ?? nonEmptyString(gitops.image) ?? expectedRuntimeImageFromRegistry(state, registry), }; return { sourceMirror: probeSourceMirror(state, timeoutSeconds), registry, gitMirror: runChildCli(["hwlab", "nodes", "git-mirror", "status", "--node", state.spec.nodeId, "--lane", state.spec.lane], timeoutSeconds), gitops, argo: probeArgoApplication(state, timeoutSeconds, effectiveExpectation.gitopsRevision), runtime: probeRuntimeObjects(state, timeoutSeconds, effectiveExpectation.runtimeImage), }; } function waitForSentinelObservedStatus(state: SentinelCicdState, timeoutSeconds: number, expectation?: SentinelObservedExpectation): SentinelObservedStatus { const startedAt = Date.now(); const timeoutMs = Math.max(30_000, Math.min(timeoutSeconds * 1000, 900_000)); let observed = collectSentinelObservedStatus(state, timeoutSeconds, expectation); while (!sentinelObservedReady(observed) && Date.now() - startedAt < timeoutMs) { runCommand(["sleep", "5"], repoRoot, { timeoutMs: 6_000 }); observed = collectSentinelObservedStatus(state, timeoutSeconds, expectation); } return observed; } function sentinelObservedReady(value: Record | SentinelObservedStatus): boolean { const observed = record(value); return record(observed.sourceMirror).ok === true && record(record(observed.registry).probe).present === true && record(observed.gitMirror).ok === true && record(observed.gitops).ok === true && record(observed.argo).ok === true && record(observed.runtime).ok === true; } function probeSourceMirror(state: SentinelCicdState, timeoutSeconds: number): Record { const sourceMode = stringAt(state.cicd, "builder.sourceMode"); if (sourceMode === "sparse-git-checkout") { return { ok: state.sourceHead.ok, probe: { mode: sourceMode, commit: state.sourceHead.commit, expectedCommit: state.sourceHead.commit, persistentMirrorPresent: false, source: "commit-pinned sparse checkout declared in config/hwlab-web-probe-sentinel/cicd.d601-v03.yaml#sentinel.cicd.source.checkoutPaths", valuesRedacted: true, }, result: { exitCode: 0, timedOut: false, stdoutBytes: 0, stderrBytes: 0, stdoutPreview: "sourceMode=sparse-git-checkout", stderrPreview: "" }, }; } const namespace = stringAt(state.cicd, "builder.namespace"); const repository = stringAt(state.cicd, "source.repository"); const branch = stringAt(state.cicd, "source.branch"); const expectedCommit = state.sourceHead.commit; const script = [ "set +e", `repo_path=${shellQuote(`/cache/${repository}.git`)}`, `branch=${shellQuote(branch)}`, `expected=${shellQuote(expectedCommit ?? "")}`, "commit=$(kubectl -n " + shellQuote(namespace) + " exec deploy/git-mirror-http -- sh -lc \"git --git-dir=\\\"$repo_path\\\" rev-parse \\\"refs/heads/$branch\\\" 2>/dev/null\" 2>/dev/null)", "rc=$?", "node - \"$rc\" \"$commit\" \"$expected\" \"$repo_path\" \"$branch\" <<'NODE'", "const [rc, commit, expected, repoPath, branch] = process.argv.slice(2);", "const present = Number(rc) === 0 && /^[0-9a-f]{40}$/i.test(commit || '');", "console.log(JSON.stringify({ ok: present && (!expected || commit === expected), present, commit: present ? commit : null, expectedCommit: expected || null, branch, repoPath, valuesRedacted: true }));", "NODE", ].join("\n"); const result = runCommand(["trans", stringAt(state.controlPlaneNode, "kubeRoute"), "sh", "--", script], repoRoot, { timeoutMs: Math.min(timeoutSeconds, 60) * 1000 }); return { ok: result.exitCode === 0 && parseJsonObject(result.stdout)?.ok === true, probe: parseJsonObject(result.stdout), result: compactCommand(result) }; } function probeArgoApplication(state: SentinelCicdState, timeoutSeconds: number, expectedRevision: string | null): Record { const namespace = stringAt(state.cicd, "argo.namespace"); const applicationName = stringAt(state.cicd, "argo.applicationName"); const jsonpath = "{.status.sync.status}{\"\\n\"}{.status.health.status}{\"\\n\"}{.status.sync.revision}{\"\\n\"}"; const result = runCommand(["trans", stringAt(state.controlPlaneNode, "kubeRoute"), "kubectl", "-n", namespace, "get", "application", applicationName, "-o", `jsonpath=${jsonpath}`], repoRoot, { timeoutMs: Math.min(timeoutSeconds, 60) * 1000 }); const [syncStatusRaw, healthStatusRaw, revisionRaw] = result.stdout.trim().split(/\r?\n/u); const syncStatus = nonEmptyString(syncStatusRaw); const healthStatus = nonEmptyString(healthStatusRaw); const revision = nonEmptyString(revisionRaw); const revisionMatches = expectedRevision === null || revision === expectedRevision; const ok = result.exitCode === 0 && syncStatus === "Synced" && healthStatus === "Healthy" && revisionMatches; return { ok, present: result.exitCode === 0, syncStatus, healthStatus, revision, expectedRevision, revisionMatches, result: compactCommand(result), }; } function probeGitopsRuntimeManifest(state: SentinelCicdState, timeoutSeconds: number): Record { const namespace = stringAt(state.cicd, "builder.namespace"); const repository = stringAt(state.controlPlaneTarget, "source.repository"); const branch = stringAt(state.cicd, "argo.targetRevision"); const manifestPath = `${stringAt(state.cicd, "gitopsPath")}/web-probe-sentinel.yaml`; const repoPath = `/cache/${repository}.git`; const inner = [ "set -eu", `git --git-dir=${shellQuote(repoPath)} rev-parse ${shellQuote(`refs/heads/${branch}`)}`, `git --git-dir=${shellQuote(repoPath)} show ${shellQuote(`refs/heads/${branch}:${manifestPath}`)}`, ].join("\n"); const result = runCommand(["trans", stringAt(state.controlPlaneNode, "kubeRoute"), "kubectl", "-n", namespace, "exec", "deploy/git-mirror-http", "--", "sh", "-lc", inner], repoRoot, { timeoutMs: Math.min(timeoutSeconds, 60) * 1000 }); const [revisionLine, ...manifestLines] = result.stdout.split(/\r?\n/u); const revision = /^[0-9a-f]{40}$/iu.test(revisionLine?.trim() ?? "") ? revisionLine.trim() : null; const manifest = manifestLines.join("\n"); const image = nonEmptyString(manifest.match(/image:\s*([^,\s}\]]+)/u)?.[1]); const imageMatchesRepository = image !== null && image.startsWith(`${state.image.repository}@sha256:`); const compact = compactCommand(result); return { ok: result.exitCode === 0 && revision !== null && imageMatchesRepository, revision, branch, manifestPath, image, imageMatchesRepository, result: { ...compact, stdoutPreview: `${revision ?? "-"} ${image ?? "-"}` }, valuesRedacted: true, }; } function probeRuntimeObjects(state: SentinelCicdState, timeoutSeconds: number, expectedImage: string | null): Record { const namespace = stringAt(state.runtime, "namespace"); const deploymentName = stringAt(state.runtime, "deploymentName"); const serviceName = stringAt(state.runtime, "serviceName"); const pvcName = stringAt(state.runtime, "pvcName"); const configMapName = `${deploymentName}-config`; const serviceAccountName = stringAt(state.runtime, "serviceAccountName"); const script = [ "set +e", `namespace=${shellQuote(namespace)}`, `deployment=${shellQuote(deploymentName)}`, `service=${shellQuote(serviceName)}`, `pvc=${shellQuote(pvcName)}`, `configmap=${shellQuote(configMapName)}`, `serviceaccount=${shellQuote(serviceAccountName)}`, "tmp=$(mktemp -d)", "kubectl -n \"$namespace\" get deploy \"$deployment\" -o json >\"$tmp/deploy.json\" 2>/dev/null; echo $? >\"$tmp/deploy.rc\"", "kubectl -n \"$namespace\" get svc \"$service\" -o json >\"$tmp/svc.json\" 2>/dev/null; echo $? >\"$tmp/svc.rc\"", "kubectl -n \"$namespace\" get pvc \"$pvc\" -o json >\"$tmp/pvc.json\" 2>/dev/null; echo $? >\"$tmp/pvc.rc\"", "kubectl -n \"$namespace\" get cm \"$configmap\" -o json >\"$tmp/cm.json\" 2>/dev/null; echo $? >\"$tmp/cm.rc\"", "kubectl -n \"$namespace\" get sa \"$serviceaccount\" -o json >\"$tmp/sa.json\" 2>/dev/null; echo $? >\"$tmp/sa.rc\"", `expected_image=${shellQuote(expectedImage ?? "")}`, "node - \"$tmp\" \"$expected_image\" <<'NODE'", "const fs = require('node:fs');", "const dir = process.argv[2];", "const expectedImage = process.argv[3] || null;", "function rc(name){ return Number(fs.readFileSync(`${dir}/${name}.rc`, 'utf8').trim()); }", "function json(name){ try { return JSON.parse(fs.readFileSync(`${dir}/${name}.json`, 'utf8')); } catch { return null; } }", "const dep = json('deploy');", "const deploymentPresent = rc('deploy') === 0;", "const desired = Number(dep?.spec?.replicas ?? 0);", "const ready = Number(dep?.status?.readyReplicas ?? 0);", "const updated = Number(dep?.status?.updatedReplicas ?? 0);", "const image = dep?.spec?.template?.spec?.containers?.[0]?.image ?? null;", "const imageMatches = expectedImage === null || image === expectedImage;", "const payload = {", " deployment: { present: deploymentPresent, desiredReplicas: desired, readyReplicas: ready, updatedReplicas: updated, image, expectedImage, imageMatches },", " service: { present: rc('svc') === 0 },", " pvc: { present: rc('pvc') === 0, phase: json('pvc')?.status?.phase ?? null },", " configMap: { present: rc('cm') === 0 },", " serviceAccount: { present: rc('sa') === 0 },", " valuesRedacted: true", "};", "payload.ok = payload.deployment.present && imageMatches && ready >= Math.max(1, desired) && updated >= Math.max(1, desired) && payload.service.present && payload.pvc.present && payload.pvc.phase === 'Bound' && payload.configMap.present && payload.serviceAccount.present;", "console.log(JSON.stringify(payload));", "NODE", ].join("\n"); const result = runCommand(["trans", stringAt(state.controlPlaneNode, "kubeRoute"), "sh", "--", script], repoRoot, { timeoutMs: Math.min(timeoutSeconds, 60) * 1000 }); const probe = parseJsonObject(result.stdout); return { ok: result.exitCode === 0 && probe?.ok === true, probe, result: compactCommand(result) }; } function expectedRuntimeImageFromRegistry(state: SentinelCicdState, registry: Record): string | null { const digest = nonEmptyString(record(record(registry).probe).digest); if (digest === null) return null; return `${state.image.repository}@${digest}`; } function runSentinelPublishJob(state: SentinelCicdState, publishGitops: boolean, timeoutSeconds: number): SentinelRemoteJobResult { const jobName = `${stringAt(state.cicd, "builder.jobPrefix")}-${Date.now().toString(36)}`.replace(/[^a-z0-9-]/giu, "-").toLowerCase().slice(0, 63); const manifest = sentinelPublishJobManifest(state, jobName, publishGitops); const namespace = stringAt(state.cicd, "builder.namespace"); sentinelProgressEvent("sentinel.publish.progress", { phase: "create-job", status: "submitting", jobName, publishGitops, sourceCommit: state.sourceHead.commit, node: state.spec.nodeId, lane: state.spec.lane }); const created = runCommand(["trans", stringAt(state.controlPlaneNode, "kubeRoute"), "sh", "--", createK8sJobScript(namespace, manifest)], repoRoot, { timeoutMs: Math.min(timeoutSeconds, 60) * 1000 }); if (created.exitCode !== 0) { sentinelProgressEvent("sentinel.publish.progress", { phase: "create-job", status: "failed", jobName, publishGitops, node: state.spec.nodeId, lane: state.spec.lane }); return { ok: false, phase: "create-job", jobName, payload: { ok: false, status: "create-failed", valuesRedacted: true }, create: compactCommand(created), valuesRedacted: true }; } sentinelProgressEvent("sentinel.publish.progress", { phase: "create-job", status: "succeeded", jobName, publishGitops, node: state.spec.nodeId, lane: state.spec.lane }); const startedAt = Date.now(); const timeoutMs = Math.max(30_000, Math.min(timeoutSeconds * 1000, 900_000)); let polls = 0; let lastProbe: Record = {}; while (Date.now() - startedAt < timeoutMs) { polls += 1; const probeCapture = runCommand(["trans", stringAt(state.controlPlaneNode, "kubeRoute"), "sh", "--", probeK8sJobScript(namespace, jobName)], repoRoot, { timeoutMs: Math.min(timeoutSeconds, 60) * 1000 }); const probe = parseJsonObject(probeCapture.stdout) ?? {}; lastProbe = { ...probe, capture: compactCommand(probeCapture) }; const payload = sentinelPayloadFromLogs(String(probe.logsTail ?? "")); sentinelProgressEvent("sentinel.publish.progress", { phase: "remote-job", status: probe.succeeded === true ? "succeeded" : probe.failed === true ? "failed" : "running", jobName, publishGitops, polls, elapsedMs: Date.now() - startedAt, pod: probe.pod ?? null, sourceCommit: state.sourceHead.commit, node: state.spec.nodeId, lane: state.spec.lane, }); if (probe.succeeded === true) { const ok = payload.ok === true; return { ok, phase: "job-succeeded", jobName, payload: Object.keys(payload).length === 0 ? { ok: false, status: "result-missing", valuesRedacted: true } : payload, polls, elapsedMs: Date.now() - startedAt, probe: lastProbe, valuesRedacted: true }; } if (probe.failed === true) { return { ok: false, phase: "job-failed", jobName, payload: Object.keys(payload).length === 0 ? { ok: false, status: "failed", valuesRedacted: true } : payload, polls, elapsedMs: Date.now() - startedAt, probe: lastProbe, valuesRedacted: true }; } if (Date.now() - startedAt > 120_000) sentinelProgressEvent("sentinel.publish.warning", { warning: "remote job exceeded 120s; investigate env-reuse/git mirror/source build path", jobName, elapsedMs: Date.now() - startedAt, node: state.spec.nodeId, lane: state.spec.lane }); runCommand(["sleep", "5"], repoRoot, { timeoutMs: 6_000 }); } return { ok: false, phase: "job-timeout", jobName, payload: { ok: false, status: "timeout", valuesRedacted: true }, polls, elapsedMs: Date.now() - startedAt, probe: lastProbe, valuesRedacted: true }; } function sentinelPublishJobManifest(state: SentinelCicdState, jobName: string, publishGitops: boolean): Record { const namespace = stringAt(state.cicd, "builder.namespace"); const labels = { "app.kubernetes.io/name": "web-probe-sentinel-publish", "app.kubernetes.io/part-of": "hwlab-web-probe-sentinel", "unidesk.ai/spec-ref": "PJ2026-01060508", "unidesk.ai/node": state.spec.nodeId, "unidesk.ai/lane": state.spec.lane, }; return { apiVersion: "batch/v1", kind: "Job", metadata: { name: jobName, namespace, labels }, spec: { backoffLimit: 0, activeDeadlineSeconds: numberAt(state.cicd, "builder.activeDeadlineSeconds"), ttlSecondsAfterFinished: numberAt(state.cicd, "builder.ttlSecondsAfterFinished"), template: { metadata: { labels }, spec: { restartPolicy: "Never", volumes: [ { name: "cache", hostPath: { path: stringAt(state.controlPlaneTarget, "gitMirror.cacheHostPath"), type: "DirectoryOrCreate" } }, { name: "git-ssh", secret: { secretName: stringAt(state.cicd, "builder.gitSshSecretName"), defaultMode: 256 } }, { name: "docker-sock", hostPath: { path: stringAt(state.cicd, "builder.dockerSocketPath"), type: "Socket" } }, ], containers: [{ name: "publish", image: state.image.baseImage, imagePullPolicy: "IfNotPresent", command: ["/bin/sh", "-ec", sentinelPublishShell(state, jobName, publishGitops)], volumeMounts: [ { name: "cache", mountPath: "/cache" }, { name: "git-ssh", mountPath: "/git-ssh", readOnly: true }, { name: "docker-sock", mountPath: stringAt(state.cicd, "builder.dockerSocketPath") }, ], }], }, }, }, }; } function sentinelPublishShell(state: SentinelCicdState, jobName: string, publishGitops: boolean): string { const gitopsFiles = publishGitops ? sentinelGitopsFiles(state) : []; const filesB64 = Buffer.from(JSON.stringify(gitopsFiles.map((file) => ({ path: file.path, contentBase64: Buffer.from(file.content, "utf8").toString("base64"), }))), "utf8").toString("base64"); const checkoutPathsB64 = Buffer.from(JSON.stringify(arrayAt(state.cicd, "source.checkoutPaths").map((item) => { if (typeof item !== "string" || item.length === 0 || item.startsWith("/") || item.includes("..")) throw new Error("source.checkoutPaths must contain safe relative paths"); return item; })), "utf8").toString("base64"); const dockerfileB64 = Buffer.from(state.image.dockerfilePreview, "utf8").toString("base64"); return [ "set -eu", `job_name=${shellQuote(jobName)}`, `source_repository=${shellQuote(stringAt(state.cicd, "source.repository"))}`, `source_branch=${shellQuote(stringAt(state.cicd, "source.branch"))}`, `source_git_url=${shellQuote(stringAt(state.cicd, "source.gitSshUrl"))}`, `source_commit=${shellQuote(state.sourceHead.commit ?? "")}`, `checkout_paths_b64=${shellQuote(checkoutPathsB64)}`, `image_ref=${shellQuote(state.image.ref)}`, `image_repository=${shellQuote(state.image.repository)}`, `dockerfile_b64=${shellQuote(dockerfileB64)}`, `gitops_repository=${shellQuote(stringAt(state.controlPlaneTarget, "source.repository"))}`, `gitops_branch=${shellQuote(stringAt(state.cicd, "argo.targetRevision"))}`, `files_b64=${shellQuote(filesB64)}`, "started_ms=$(node -e 'console.log(Date.now())')", "emit_failed() { code=$?; if [ \"$code\" -ne 0 ]; then node - \"$code\" \"$job_name\" <<'NODE'\nconst [code, jobName] = process.argv.slice(2); console.log(JSON.stringify({ ok:false, status:'failed', exitCode:Number(code), jobName, valuesRedacted:true }));\nNODE\nfi; exit \"$code\"; }", "trap emit_failed EXIT", "mkdir -p /root/.ssh", "cp /git-ssh/ssh-privatekey /root/.ssh/id_rsa", "chmod 0400 /root/.ssh/id_rsa", "export GIT_SSH_COMMAND='ssh -i /root/.ssh/id_rsa -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/root/.ssh/known_hosts -o ConnectTimeout=15 -o ServerAliveInterval=5 -o ServerAliveCountMax=1'", "worktree=\"/tmp/$job_name/source\"", "rm -rf \"/tmp/$job_name\"", "mkdir -p \"/tmp/$job_name\"", "git init \"$worktree\"", "cd \"$worktree\"", "git remote add origin \"$source_git_url\"", "git config core.sparseCheckout true", "git config remote.origin.promisor true", "git config remote.origin.partialclonefilter blob:none", "CHECKOUT_PATHS_B64=\"$checkout_paths_b64\" node <<'NODE'", "const fs = require('node:fs');", "const paths = JSON.parse(Buffer.from(process.env.CHECKOUT_PATHS_B64 || '', 'base64').toString('utf8'));", "fs.mkdirSync('.git/info', { recursive: true });", "fs.writeFileSync('.git/info/sparse-checkout', paths.map((item) => item.endsWith('/') ? item : item + (item.includes('.') ? '' : '/')).join('\\n') + '\\n');", "NODE", "git fetch --depth=1 --filter=blob:none origin \"+refs/heads/$source_branch:refs/remotes/origin/$source_branch\"", "git checkout --detach \"$source_commit\"", "mirror_commit=$(git rev-parse HEAD)", "test \"$mirror_commit\" = \"$source_commit\"", "DOCKERFILE_B64=\"$dockerfile_b64\" node <<'NODE'", "const fs = require('node:fs');", "fs.writeFileSync('Dockerfile.web-probe-sentinel', Buffer.from(process.env.DOCKERFILE_B64 || '', 'base64'));", "NODE", "docker build -f Dockerfile.web-probe-sentinel -t \"$image_ref\" .", "docker push \"$image_ref\" > /tmp/web-probe-sentinel-docker-push.log 2>&1", "cat /tmp/web-probe-sentinel-docker-push.log", "tag=${image_ref##*:}", "repo_no_tag=${image_ref%:*}", "registry_path=${repo_no_tag#127.0.0.1:5000/}", "digest=$(awk '/digest: sha256:/ {print $3; exit}' /tmp/web-probe-sentinel-docker-push.log)", "if [ -z \"$digest\" ]; then digest=$(curl -fsSI --max-time 10 \"http://127.0.0.1:5000/v2/$registry_path/manifests/$tag\" 2>/dev/null | awk 'BEGIN{IGNORECASE=1} /^docker-content-digest:/ {gsub(/\\r/,\"\",$2); print $2; exit}'); fi", "test -n \"$digest\"", "digest_ref=\"$repo_no_tag@$digest\"", "gitops_commit=''", "changed=false", "file_count=0", "if [ \"$files_b64\" != \"W10=\" ]; then", " gitops_cache=\"/cache/${gitops_repository}.git\"", " gitops_worktree=\"/tmp/$job_name/gitops\"", " git clone --no-checkout \"$gitops_cache\" \"$gitops_worktree\"", " cd \"$gitops_worktree\"", " git fetch origin \"$gitops_branch\" || true", " if git rev-parse --verify \"refs/remotes/origin/$gitops_branch^{commit}\" >/dev/null 2>&1; then git checkout -B \"$gitops_branch\" \"refs/remotes/origin/$gitops_branch\"; else git checkout --orphan \"$gitops_branch\"; git rm -rf . >/dev/null 2>&1 || true; fi", " FILES_B64=\"$files_b64\" IMAGE_REF=\"$image_ref\" DIGEST_REF=\"$digest_ref\" node <<'NODE'", "const fs = require('node:fs');", "const path = require('node:path');", "const files = JSON.parse(Buffer.from(process.env.FILES_B64 || '', 'base64').toString('utf8'));", "for (const file of files) {", " const target = path.resolve(process.cwd(), file.path);", " if (!target.startsWith(process.cwd() + path.sep)) throw new Error(`refuse path outside workspace: ${file.path}`);", " fs.mkdirSync(path.dirname(target), { recursive: true });", " const text = Buffer.from(file.contentBase64, 'base64').toString('utf8').split(process.env.IMAGE_REF).join(process.env.DIGEST_REF);", " fs.writeFileSync(target, text);", "}", "console.error(JSON.stringify({event:'web-probe-sentinel-gitops-files', fileCount: files.length, valuesRedacted:true}));", "NODE", " git add .", " file_count=$(git diff --cached --name-only | wc -l | tr -d ' ')", " if git diff --quiet --cached; then changed=false; else changed=true; git -c user.email=web-probe-sentinel@unidesk.local -c user.name='UniDesk Web Probe Sentinel' commit -m \"deploy: render web-probe sentinel ${source_commit}\"; fi", " git push origin \"HEAD:refs/heads/$gitops_branch\"", " gitops_commit=$(git rev-parse HEAD)", "fi", "finished_ms=$(node -e 'console.log(Date.now())')", "node - \"$job_name\" \"$source_commit\" \"$mirror_commit\" \"$image_ref\" \"$digest_ref\" \"$gitops_commit\" \"$changed\" \"$file_count\" \"$started_ms\" \"$finished_ms\" <<'NODE'", "const [jobName, sourceCommit, mirrorCommit, imageRef, digestRef, gitopsCommit, changed, fileCount, startedMs, finishedMs] = process.argv.slice(2);", "console.log(JSON.stringify({ ok:true, status:'succeeded', jobName, sourceCommit, mirrorCommit, imageRef, digestRef, gitopsCommit: gitopsCommit || null, changed: changed === 'true', fileCount: Number(fileCount || 0), elapsedMs: Number(finishedMs) - Number(startedMs), valuesRedacted:true }));", "NODE", "trap - EXIT", ].join("\n"); } function sentinelGitopsFiles(state: SentinelCicdState): readonly { path: string; content: string }[] { const runtimeManifests = state.manifests.filter((item) => item.kind !== "Application"); return [{ path: `${stringAt(state.cicd, "gitopsPath")}/web-probe-sentinel.yaml`, content: `${runtimeManifests.map((item) => Bun.YAML.stringify(item).trim()).join("\n---\n")}\n`, }]; } function applySentinelArgoApplication(state: SentinelCicdState, timeoutSeconds: number): Record { const app = state.manifests.find((item) => item.kind === "Application"); if (app === undefined) return { ok: false, reason: "application-manifest-missing", valuesRedacted: true }; const yaml = `${Bun.YAML.stringify(app).trim()}\n`; const namespace = stringAt(state.cicd, "argo.namespace"); const applicationName = stringAt(state.cicd, "argo.applicationName"); const script = [ "set -eu", "tmp=$(mktemp)", `cat >"$tmp" <<'YAML'\n${yaml}YAML`, "kubectl apply -f \"$tmp\"", `kubectl -n ${shellQuote(namespace)} annotate application ${shellQuote(applicationName)} argocd.argoproj.io/refresh=hard --overwrite`, ].join("\n"); const result = runCommand(["trans", stringAt(state.controlPlaneNode, "kubeRoute"), "sh", "--", script], repoRoot, { timeoutMs: Math.min(timeoutSeconds, 60) * 1000 }); return { ok: result.exitCode === 0, result: compactCommand(result), valuesRedacted: true }; } function createK8sJobScript(namespace: string, manifest: Record): string { const yaml = `${Bun.YAML.stringify(manifest).trim()}\n`; return [ "set -eu", `kubectl -n ${shellQuote(namespace)} delete job ${shellQuote(stringAt(manifest, "metadata.name"))} --ignore-not-found=true >/dev/null 2>&1 || true`, "tmp=$(mktemp)", `cat >"$tmp" <<'YAML'\n${yaml}YAML`, "kubectl apply -f \"$tmp\"", ].join("\n"); } function probeK8sJobScript(namespace: string, jobName: string): string { return [ "set +e", `namespace=${shellQuote(namespace)}`, `job=${shellQuote(jobName)}`, "succeeded=$(kubectl -n \"$namespace\" get job \"$job\" -o jsonpath='{.status.succeeded}' 2>/dev/null)", "failed=$(kubectl -n \"$namespace\" get job \"$job\" -o jsonpath='{.status.failed}' 2>/dev/null)", "pod=$(kubectl -n \"$namespace\" get pod -l job-name=\"$job\" -o jsonpath='{.items[0].metadata.name}' 2>/dev/null)", "logs_tail=''", "if [ -n \"$pod\" ]; then logs_tail=$(kubectl -n \"$namespace\" logs \"$pod\" --tail=120 2>/dev/null | tail -c 12000 | base64 | tr -d '\\n'); fi", "node - \"$succeeded\" \"$failed\" \"$pod\" \"$logs_tail\" <<'NODE'", "const [succeeded, failed, pod, logsB64] = process.argv.slice(2);", "console.log(JSON.stringify({ succeeded: Number(succeeded || 0) > 0, failed: Number(failed || 0) > 0, pod: pod || null, logsTail: Buffer.from(logsB64 || '', 'base64').toString('utf8'), valuesRedacted: true }));", "NODE", ].join("\n"); } function sentinelPayloadFromLogs(logsTail: string): Record { const lines = logsTail.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean); for (let index = lines.length - 1; index >= 0; index -= 1) { const line = lines[index]; if (!line.startsWith("{") || !line.endsWith("}")) continue; const parsed = parseJsonObject(line); if (parsed !== null && (parsed.ok === true || parsed.ok === false)) return parsed; } return {}; } function sentinelElapsedWarnings(value: unknown): string[] { const elapsedMs = typeof value === "number" && Number.isFinite(value) ? value : null; if (elapsedMs === null || elapsedMs <= 120_000) return []; return [`sentinel confirmed operation exceeded 120s (${Math.round(elapsedMs / 1000)}s); investigate env-reuse/git mirror/source build path before treating this as normal.`]; } function sentinelProgressEvent(event: string, payload: Record): void { console.error(JSON.stringify({ event, at: new Date().toISOString(), ...payload, valuesRedacted: true })); } function confirmBlocked(action: string, state: SentinelCicdState): Record { return { code: "sentinel-cicd-confirm-requires-remote-publish-job", action, reason: "P4 currently provides YAML-first render/status/trigger dry-run and refuses to report a deployment mutation before the remote publish job is wired to the node-local git mirror.", sourceGitMirrorReadUrl: stringAt(state.cicd, "source.gitMirrorReadUrl"), requiredNextImplementation: [ "clone source from source.gitMirrorReadUrl at selected commit", "build and push digest-pinned image on the selected node", "publish manifests to the HWLAB gitops branch/path through git-mirror", "flush/recheck git-mirror and let Argo reconcile the Application", ], valuesRedacted: true, }; } function controlPlaneNext(state: SentinelCicdState, action: WebProbeSentinelControlPlaneAction): Record { const node = state.spec.nodeId; const lane = state.spec.lane; return { plan: `bun scripts/cli.ts web-probe sentinel control-plane plan --node ${node} --lane ${lane} --dry-run`, status: `bun scripts/cli.ts web-probe sentinel control-plane status --node ${node} --lane ${lane}`, image: `bun scripts/cli.ts web-probe sentinel image status --node ${node} --lane ${lane}`, triggerCurrent: `bun scripts/cli.ts web-probe sentinel control-plane trigger-current --node ${node} --lane ${lane} --dry-run`, issue: "https://github.com/pikasTech/unidesk/issues/889", currentAction: action, }; } function runSentinelMaintenance(state: SentinelCicdState, options: Extract): RenderedCliResult { const command = `web-probe sentinel maintenance ${options.action}`; const serviceHealth = callSentinelService(state, "GET", "/api/health", null, options.timeoutSeconds); if (options.action === "status") { const maintenance = callSentinelService(state, "GET", "/api/maintenance", null, options.timeoutSeconds); const result = { ok: serviceHealth.ok && maintenance.ok, command, node: state.spec.nodeId, lane: state.spec.lane, serviceHealth, maintenance, next: sentinelP5Next(state), valuesRedacted: true, }; return rendered(result.ok, command, renderMaintenanceResult(result)); } if (!options.confirm) { const result = { ok: serviceHealth.ok, command, node: state.spec.nodeId, lane: state.spec.lane, mode: "dry-run", serviceHealth, mutation: false, planned: { action: options.action, releaseId: options.releaseId, reason: options.reason, quickVerify: options.action === "stop" && options.quickVerify, }, next: sentinelP5Next(state), valuesRedacted: true, }; return rendered(result.ok, command, renderMaintenanceResult(result)); } if (!options.wait) return renderAsyncP5Job(state, ["maintenance", options.action], options.timeoutSeconds, options.releaseId, options.reason, options.quickVerify); if (!serviceHealth.ok) { const result = { ok: false, command, node: state.spec.nodeId, lane: state.spec.lane, mode: "confirm-wait", mutation: false, serviceHealth, blocker: serviceUnavailableBlocker(state), next: sentinelP5Next(state), valuesRedacted: true, }; return rendered(false, command, renderMaintenanceResult(result)); } const body = { releaseId: options.releaseId, reason: options.reason, source: "unidesk-cli", valuesRedacted: true }; const mutation = callSentinelService(state, "POST", `/api/maintenance/${options.action}`, body, options.timeoutSeconds); const quickVerify = options.action === "stop" && options.quickVerify && mutation.ok ? runSentinelQuickVerify(state, "maintenance-stop", options.timeoutSeconds) : null; const result = { ok: mutation.ok && (quickVerify === null || quickVerify.ok === true), command, node: state.spec.nodeId, lane: state.spec.lane, mode: "confirm-wait", mutation: true, serviceHealth, maintenance: mutation, quickVerify, blocker: mutation.ok ? null : serviceUnavailableBlocker(state), next: sentinelP5Next(state), valuesRedacted: true, }; return rendered(result.ok, command, renderMaintenanceResult(result)); } function runSentinelValidate(state: SentinelCicdState, options: Extract): RenderedCliResult { const command = "web-probe sentinel validate"; const initialHealth = callSentinelService(state, "GET", "/api/health", null, options.timeoutSeconds); let quickVerify: Record | null = null; if (options.quickVerify) { if (!options.confirm) { const result = { ok: initialHealth.ok, command, node: state.spec.nodeId, lane: state.spec.lane, mode: "dry-run", serviceHealth: initialHealth, planned: { quickVerify: true, waitRequired: true }, next: sentinelP5Next(state), valuesRedacted: true, }; return rendered(result.ok, command, renderValidateResult(result)); } if (!options.wait) return renderAsyncP5Job(state, ["validate"], options.timeoutSeconds, null, "manual-validate-quick-verify", true); if (!initialHealth.ok) { const result = { ok: false, command, node: state.spec.nodeId, lane: state.spec.lane, mode: "confirm-wait", serviceHealth: initialHealth, blocker: serviceUnavailableBlocker(state), next: sentinelP5Next(state), valuesRedacted: true, }; return rendered(false, command, renderValidateResult(result)); } quickVerify = runSentinelQuickVerify(state, "manual-validate", options.timeoutSeconds); } const health = callSentinelService(state, "GET", "/api/health", null, options.timeoutSeconds); const metrics = callSentinelService(state, "GET", "/metrics", null, options.timeoutSeconds); const report = callSentinelService(state, "GET", "/api/report?view=summary", null, options.timeoutSeconds); const publicExposure = probeSentinelPublicExposure(state, options.timeoutSeconds); const ok = health.ok && record(health.bodyJson).ok === true && metrics.ok && metricNames(record(metrics).bodyTextPreview).includes("web_probe_sentinel_health") && report.ok && publicExposure.ok === true && (quickVerify === null || quickVerify.ok === true); const result = { ok, command, node: state.spec.nodeId, lane: state.spec.lane, mode: options.quickVerify ? "confirm-wait" : "status", serviceHealth: health, metrics, report, publicExposure, quickVerify, blocker: ok ? null : validationBlocker(health, metrics, report, publicExposure, quickVerify), next: sentinelP5Next(state), valuesRedacted: true, }; return rendered(ok, command, renderValidateResult(result)); } function runSentinelReport(state: SentinelCicdState, options: Extract): RenderedCliResult { const command = `web-probe sentinel report ${options.latest ? "--latest " : ""}--view ${options.view}`; const query = new URLSearchParams({ view: options.view }); if (options.runId !== null) query.set("run", options.runId); if (options.traceId !== null) query.set("traceId", options.traceId); if (options.sampleSeq !== null) query.set("sampleSeq", String(options.sampleSeq)); const report = callSentinelService(state, "GET", `/api/report?${query.toString()}`, null, options.timeoutSeconds); const body = record(report.bodyJson); const renderedText = typeof body.renderedText === "string" ? body.renderedText : renderReportResult({ command, node: state.spec.nodeId, lane: state.spec.lane, report, valuesRedacted: true }); const rawPayload = Object.keys(body).length > 0 ? body : report; return rendered(report.ok && body.ok !== false, command, options.raw ? JSON.stringify(rawPayload, null, 2) : renderedText); } function renderAsyncP5Job(state: SentinelCicdState, subcommand: readonly string[], timeoutSeconds: number, releaseId: string | null, reason: string | null, quickVerify: boolean): RenderedCliResult { const args = ["web-probe", "sentinel", ...subcommand, "--node", state.spec.nodeId, "--lane", state.spec.lane, "--confirm", "--wait", "--timeout-seconds", String(timeoutSeconds)]; if (releaseId !== null) args.push("--release-id", releaseId); if (reason !== null) args.push("--reason", reason); if (quickVerify) args.push("--quick-verify"); const job = startJob(`hwlab_nodes_${state.spec.lane}_web_probe_sentinel_${subcommand.join("_")}`, ["bun", "scripts/cli.ts", ...args], `Run HWLAB ${state.spec.lane} web-probe sentinel ${subcommand.join(" ")} for node ${state.spec.nodeId}`); const command = `web-probe sentinel ${subcommand.join(" ")}`; return rendered(true, command, renderAsyncJobResult({ ok: true, command, node: state.spec.nodeId, lane: state.spec.lane, mode: "async-job", mutation: true, job, next: { status: `bun scripts/cli.ts job status ${job.id} --tail-bytes 12000`, wait: ["bun", "scripts/cli.ts", ...args].join(" "), }, valuesRedacted: true, })); } function runSentinelQuickVerify(state: SentinelCicdState, reason: string, timeoutSeconds: number): Record { const scenarioId = stringAt(state.cicd, "targetValidation.scenarioId"); const maxSeconds = numberAt(state.cicd, "targetValidation.maxSeconds"); const scenario = findScenario(state, scenarioId); if (scenario === null) return { ok: false, status: "blocked", reason: "scenario-not-found", scenarioId, valuesRedacted: true }; const prompts = readPromptSetForScenario(scenario); if (!prompts.ok) return { ok: false, status: "blocked", reason: "prompt-source-unavailable", promptSource: prompts, valuesRedacted: true }; const sampleIntervalMs = numberAt(scenario, "sampleIntervalMs"); const deadline = Date.now() + Math.min(timeoutSeconds, maxSeconds) * 1000; const runId = `sentinel-run-${Date.now().toString(36)}-${randomUUID().slice(0, 8)}`; const steps: Record[] = []; const startArgs = [ "web-probe", "observe", "start", "--node", state.spec.nodeId, "--lane", state.spec.lane, "--target-path", stringAt(scenario, "observeTargetPath"), "--sample-interval-ms", String(sampleIntervalMs), "--screenshot-interval-ms", String(numberAt(scenario, "screenshotIntervalMs")), "--command-timeout-seconds", "55", ]; const started = runChildCli(startArgs, remainingSeconds(deadline, 55)); steps.push({ phase: "observe-start", ok: started.ok, result: started.result }); const observerId = observerIdFromText(String(record(started.result).stdoutPreview ?? "")); if (!started.ok || observerId === null) { return recordQuickVerify(state, { ok: false, runId, scenarioId, reason, status: "blocked", observerId, steps, failure: "observe-start-failed", valuesRedacted: true, }); } let promptIndex = 0; for (const item of arrayAt(scenario, "commandSequence").map(record)) { const type = stringAt(item, "type"); const repeat = Math.max(1, typeof item.repeat === "number" && Number.isFinite(item.repeat) ? Math.trunc(item.repeat) : 1); for (let index = 0; index < repeat; index += 1) { if (Date.now() >= deadline) { return recordQuickVerify(state, finalizeQuickVerifyFailure(state, { runId, scenarioId, reason, observerId, promptIndex, steps, failure: "quick-verify-timeout-over-120s", warnings: ["quick verify exceeded the configured 120s targetValidation budget; investigate env-reuse/git mirror/source build path before retrying."], promptSource: prompts.summary, })); } const args = ["web-probe", "observe", "command", observerId, "--node", state.spec.nodeId, "--lane", state.spec.lane, "--type", type, "--wait-ms", "55000", "--command-timeout-seconds", String(remainingSeconds(deadline, 55))]; if (type === "selectProvider") args.push("--provider", stringAt(item, "provider")); if (type === "sendPrompt") { args.push("--text", prompts.prompts[promptIndex % prompts.prompts.length] ?? ""); promptIndex += 1; } const commandResult = runChildCli(args, remainingSeconds(deadline, 60)); steps.push({ phase: `observe-command-${type}`, ok: commandResult.ok, promptIndex: type === "sendPrompt" ? promptIndex : null, result: commandResult.result }); if (!commandResult.ok) { return recordQuickVerify(state, finalizeQuickVerifyFailure(state, { runId, scenarioId, reason, observerId, promptIndex, steps, failure: `observe-command-${type}-failed`, promptSource: prompts.summary, })); } if (type === "sendPrompt") { const waitResult = waitForQuickVerifyPromptTurn(state, observerId, promptIndex, deadline, sampleIntervalMs); steps.push({ phase: "observe-wait-turn-terminal", ok: waitResult.ok, promptIndex, result: waitResult }); if (waitResult.ok !== true) { return recordQuickVerify(state, finalizeQuickVerifyFailure(state, { runId, scenarioId, reason, observerId, promptIndex, steps, failure: text(waitResult.failure ?? "observe-turn-terminal-wait-failed"), promptSource: prompts.summary, warnings: Array.isArray(waitResult.warnings) ? waitResult.warnings : [], })); } } } } const analysis = runChildCli(["web-probe", "observe", "analyze", observerId, "--node", state.spec.nodeId, "--lane", state.spec.lane, "--command-timeout-seconds", String(remainingSeconds(deadline, 120))], remainingSeconds(deadline, 120)); steps.push({ phase: "observe-analyze", ok: analysis.ok, result: analysis.result }); const indexEntry = readLocalObserveIndex(observerId); const artifactSummary = indexEntry === null ? { ok: false, reason: "observe-index-entry-missing", observerId, valuesRedacted: true } : readAnalysisSummaryFromWorkspace(state, indexEntry.stateDir, remainingSeconds(deadline, 30)); const turnSummary = collectObserveView(state, observerId, "turn-summary", null, remainingSeconds(deadline, 30)); const traceFrame = collectObserveView(state, observerId, "trace-frame", promptIndex > 0 ? promptIndex : null, remainingSeconds(deadline, 30)); const ok = analysis.ok && record(artifactSummary).ok === true; return recordQuickVerify(state, { ok, runId, scenarioId, reason, status: ok ? "analyzed" : "blocked", observerId, stateDir: indexEntry?.stateDir ?? null, reportJsonSha256: stringAtNullable(artifactSummary, "reportJsonSha256"), findingCount: numberAtNullable(artifactSummary, "findingCount") ?? 0, artifactCount: numberAtNullable(artifactSummary, "artifactCount") ?? 0, promptSource: prompts.summary, steps, analysis: artifactSummary, views: { summary: { renderedText: renderQuickVerifySummary({ runId, scenarioId, observerId, artifactSummary, steps, publicOrigin: stringAt(state.publicExposure, "publicBaseUrl") }) }, "turn-summary": { renderedText: typeof turnSummary.renderedText === "string" ? turnSummary.renderedText : null, ok: turnSummary.ok }, "trace-frame": { renderedText: typeof traceFrame.renderedText === "string" ? traceFrame.renderedText : null, ok: traceFrame.ok }, }, findings: Array.isArray(record(artifactSummary).findings) ? record(artifactSummary).findings : [], screenshot: record(artifactSummary).screenshot, publicOrigin: stringAt(state.publicExposure, "publicBaseUrl"), valuesRedacted: true, }); } function finalizeQuickVerifyFailure(state: SentinelCicdState, input: { readonly runId: string; readonly scenarioId: string; readonly reason: string; readonly observerId: string; readonly promptIndex: number; readonly steps: readonly Record[]; readonly failure: string; readonly promptSource?: Record; readonly warnings?: readonly unknown[]; }): Record { const cleanupSteps: Record[] = []; if (input.promptIndex > 0) { const cancel = runChildCli([ "web-probe", "observe", "command", input.observerId, "--node", state.spec.nodeId, "--lane", state.spec.lane, "--type", "cancel", "--wait-ms", "55000", "--command-timeout-seconds", "55", ], 60); cleanupSteps.push({ phase: "observe-cancel-after-failure", ok: cancel.ok, result: cancel.result }); } const stop = runChildCli([ "web-probe", "observe", "stop", input.observerId, "--node", state.spec.nodeId, "--lane", state.spec.lane, "--force", "--command-timeout-seconds", "55", ], 30); cleanupSteps.push({ phase: "observe-stop-after-failure", ok: stop.ok, result: stop.result }); const analysis = runChildCli([ "web-probe", "observe", "analyze", input.observerId, "--node", state.spec.nodeId, "--lane", state.spec.lane, "--command-timeout-seconds", "55", ], 60); cleanupSteps.push({ phase: "observe-analyze-after-failure", ok: analysis.ok, result: analysis.result }); const indexEntry = readLocalObserveIndex(input.observerId); const artifactSummary = indexEntry === null ? { ok: false, reason: "observe-index-entry-missing", observerId: input.observerId, valuesRedacted: true } : readAnalysisSummaryFromWorkspace(state, indexEntry.stateDir, 30); const turnSummary = collectObserveView(state, input.observerId, "turn-summary", null, 30); const traceFrame = collectObserveView(state, input.observerId, "trace-frame", input.promptIndex > 0 ? input.promptIndex : null, 30); return { ok: false, runId: input.runId, scenarioId: input.scenarioId, reason: input.reason, status: "blocked", observerId: input.observerId, stateDir: indexEntry?.stateDir ?? null, reportJsonSha256: stringAtNullable(artifactSummary, "reportJsonSha256"), findingCount: numberAtNullable(artifactSummary, "findingCount") ?? 0, artifactCount: numberAtNullable(artifactSummary, "artifactCount") ?? 0, failure: input.failure, promptSource: input.promptSource, steps: [...input.steps, ...cleanupSteps], analysis: artifactSummary, views: { summary: { renderedText: renderQuickVerifySummary({ runId: input.runId, scenarioId: input.scenarioId, observerId: input.observerId, artifactSummary, steps: input.steps, publicOrigin: stringAt(state.publicExposure, "publicBaseUrl") }) }, "turn-summary": { renderedText: typeof turnSummary.renderedText === "string" ? turnSummary.renderedText : null, ok: turnSummary.ok }, "trace-frame": { renderedText: typeof traceFrame.renderedText === "string" ? traceFrame.renderedText : null, ok: traceFrame.ok }, }, findings: Array.isArray(record(artifactSummary).findings) ? record(artifactSummary).findings : [], screenshot: record(artifactSummary).screenshot, publicOrigin: stringAt(state.publicExposure, "publicBaseUrl"), warnings: Array.isArray(input.warnings) ? input.warnings.map(text) : [], valuesRedacted: true, }; } function recordQuickVerify(state: SentinelCicdState, payload: Record): Record { const recordResult = callSentinelService(state, "POST", "/api/runs/record", { runId: payload.runId, scenarioId: payload.scenarioId, status: payload.status, observerId: payload.observerId, stateDir: payload.stateDir, reportJsonSha256: payload.reportJsonSha256, findingCount: payload.findingCount, artifactCount: payload.artifactCount, summary: { reason: payload.reason, status: payload.status, analysis: payload.analysis, promptSource: payload.promptSource, steps: payload.steps, valuesRedacted: true, }, findings: payload.findings, views: payload.views, screenshot: payload.screenshot, publicOrigin: payload.publicOrigin ?? stringAt(state.publicExposure, "publicBaseUrl"), maintenance: payload.reason === "maintenance-stop", valuesRedacted: true, }, 60); return { ...payload, recordResult, valuesRedacted: true }; } function callSentinelService(state: SentinelCicdState, method: "GET" | "POST", pathWithQuery: string, body: Record | null, timeoutSeconds: number): Record { const namespace = stringAt(state.runtime, "namespace"); const deploymentName = stringAt(state.runtime, "deploymentName"); const serviceName = stringAt(state.runtime, "serviceName"); const servicePort = numberAt(state.runtime, "servicePort"); const url = `http://${serviceName}.${namespace}.svc.cluster.local:${servicePort}${pathWithQuery}`; const bodyB64 = Buffer.from(body === null ? "" : JSON.stringify(body), "utf8").toString("base64"); const js = [ "const method=process.env.REQ_METHOD||'GET';", "const url=process.env.REQ_URL||'';", "const body=Buffer.from(process.env.REQ_BODY_B64||'', 'base64').toString('utf8');", "const init={method,headers:{}};", "if(method!=='GET'&&method!=='HEAD'){init.headers['content-type']='application/json';init.body=body;}", "let out;", "function rec(v){return v&&typeof v==='object'&&!Array.isArray(v)?v:{}}", "function pick(o,keys){const r={};for(const k of keys){if(o&&Object.prototype.hasOwnProperty.call(o,k))r[k]=o[k];}return r}", "function compactBodyJson(v){const o=rec(v);if(typeof o.renderedText!=='string')return v;return {...pick(o,['ok','view','error','availableViews','valuesRedacted']),run:pick(rec(o.run),['id','scenario_id','status','node','lane','observer_id','state_dir','report_json_sha256','finding_count','artifact_count','maintenance','created_at','updated_at']),summary:pick(rec(o.summary),['reason','status','valuesRedacted']),findings:Array.isArray(o.findings)?o.findings.slice(0,12):[],renderedText:o.renderedText,valuesRedacted:true}}", "try{const res=await fetch(url,init);const text=await res.text();let bodyJson=null;try{bodyJson=JSON.parse(text)}catch{};out={ok:res.ok,httpStatus:res.status,contentType:res.headers.get('content-type'),bodyJson:compactBodyJson(bodyJson),bodyTextPreview:bodyJson===null?text.slice(0,4000):'',bodyBytes:Buffer.byteLength(text),valuesRedacted:true};}", "catch(error){out={ok:false,error:error instanceof Error?error.message:String(error),valuesRedacted:true};}", "console.log(JSON.stringify(out));", ].join(""); const script = [ "set +e", `namespace=${shellQuote(namespace)}`, `deployment=${shellQuote(deploymentName)}`, `method=${shellQuote(method)}`, `url=${shellQuote(url)}`, `body_b64=${shellQuote(bodyB64)}`, `js=${shellQuote(js)}`, "if ! kubectl -n \"$namespace\" get deploy \"$deployment\" >/dev/null 2>&1; then node - \"$namespace\" \"$deployment\" <<'NODE'\nconst [namespace,deployment]=process.argv.slice(2); console.log(JSON.stringify({ok:false,error:'sentinel-deployment-missing',namespace,deployment,valuesRedacted:true}));\nNODE\nexit 0; fi", "kubectl -n \"$namespace\" exec deploy/\"$deployment\" -- env REQ_METHOD=\"$method\" REQ_URL=\"$url\" REQ_BODY_B64=\"$body_b64\" bun -e \"$js\"", ].join("\n"); const result = runCommand(["trans", stringAt(state.controlPlaneNode, "kubeRoute"), "sh", "--", script], repoRoot, { timeoutMs: Math.min(timeoutSeconds, 60) * 1000 }); const parsed = parseJsonObject(result.stdout); return { ok: result.exitCode === 0 && parsed?.ok === true, method, path: pathWithQuery, internalUrl: `http://${serviceName}.${namespace}.svc.cluster.local:${servicePort}${pathWithQuery}`, httpStatus: parsed?.httpStatus ?? null, bodyJson: record(parsed?.bodyJson), bodyTextPreview: typeof parsed?.bodyTextPreview === "string" ? parsed.bodyTextPreview : "", bodyBytes: parsed?.bodyBytes ?? null, error: parsed?.error ?? null, result: compactCommand(result), valuesRedacted: true, }; } function probeSentinelPublicExposure(state: SentinelCicdState, timeoutSeconds: number): Record { const publicBaseUrl = stringAt(state.publicExposure, "publicBaseUrl"); const hostname = stringAt(state.publicExposure, "hostname"); const expectedA = stringAt(state.publicExposure, "expectedA"); const probeUrl = `${publicBaseUrl.replace(/\/$/u, "")}/api/health`; const script = [ "set +e", `host=${shellQuote(hostname)}`, `expected=${shellQuote(expectedA)}`, `url=${shellQuote(probeUrl)}`, "dns=$(getent ahostsv4 \"$host\" 2>/dev/null | awk '{print $1}' | sort -u | paste -sd, -)", "headers=$(mktemp)", "body=$(mktemp)", "writeout=$(curl -sS -D \"$headers\" -o \"$body\" --connect-timeout 8 --max-time 20 --write-out '%{http_code} %{ssl_verify_result} %{remote_ip}' \"$url\" 2>/tmp/web-probe-sentinel-public.err)", "curl_rc=$?", "body_head=$(head -c 1000 \"$body\" | base64 | tr -d '\\n')", "node - \"$dns\" \"$expected\" \"$writeout\" \"$curl_rc\" \"$url\" \"$body_head\" \"$headers\" <<'NODE'", "const fs=require('node:fs');", "const [dns,expected,writeout,rcRaw,url,bodyB64,headersPath]=process.argv.slice(2);", "const [statusRaw,sslRaw,remoteIp]=String(writeout||'').trim().split(/\\s+/);", "const status=Number(statusRaw||0);", "const ssl=Number(sslRaw||-1);", "const addrs=dns?dns.split(',').filter(Boolean):[];", "const headers=(()=>{try{return fs.readFileSync(headersPath,'utf8')}catch{return ''}})();", "const body=Buffer.from(bodyB64||'', 'base64').toString('utf8');", "const authCovered=status===401||status===403||status>=200&&status<300;", "const edgeOk=Number(rcRaw)===0&&ssl===0&&status>0&&status<500;", "const upstreamOk=status>=200&&status<300&&body.includes('valuesRedacted');", "const dnsMatches=addrs.includes(expected);", "console.log(JSON.stringify({ok:dnsMatches&&edgeOk&&authCovered&&upstreamOk,publicUrl:url,dns:{addresses:addrs,expectedA:expected,matches:dnsMatches},tls:{verified:ssl===0,sslVerifyResult:ssl,remoteIp:remoteIp||null},https:{curlExitCode:Number(rcRaw),httpStatus:status,edgeOk},auth:{requestAuthorizationHeader:false,covered:authCovered,status},upstream:{ok:upstreamOk,bodyPreview:body.slice(0,200)},headers:{wwwAuthenticate:/^www-authenticate:/im.test(headers)},valuesRedacted:true}));", "NODE", ].join("\n"); const result = runCommand(["bash", "-lc", script], repoRoot, { timeoutMs: Math.min(timeoutSeconds, 30) * 1000 }); const parsed = parseJsonObject(result.stdout); return { ok: result.exitCode === 0 && parsed?.ok === true, ...record(parsed), result: compactCommand(result), valuesRedacted: true }; } function applySentinelPublicExposure(state: SentinelCicdState, timeoutSeconds: number): Record { const material = readSentinelFrpcMaterial(state); if (!material.ok) return { ok: false, hostname: stringAt(state.publicExposure, "hostname"), material, valuesRedacted: true }; const secret = applySentinelFrpcSecret(state, stringAt(material, "frpcToml"), timeoutSeconds); const caddy = applySentinelCaddyBlock(state, timeoutSeconds); return { ok: secret.ok === true && caddy.ok === true, hostname: stringAt(state.publicExposure, "hostname"), publicBaseUrl: stringAt(state.publicExposure, "publicBaseUrl"), material: { ok: true, sourceRef: material.sourceRef, sourcePath: material.sourcePath, fingerprint: material.fingerprint, valuesRedacted: true, }, secret, caddy, valuesRedacted: true, }; } function readSentinelFrpcMaterial(state: SentinelCicdState): Record { const sourceRef = stringAt(state.publicExposure, "frpc.tokenSourceRef"); const sourceKey = stringAt(state.publicExposure, "frpc.tokenSourceKey"); const paths = secretSourcePaths(sourceRef); const sourcePath = paths.find((item) => existsSync(item)) ?? paths[0] ?? join(repoRoot, ".state", "secrets", sourceRef); if (!existsSync(sourcePath)) return { ok: false, error: "frp-token-source-missing", sourceRef, sourceKey, sourcePath: displayPath(sourcePath), valuesRedacted: true }; const values = parseEnvFile(readFileSync(sourcePath, "utf8")); const token = values[sourceKey]; if (token === undefined || token.length === 0) return { ok: false, error: "frp-token-key-missing", sourceRef, sourceKey, sourcePath: displayPath(sourcePath), valuesRedacted: true }; const proxy = record(valueAtPath(state.publicExposure, "frpc.httpProxy")); const frpcToml = [ `serverAddr = "${tomlEscape(stringAt(state.publicExposure, "frpc.serverAddr"))}"`, `serverPort = ${numberAt(state.publicExposure, "frpc.serverPort")}`, "loginFailExit = true", `auth.token = "${tomlEscape(token)}"`, "", "[[proxies]]", `name = "${tomlEscape(stringAt(proxy, "name"))}"`, 'type = "tcp"', `localIP = "${tomlEscape(stringAt(proxy, "localIP"))}"`, `localPort = ${numberAt(proxy, "localPort")}`, `remotePort = ${numberAt(proxy, "remotePort")}`, "", ].join("\n"); return { ok: true, sourceRef, sourceKey, sourcePath: displayPath(sourcePath), frpcToml, fingerprint: `sha256:${createHash("sha256").update(`${token}\n${frpcToml}`).digest("hex").slice(0, 16)}`, valuesRedacted: true, }; } function applySentinelFrpcSecret(state: SentinelCicdState, frpcToml: string, timeoutSeconds: number): Record { const namespace = stringAt(state.runtime, "namespace"); const secretName = stringAt(state.publicExposure, "frpc.secretName"); const secretKey = stringAt(state.publicExposure, "frpc.secretKey"); const script = [ "set +e", `namespace=${shellQuote(namespace)}`, `secret=${shellQuote(secretName)}`, `key=${shellQuote(secretKey)}`, "tmp=$(mktemp -d)", "trap 'rm -rf \"$tmp\"' EXIT", "cat >\"$tmp/frpc.toml\"", "kubectl -n \"$namespace\" create secret generic \"$secret\" --from-file=\"$key=$tmp/frpc.toml\" --dry-run=client -o yaml | kubectl apply --server-side --force-conflicts --field-manager=unidesk-web-probe-sentinel-public-exposure -f - >/tmp/web-probe-sentinel-frpc-secret.out 2>/tmp/web-probe-sentinel-frpc-secret.err", "rc=$?", "present=no", "bytes=0", "if kubectl -n \"$namespace\" get secret \"$secret\" -o jsonpath=\"{.data.$key}\" >/tmp/web-probe-sentinel-frpc-secret.data 2>/dev/null; then present=yes; bytes=$(base64 -d /dev/null | wc -c | tr -d ' '); fi", "node - \"$rc\" \"$namespace\" \"$secret\" \"$key\" \"$present\" \"$bytes\" <<'NODE'", "const [rc, namespace, secret, key, present, bytes] = process.argv.slice(2);", "console.log(JSON.stringify({ok:Number(rc)===0&&present==='yes',namespace,secret,key,present:present==='yes',bytes:Number(bytes||0),applyExitCode:Number(rc),valuesRedacted:true}));", "NODE", ].join("\n"); const result = runCommand(["trans", stringAt(state.controlPlaneNode, "kubeRoute"), "sh", "--", script], repoRoot, { input: frpcToml, timeoutMs: Math.min(timeoutSeconds, 60) * 1000 }); const parsed = parseJsonObject(result.stdout); return { ok: result.exitCode === 0 && parsed?.ok === true, ...record(parsed), result: compactCommand(result), valuesRedacted: true }; } function applySentinelCaddyBlock(state: SentinelCicdState, timeoutSeconds: number): Record { const hostname = stringAt(state.publicExposure, "hostname"); const owner = stringAt(state.publicExposure, "caddy.managedBlockOwner"); const configPath = stringAt(state.publicExposure, "caddy.configPath"); const serviceName = stringAt(state.publicExposure, "caddy.serviceName"); const responseHeaderTimeoutSeconds = numberAt(state.publicExposure, "caddy.responseHeaderTimeoutSeconds"); const remotePort = numberAt(state.publicExposure, "frpc.httpProxy.remotePort"); const block = [ `${hostname} {`, ` reverse_proxy 127.0.0.1:${remotePort} {`, " transport http {", ` response_header_timeout ${responseHeaderTimeoutSeconds}s`, " }", " }", "}", "", ].join("\n"); const blockB64 = Buffer.from(block, "utf8").toString("base64"); const script = [ "set +e", `hostname=${shellQuote(hostname)}`, `owner=${shellQuote(owner)}`, `config_path=${shellQuote(configPath)}`, `service=${shellQuote(serviceName)}`, `block_b64=${shellQuote(blockB64)}`, "marker=\"unidesk managed $owner\"", "tmp=$(mktemp -d)", "trap 'rm -rf \"$tmp\"' EXIT", "block=\"$tmp/block\"", "next=\"$tmp/Caddyfile\"", "printf '%s' \"$block_b64\" | base64 -d >\"$block\"", "if [ -f \"$config_path\" ]; then cp \"$config_path\" \"$next\"; else : >\"$next\"; fi", "python3 - \"$next\" \"$block\" \"$marker\" <<'PY' >/tmp/web-probe-sentinel-caddy-python.out 2>/tmp/web-probe-sentinel-caddy-python.err", "import pathlib, re, sys", "config = pathlib.Path(sys.argv[1])", "block = pathlib.Path(sys.argv[2]).read_text(encoding='utf-8')", "marker = sys.argv[3]", "text = config.read_text(encoding='utf-8') if config.exists() else ''", "begin = f'# BEGIN {marker}'", "end = f'# END {marker}'", "managed = f'{begin}\\n{block.rstrip()}\\n# END {marker}\\n'", "pattern = re.compile(rf'(?ms)^# BEGIN {re.escape(marker)}\\n.*?\\n# END {re.escape(marker)}\\n*')", "if pattern.search(text):", " text = pattern.sub(managed, text)", "else:", " text = text.rstrip() + '\\n\\n' + managed", "config.write_text(text, encoding='utf-8')", "PY", "python_rc=$?", "validate_rc=1", "reload_rc=", "if [ \"$python_rc\" = 0 ]; then sudo caddy validate --config \"$next\" --adapter caddyfile >/tmp/web-probe-sentinel-caddy-validate.out 2>/tmp/web-probe-sentinel-caddy-validate.err; validate_rc=$?; fi", "if [ \"$validate_rc\" = 0 ]; then sudo install -m 0644 \"$next\" \"$config_path\" >/tmp/web-probe-sentinel-caddy-install.out 2>/tmp/web-probe-sentinel-caddy-install.err && (sudo systemctl reload \"$service\" >/tmp/web-probe-sentinel-caddy-reload.out 2>/tmp/web-probe-sentinel-caddy-reload.err || sudo systemctl restart \"$service\" >>/tmp/web-probe-sentinel-caddy-reload.out 2>>/tmp/web-probe-sentinel-caddy-reload.err); reload_rc=$?; fi", "after_present=no", "grep -Fq \"# BEGIN $marker\" \"$config_path\" 2>/dev/null && after_present=yes", "active=$(systemctl is-active \"$service\" 2>/dev/null || true)", "err=$(cat /tmp/web-probe-sentinel-caddy-python.err /tmp/web-probe-sentinel-caddy-validate.err /tmp/web-probe-sentinel-caddy-install.err /tmp/web-probe-sentinel-caddy-reload.err 2>/dev/null | tr '\\n' ';' | cut -c1-1000 || true)", "python3 - \"$python_rc\" \"$validate_rc\" \"$reload_rc\" \"$after_present\" \"$active\" \"$hostname\" \"$config_path\" \"$err\" <<'PY'", "import json, sys", "python_rc, validate_rc, reload_rc, after_present, active, hostname, config_path, error_preview = sys.argv[1:9]", "def num(value):", " if value == '':", " return None", " try:", " return int(value)", " except ValueError:", " return None", "payload = {", " 'ok': num(python_rc) == 0 and num(validate_rc) == 0 and num(reload_rc) == 0 and after_present == 'yes',", " 'hostname': hostname,", " 'configPath': config_path,", " 'pythonExitCode': num(python_rc),", " 'validateExitCode': num(validate_rc),", " 'reloadExitCode': num(reload_rc),", " 'afterBlockPresent': after_present == 'yes',", " 'active': active,", " 'errorPreview': error_preview,", " 'valuesRedacted': True,", "}", "print(json.dumps(payload, ensure_ascii=False))", "PY", ].join("\n"); const result = runCommand(["trans", stringAt(state.publicExposure, "caddy.route"), "sh", "--", script], repoRoot, { timeoutMs: Math.min(timeoutSeconds, 60) * 1000 }); const parsed = parseJsonObject(result.stdout); return { ok: result.exitCode === 0 && parsed?.ok === true, ...record(parsed), result: compactCommand(result), valuesRedacted: true }; } function readAnalysisSummaryFromWorkspace(state: SentinelCicdState, stateDir: string, timeoutSeconds: number): Record { if (!isSafeRelativeStateDir(stateDir)) return { ok: false, reason: "unsafe-state-dir", stateDir, valuesRedacted: true }; const script = [ "set -eu", `state_dir=${shellQuote(stateDir)}`, "node - \"$state_dir\" <<'NODE'", "const fs=require('node:fs'); const path=require('node:path'); const crypto=require('node:crypto');", "const stateDir=process.argv[2]; const reportPath=path.join(stateDir,'analysis','report.json'); const reportMdPath=path.join(stateDir,'analysis','report.md');", "const read=(p)=>{try{return fs.readFileSync(p)}catch{return null}}; const jsonBuf=read(reportPath);", "const sha=(buf)=>buf?`sha256:${crypto.createHash('sha256').update(buf).digest('hex')}`:null;", "const rec=(v)=>v&&typeof v==='object'&&!Array.isArray(v)?v:{}; const arr=(v)=>Array.isArray(v)?v:[]; const clip=(v,n=180)=>v==null?null:String(v).slice(0,n);", "let report=null; try{report=jsonBuf?JSON.parse(jsonBuf.toString('utf8')):null}catch{}", "let artifactCount=0; let screenshot=null;", "function walk(dir){let entries=[]; try{entries=fs.readdirSync(dir,{withFileTypes:true})}catch{return}; for(const e of entries){const p=path.join(dir,e.name); if(e.isDirectory()) walk(p); else { artifactCount++; if(/\\.png$/i.test(e.name)){const b=read(p); screenshot={path:p,sha256:sha(b),bytes:b?b.length:0}; } } }}", "walk(stateDir);", "const findings=arr(report?.findings ?? report?.archiveSummary?.redFindings).slice(0,20).map((item)=>{const v=rec(item); return {id:clip(v.id??v.kind??v.code,80),kind:clip(v.kind??v.id??v.code,80),code:clip(v.code??v.kind??v.id,80),severity:clip(v.severity??v.level,32),level:clip(v.level??v.severity,32),count:Number(v.count??v.sampleCount??1),summary:clip(v.summary??v.message,220),message:clip(v.message??v.summary,220)};});", "const slow=arr(report?.pagePerformanceSlowApi ?? report?.archivePagePerformanceSlowApi).slice(0,8).map((item)=>{const v=rec(item); return {path:clip(v.path??v.route,120),sampleCount:v.sampleCount??null,p95Ms:v.p95Ms??null,maxMs:v.maxMs??null,overFiveSecondCount:v.overFiveSecondCount??null};});", "console.log(JSON.stringify({ok:!!report&&report.ok!==false,stateDir,reportJsonPath:reportPath,reportJsonSha256:sha(jsonBuf),reportMdPath,reportMdSha256:sha(read(reportMdPath)),findingCount:Number(report?.findingCount??findings.length),artifactCount,screenshot,findings,counts:rec(report?.counts),analysisWindow:rec(report?.analysisWindow??report?.windows?.recent?.summary),pagePerformanceSlowApi:slow,valuesRedacted:true}));", "NODE", ].join("\n"); const result = runCommand(["trans", `${state.spec.nodeId}:${state.spec.workspace}`, "sh"], repoRoot, { input: script, timeoutMs: Math.min(timeoutSeconds, 60) * 1000 }); const parsed = parseJsonObject(result.stdout); return { ok: result.exitCode === 0 && parsed?.ok === true, ...record(parsed), result: compactCommand(result), valuesRedacted: true }; } function collectObserveView(state: SentinelCicdState, observerId: string, view: "turn-summary" | "trace-frame", turn: number | null, timeoutSeconds: number): Record { const args = ["web-probe", "observe", "collect", observerId, "--node", state.spec.nodeId, "--lane", state.spec.lane, "--view", view, "--command-timeout-seconds", String(Math.max(5, Math.min(timeoutSeconds, 55))), "--raw", "--compact-raw"]; if (turn !== null) args.push("--turn", String(turn)); const result = runChildCli(args, timeoutSeconds); const payload = cliDataPayload(result.parsed); const collect = record(payload.collect); return { ok: result.ok && result.parsed !== null && payload.ok !== false && collect.ok !== false, view, renderedText: typeof collect.renderedText === "string" ? collect.renderedText : typeof payload.renderedText === "string" ? payload.renderedText : String(record(result.result).stdoutTail ?? record(result.result).stdoutPreview ?? ""), collect, payload, result: result.result, valuesRedacted: true, }; } function runChildCli(args: string[], timeoutSeconds: number, input?: string): ChildCliResult { const result = runCommand(["bun", "scripts/cli.ts", ...args], repoRoot, { input, timeoutMs: Math.max(5, Math.min(timeoutSeconds, 120)) * 1000 }); return { ok: result.exitCode === 0 && !result.timedOut, parsed: parseJsonObject(result.stdout), result: compactCommandWithTail(result), }; } function waitForQuickVerifyPromptTurn(state: SentinelCicdState, observerId: string, promptIndex: number, deadline: number, pollIntervalMs: number): Record { const observations: Record[] = []; const indexEntry = readLocalObserveIndex(observerId); if (indexEntry === null) { return { ok: false, failure: "observe-index-entry-missing", round: promptIndex, observerId, valuesRedacted: true, }; } const pollSleepMs = Math.max(250, Math.min(5000, Math.trunc(pollIntervalMs))); while (Date.now() < deadline) { const waitMs = Math.max(1000, Math.min(55_000, deadline - Date.now())); const script = quickVerifyPromptWaitScript(indexEntry.stateDir, promptIndex, waitMs, pollSleepMs); const result = runCommand(["trans", `${state.spec.nodeId}:${state.spec.workspace}`, "sh"], repoRoot, { input: script, timeoutMs: waitMs + 5000 }); const payload = parseJsonObject(result.stdout); if (Array.isArray(payload?.observations)) observations.push(...payload.observations.map(record)); const status = typeof payload?.status === "string" ? payload.status : null; const terminalPayload = { round: promptIndex, status, traceId: payload?.traceId ?? null, finalResponseEmpty: payload?.finalResponseEmpty === true, composerReadyForTurn: payload?.composerReadyForTurn === true, composerAction: typeof payload?.composerAction === "string" ? payload.composerAction : null, observations: observations.slice(-6), waitResult: compactCommand(result), valuesRedacted: true, }; if (result.exitCode !== 0 || payload === null || payload.ok === false && payload.failure !== "quick-verify-wait-chunk-timeout") { return { ok: false, failure: text(payload?.failure ?? "quick-verify-artifact-wait-failed"), ...terminalPayload, }; } if (payload.ok === true) return { ok: true, ...terminalPayload }; if (isQuickVerifyTurnTerminal(status)) { return { ok: false, failure: "observe-turn-terminal-non-success", ...terminalPayload, }; } } return { ok: false, failure: "quick-verify-timeout-over-120s", round: promptIndex, observations: observations.slice(-6), warnings: ["quick verify exceeded the configured 120s targetValidation budget while waiting for a submitted turn to become terminal; investigate env-reuse/git mirror/source build path before retrying."], valuesRedacted: true, }; } function quickVerifyPromptWaitScript(stateDir: string, promptIndex: number, timeoutMs: number, pollSleepMs: number): string { return [ "set -eu", `state_dir=${shellQuote(stateDir)}`, `prompt_index=${shellQuote(String(promptIndex))}`, `timeout_ms=${shellQuote(String(Math.max(1, Math.trunc(timeoutMs))))}`, `poll_ms=${shellQuote(String(Math.max(250, Math.trunc(pollSleepMs))))}`, "test -d \"$state_dir\" || { printf '{\"ok\":false,\"failure\":\"state-dir-missing\",\"stateDir\":\"%s\",\"valuesRedacted\":true}\\n' \"$state_dir\"; exit 0; }", "node - \"$state_dir\" \"$prompt_index\" \"$timeout_ms\" \"$poll_ms\" <<'NODE'", "const fs = require('node:fs');", "const path = require('node:path');", "const dir = process.argv[2];", "const promptIndex = Number(process.argv[3]);", "const timeoutMs = Number(process.argv[4]);", "const pollMs = Number(process.argv[5]);", "const startedAt = Date.now();", "const short = (value, limit = 160) => String(value || '').replace(/\\s+/gu, ' ').trim().slice(0, limit);", "const textOf = (value) => String(value?.text || value?.textPreview || value?.preview || '');", "const arr = (value) => Array.isArray(value) ? value : [];", "const unique = (values) => Array.from(new Set(values.filter(Boolean)));", "const numOrNull = (value) => { const n = Number(value); return Number.isFinite(n) ? n : null; };", "const tsMs = (value) => { const ms = Date.parse(String(value || '')); return Number.isFinite(ms) ? ms : null; };", "const readJson = (rel) => { try { return JSON.parse(fs.readFileSync(path.join(dir, rel), 'utf8')); } catch { return null; } };", "const readJsonl = (rel) => { try { return fs.readFileSync(path.join(dir, rel), 'utf8').split(/\\r?\\n/u).filter(Boolean).map((line) => { try { return JSON.parse(line); } catch { return null; } }).filter(Boolean); } catch { return []; } };", "const readDone = (id) => id ? readJson(path.join('commands', 'done', `${id}.json`)) : null;", "function sessionIdFromUrl(value) { const match = String(value || '').match(/\\/workbench\\/sessions\\/(ses_[A-Za-z0-9_-]+)/u); return match ? match[1] : null; }", "function commandSessionId(item) { const done = readDone(item?.commandId); return item?.sessionId || item?.detail?.sessionId || item?.input?.sessionId || item?.result?.sessionId || done?.result?.sessionId || done?.result?.observer?.sessionId || sessionIdFromUrl(item?.afterUrl) || sessionIdFromUrl(item?.detail?.afterUrl) || sessionIdFromUrl(done?.result?.afterUrl) || null; }", "function firstTraceId(value) { const match = String(value || '').match(/\\btrc_[A-Za-z0-9_-]+\\b/u); return match ? match[0] : null; }", "function commandTraceId(item) { const done = readDone(item?.commandId); return item?.traceId || item?.detail?.chatSubmit?.traceId || item?.detail?.traceId || item?.input?.traceId || item?.result?.chatSubmit?.traceId || done?.result?.chatSubmit?.traceId || done?.result?.traceId || null; }", "function itemTraceId(item) { return item?.traceId || firstTraceId(textOf(item)) || null; }", "function completedNewSessionIdsBefore(control, ts) { const limit = tsMs(ts); return control.filter((item) => item.type === 'newSession' && item.phase === 'completed').filter((item) => limit === null || tsMs(item.ts) === null || tsMs(item.ts) <= limit).map(commandSessionId).filter(Boolean); }", "function authoritativeSessionIdForPrompts(control, prompts) { const ids = completedNewSessionIdsBefore(control, prompts[0]?.firstTs || null); return ids.slice(-1)[0] || unique(prompts.map((item) => item.sessionId))[0] || null; }", "function promptCommands(control) {", " const map = new Map();", " for (const item of control) {", " if (item.type !== 'sendPrompt' || !['started', 'completed', 'failed'].includes(item.phase)) continue;", " const id = item.commandId || item.seq || String(map.size + 1);", " const existing = map.get(id) || {};", " map.set(id, { ...existing, ...item, input: { ...(existing.input || {}), ...(item.input || {}) }, sessionId: existing.sessionId || commandSessionId(item), traceId: existing.traceId || commandTraceId(item), firstTs: existing.firstTs || item.ts, lastTs: item.ts });", " }", " const prompts = Array.from(map.values()).filter((item) => tsMs(item.firstTs) !== null).sort((a, b) => tsMs(a.firstTs) - tsMs(b.firstTs));", " const sessionId = authoritativeSessionIdForPrompts(control, prompts);", " if (!sessionId) return prompts;", " const scoped = prompts.filter((item) => item.sessionId === sessionId);", " return scoped.length > 0 ? scoped : prompts;", "}", "function segmentFor(samples, prompts, index) { const start = tsMs(prompts[index]?.firstTs); const end = index + 1 < prompts.length ? tsMs(prompts[index + 1].firstTs) : Infinity; return samples.filter((sample) => { const ms = tsMs(sample.ts); return ms !== null && ms >= start && ms < end; }); }", "function entryGroups(sample) { return [...arr(sample.turns).map((item) => ({ group: 'turn', item })), ...arr(sample.traceRows).map((item) => ({ group: 'traceRow', item })), ...arr(sample.messages).map((item) => ({ group: 'message', item }))]; }", "function traceIdsFromSamples(items) { const ids = []; for (const sample of items) for (const entry of entryGroups(sample)) { const id = itemTraceId(entry.item); if (id) ids.push(id); } return unique(ids); }", "function chooseTraceId(segment, prompt) { const promptTraceId = commandTraceId(prompt) || prompt?.traceId || null; const ids = traceIdsFromSamples(segment); if (promptTraceId && (ids.length === 0 || ids.includes(promptTraceId))) return promptTraceId; return ids.slice(-1)[0] || promptTraceId || null; }", "function traceEntries(items, traceId) { const entries = []; for (const sample of items) for (const entry of entryGroups(sample)) { const text = textOf(entry.item); const id = itemTraceId(entry.item); if (!traceId || id === traceId || text.includes(traceId)) entries.push({ ...entry, sample, text }); } return entries; }", "function normalizeLifecycleStatus(value) { const raw = String(value || '').trim().toLowerCase(); if (/^(canceled|cancelled)$/u.test(raw)) return 'canceled'; if (/^(failed|failure|error)$/u.test(raw)) return 'failed'; if (/^(completed|complete|succeeded|success|terminal)$/u.test(raw)) return 'completed'; if (/^(running|admitting|queued|pending|in_progress|in-progress)$/u.test(raw)) return 'running'; return null; }", "function statusFor(items, traceId) { const entries = traceId ? traceEntries(items, traceId) : items.flatMap((sample) => entryGroups(sample).map((entry) => ({ ...entry, sample, text: textOf(entry.item) }))); const lastTurn = entries.filter((entry) => entry.group === 'turn').slice(-1)[0]?.item || null; const turnStatus = normalizeLifecycleStatus(lastTurn?.status); if (turnStatus) return turnStatus; const lastMessage = entries.filter((entry) => entry.group === 'message').slice(-1)[0]?.item || null; return normalizeLifecycleStatus(lastMessage?.status) || 'unknown'; }", "function cleanFinalResponseText(value) { const raw = String(value || '').trim(); if (!raw) return ''; if (/^(completed|failed|canceled|cancelled|轮次完成|轮次失败|轮次取消|已记录)$/iu.test(raw.replace(/\\s+/gu, ' '))) return ''; if (/^(admitted|run|ok|error)\\s+/iu.test(raw)) return ''; return raw; }", "function finalResponseEmpty(items, traceId) { if (!/^(completed|failed|canceled)$/u.test(statusFor(items, traceId))) return true; const entries = (traceId ? traceEntries(items, traceId) : items.flatMap((sample) => entryGroups(sample).map((entry) => ({ ...entry, sample, text: textOf(entry.item) })))).slice().reverse(); for (const entry of entries) { const role = String(entry.item?.role || entry.item?.dataRole || entry.item?.messageRole || '').toLowerCase(); if (entry.group === 'message' && role && !/assistant|agent|system/u.test(role)) continue; const text = cleanFinalResponseText(entry.item?.finalResponse?.text || entry.item?.finalResponse?.preview || entry.text); if (text && !/^Code Agent\\s*耗时/iu.test(text)) return false; } return true; }", "function rowFor() {", " const control = readJsonl('control.jsonl');", " const samples = readJsonl('samples.jsonl');", " const prompts = promptCommands(control);", " const prompt = prompts[promptIndex - 1] || null;", " if (!prompt) return { ok: true, round: promptIndex, status: null, traceId: null, finalResponseEmpty: true, lastSeq: null, lastTs: null, valuesRedacted: true };", " const segment = segmentFor(samples, prompts, promptIndex - 1);", " const traceId = chooseTraceId(segment, prompt);", " const status = statusFor(segment, traceId);", " const sampleForTrace = traceId ? segment.filter((sample) => traceIdsFromSamples([sample]).includes(traceId)).slice(-1)[0] || null : null;", " const lastSample = sampleForTrace || segment.slice(-1)[0] || null;", " const composerSample = segment.filter((sample) => sample.pageRole === 'control').slice(-1)[0] || lastSample;", " const composer = composerSample?.composer || {};", " const composerReadyForTurn = composer.inputPresent === true && composer.inputDisabled !== true && composer.submitPresent === true && composer.warningPresent !== true && composer.submitAction === 'turn';", " return { ok: true, round: promptIndex, status, traceId, finalResponseEmpty: finalResponseEmpty(segment, traceId), lastSeq: numOrNull(lastSample?.seq), lastTs: lastSample?.ts || null, composerReadyForTurn, composerAction: composer.submitAction || null, source: 'observe-artifact-wait-script', valuesRedacted: true };", "}", "function norm(value) { return String(value || '').trim().toLowerCase().replace(/_/gu, '-'); }", "function successful(value) { return ['completed', 'succeeded', 'success'].includes(norm(value)); }", "function terminal(value) { return ['completed', 'succeeded', 'success', 'failed', 'error', 'blocked', 'timeout', 'canceled', 'cancelled', 'terminal'].includes(norm(value)); }", "const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));", "(async () => {", " const observations = [];", " while (Date.now() - startedAt <= timeoutMs) {", " const row = rowFor();", " observations.push(row);", " if (successful(row.status) && row.composerReadyForTurn === true) { console.log(JSON.stringify({ ok: true, ...row, observations: observations.slice(-6), elapsedMs: Date.now() - startedAt, valuesRedacted: true })); return; }", " if (!successful(row.status) && terminal(row.status)) { console.log(JSON.stringify({ ok: false, failure: 'observe-turn-terminal-non-success', ...row, observations: observations.slice(-6), elapsedMs: Date.now() - startedAt, valuesRedacted: true })); return; }", " await sleep(Math.min(pollMs, Math.max(0, timeoutMs - (Date.now() - startedAt))));", " }", " const row = rowFor();", " observations.push(row);", " console.log(JSON.stringify({ ok: false, failure: 'quick-verify-wait-chunk-timeout', ...row, observations: observations.slice(-6), elapsedMs: Date.now() - startedAt, valuesRedacted: true }));", "})().catch((error) => { console.log(JSON.stringify({ ok: false, failure: 'quick-verify-artifact-wait-script-error', error: error instanceof Error ? error.message : String(error), valuesRedacted: true })); });", "NODE", ].join("\n"); } function isQuickVerifyTurnSuccessful(value: string | null): boolean { const status = normalizeQuickVerifyStatus(value); return status === "completed" || status === "succeeded" || status === "success"; } function isQuickVerifyTurnTerminal(value: string | null): boolean { const status = normalizeQuickVerifyStatus(value); return status === "completed" || status === "succeeded" || status === "success" || status === "failed" || status === "error" || status === "blocked" || status === "timeout" || status === "canceled" || status === "cancelled" || status === "terminal"; } function normalizeQuickVerifyStatus(value: string | null): string { return String(value ?? "").trim().toLowerCase().replace(/_/gu, "-"); } function cliDataPayload(parsed: Record | null): Record { const root = record(parsed); return isRecord(root.data) ? root.data : root; } function findScenario(state: SentinelCicdState, scenarioId: string): Record | null { const sentinel = state.spec.observability.webProbe?.sentinel; if (sentinel === undefined) return null; const scenarios = readConfigRefTarget(sentinel.configRefs.scenarios); if (!Array.isArray(scenarios)) return null; return scenarios.map(record).find((item) => item.id === scenarioId) ?? null; } function readPromptSetForScenario(scenario: Record): { ok: true; prompts: string[]; summary: Record } | { ok: false; error: string; summary: Record } { const promptSet = recordTarget(readConfigRefTarget(stringAt(scenario, "promptSetRef")), stringAt(scenario, "promptSetRef")); const sourceRef = stringAt(promptSet, "promptSourceRef"); const key = stringAt(promptSet, "promptSourceKey"); const paths = secretSourcePaths(sourceRef); const sourcePath = paths.find((item) => existsSync(item)) ?? paths[0] ?? join(repoRoot, ".state", "secrets", sourceRef); const summary = { sourceRef, sourceKey: key, sourcePath: displayPath(sourcePath), valuesRedacted: true }; if (!existsSync(sourcePath)) return { ok: false, error: "prompt-source-missing", summary }; const values = parseEnvFile(readFileSync(sourcePath, "utf8")); const raw = values[key]; if (raw === undefined || raw.length === 0) return { ok: false, error: "prompt-key-missing", summary }; const parsed = parsePromptJson(raw); if (parsed.length === 0) return { ok: false, error: "prompt-json-empty", summary }; return { ok: true, prompts: parsed, summary: { ...summary, promptCount: parsed.length, promptTextHashes: parsed.map((item) => `sha256:${createHash("sha256").update(item).digest("hex").slice(0, 16)}`), promptTextBytes: parsed.map((item) => Buffer.byteLength(item)), valuesRedacted: true, }, }; } function parsePromptJson(raw: string): string[] { try { const parsed = JSON.parse(raw) as unknown; if (Array.isArray(parsed)) return parsed.filter((item): item is string => typeof item === "string" && item.length > 0); const recordValue = record(parsed); if (Array.isArray(recordValue.prompts)) return recordValue.prompts.filter((item): item is string => typeof item === "string" && item.length > 0); if (typeof recordValue.prompt === "string" && recordValue.prompt.length > 0) return [recordValue.prompt]; } catch { if (raw.trim().length > 0) return [raw]; } return []; } function readLocalObserveIndex(observerId: string): { stateDir: string } | null { const path = rootPath(".state/web-observe/index.json"); if (!existsSync(path)) return null; const parsed = parseJsonObject(readFileSync(path, "utf8")); const entry = record(parsed?.[observerId]); const stateDir = typeof entry.stateDir === "string" ? entry.stateDir : null; return stateDir === null ? null : { stateDir }; } function secretSourcePaths(sourceRef: string): string[] { const paths = [join(repoRoot, ".state", "secrets", sourceRef)]; const marker = "/.worktree/"; const index = repoRoot.indexOf(marker); if (index >= 0) paths.push(join(repoRoot.slice(0, index), ".state", "secrets", sourceRef)); return [...new Set(paths)]; } function parseEnvFile(textValue: string): Record { const values: Record = {}; for (const rawLine of textValue.split(/\r?\n/u)) { const line = rawLine.trim(); if (line.length === 0 || line.startsWith("#")) continue; const index = line.indexOf("="); if (index <= 0) continue; const key = line.slice(0, index).trim(); let value = line.slice(index + 1).trim(); if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) value = value.slice(1, -1); values[key] = value; } return values; } function observerIdFromText(textValue: string): string | null { return /\bwebobs-[a-z0-9-]+\b/iu.exec(textValue)?.[0] ?? null; } function remainingSeconds(deadline: number, cap: number): number { return Math.max(5, Math.min(cap, Math.ceil((deadline - Date.now()) / 1000))); } function metricNames(textValue: unknown): string[] { if (typeof textValue !== "string") return []; return textValue.split(/\r?\n/u).map((line) => /^([A-Za-z_:][A-Za-z0-9_:]*)/u.exec(line)?.[1]).filter((item): item is string => typeof item === "string"); } function validationBlocker(health: Record, metrics: Record, report: Record, publicExposure: Record, quickVerify: Record | null): Record { const blockers = []; if (!health.ok || record(health.bodyJson).ok !== true) blockers.push("health"); if (!metrics.ok || !metricNames(metrics.bodyTextPreview).includes("web_probe_sentinel_health")) blockers.push("metrics"); if (!report.ok) blockers.push("recent-report"); if (publicExposure.ok !== true) blockers.push("public-exposure"); if (quickVerify !== null && quickVerify.ok !== true) blockers.push("quick-verify"); return { code: "sentinel-validation-failed", blockers, valuesRedacted: true }; } function serviceUnavailableBlocker(state: SentinelCicdState): Record { return { code: "sentinel-service-unavailable", policy: stringAt(state.cicd, "targetValidation.serviceUnavailablePolicy"), reason: "sentinel service must be reachable through k3s internal Service DNS before quick verify can run; no public/fallback path is used.", retry: `bun scripts/cli.ts web-probe sentinel validate --node ${state.spec.nodeId} --lane ${state.spec.lane}`, valuesRedacted: true, }; } function sentinelP5Next(state: SentinelCicdState): Record { const node = state.spec.nodeId; const lane = state.spec.lane; return { validate: `bun scripts/cli.ts web-probe sentinel validate --node ${node} --lane ${lane}`, quickVerify: `bun scripts/cli.ts web-probe sentinel validate --node ${node} --lane ${lane} --quick-verify --confirm --wait`, maintenanceStart: `bun scripts/cli.ts web-probe sentinel maintenance start --node ${node} --lane ${lane} --confirm --wait`, maintenanceStop: `bun scripts/cli.ts web-probe sentinel maintenance stop --node ${node} --lane ${lane} --confirm --wait`, report: `bun scripts/cli.ts web-probe sentinel report --node ${node} --lane ${lane} --view summary`, }; } function isSafeRelativeStateDir(value: string): boolean { return value.startsWith(".state/web-observe/") && !value.includes("\0") && !value.includes(".."); } function stringAtNullable(value: unknown, path: string): string | null { const found = valueAtPath(value, path); return typeof found === "string" && found.length > 0 ? found : null; } function numberAtNullable(value: unknown, path: string): number | null { const found = valueAtPath(value, path); return typeof found === "number" && Number.isFinite(found) ? found : null; } function displayPath(pathValue: string): string { if (pathValue.startsWith(`${repoRoot}/`)) return pathValue.slice(repoRoot.length + 1); const marker = "/.worktree/"; const index = repoRoot.indexOf(marker); if (index >= 0) { const mainRoot = repoRoot.slice(0, index); if (pathValue.startsWith(`${mainRoot}/`)) return pathValue.slice(mainRoot.length + 1); } return pathValue; } function compactCommandWithTail(result: CommandResult): CompactCommandResult & { stdoutTail: string; stderrTail: string } { return { ...compactCommand(result), stdoutPreview: result.stdout.trim().slice(0, 1200), stderrPreview: result.stderr.trim().slice(0, 1200), stdoutTail: result.stdout.trim().slice(-4000), stderrTail: result.stderr.trim().slice(-4000), }; } function renderQuickVerifySummary(input: Record): string { const artifact = record(input.artifactSummary); const findings = Array.isArray(artifact.findings) ? artifact.findings.map(record).slice(0, 8) : []; return [ "Web Probe Sentinel Quick Verify", "=======================================================", `run=${input.runId ?? "-"} scenario=${input.scenarioId ?? "-"} observer=${input.observerId ?? "-"}`, `report=${artifact.reportJsonSha256 ?? "-"} artifacts=${artifact.artifactCount ?? "-"} findings=${artifact.findingCount ?? findings.length}`, `publicOrigin=${input.publicOrigin ?? "-"}`, "", "Findings", findings.length === 0 ? "-" : findings.map((item) => `${item.severity ?? item.level ?? "-"} ${item.kind ?? item.id ?? item.code ?? "-"} count=${item.count ?? "-"} ${item.summary ?? item.message ?? ""}`).join("\n"), ].join("\n"); } function renderMaintenanceResult(result: Record): string { const serviceHealth = record(result.serviceHealth); const maintenance = record(result.maintenance); const quickVerify = record(result.quickVerify); const planned = record(result.planned); const blocker = record(result.blocker); const next = record(result.next); const maintenanceBody = record(maintenance.bodyJson); const state = record(maintenanceBody.maintenance); return [ String(result.command), "", table(["NODE", "LANE", "STATUS", "MODE", "MUTATION"], [[result.node, result.lane, result.ok === true ? "ok" : "blocked", result.mode ?? "status", result.mutation ?? false]]), "", table(["SERVICE", "HTTP", "INTERNAL_URL"], [[serviceHealth.ok, serviceHealth.httpStatus, serviceHealth.internalUrl]]), "", Object.keys(state).length > 0 ? table(["ACTIVE", "RELEASE", "STARTED", "STOPPED", "VERIFY_RUN"], [[state.active, state.releaseId, state.startedAt, state.stoppedAt, state.quickVerifyPlannedRunId]]) : Object.keys(planned).length > 0 ? table(["ACTION", "RELEASE", "REASON", "QUICK_VERIFY"], [[planned.action, planned.releaseId, planned.reason, planned.quickVerify]]) : "MAINTENANCE\n-", "", Object.keys(quickVerify).length === 0 ? "QUICK_VERIFY\n-" : table(["OK", "RUN", "SCENARIO", "OBSERVER", "REPORT", "FINDINGS"], [[quickVerify.ok, quickVerify.runId, quickVerify.scenarioId, quickVerify.observerId, quickVerify.reportJsonSha256, quickVerify.findingCount]]), "", Object.keys(blocker).length === 0 ? "BLOCKER\n-" : table(["CODE", "REASON"], [[blocker.code, blocker.reason]]), "", "NEXT", ` validate: ${next.validate ?? "-"}`, ` report: ${next.report ?? "-"}`, ` maintenance-stop: ${next.maintenanceStop ?? "-"}`, "", "DISCLOSURE", " maintenance uses the k3s internal sentinel Service DNS; no public fallback or second runner is used.", ].join("\n"); } function renderValidateResult(result: Record): string { const health = record(result.serviceHealth); const metrics = record(result.metrics); const report = record(result.report); const publicExposure = record(result.publicExposure); const quickVerify = record(result.quickVerify); const blocker = record(result.blocker); const next = record(result.next); const warnings = Array.isArray(quickVerify.warnings) ? quickVerify.warnings : []; return [ String(result.command), "", table(["NODE", "LANE", "STATUS", "MODE"], [[result.node, result.lane, result.ok === true ? "ok" : "blocked", result.mode ?? "status"]]), "", table(["CHECK", "OK", "DETAIL"], [ ["health", health.ok, `${health.httpStatus ?? "-"} ${short(health.internalUrl)}`], ["metrics", metrics.ok && metricNames(metrics.bodyTextPreview).includes("web_probe_sentinel_health"), `bytes=${metrics.bodyBytes ?? "-"} metric=web_probe_sentinel_health`], ["recent-report", report.ok, `${record(record(report.bodyJson).run).id ?? "-"} ${short(record(record(report.bodyJson).run).report_json_sha256)}`], ["public-exposure", publicExposure.ok, `${record(publicExposure.dns).expectedA ?? "-"} http=${record(publicExposure.https).httpStatus ?? "-"}`], ["quick-verify", Object.keys(quickVerify).length === 0 ? "skipped" : quickVerify.ok, `${quickVerify.runId ?? "-"} ${short(quickVerify.reportJsonSha256)}`], ]), "", warnings.length === 0 ? "WARNINGS\n-" : ["WARNINGS", ...warnings.map((item) => `- ${text(item)}`)].join("\n"), "", Object.keys(blocker).length === 0 ? "BLOCKER\n-" : table(["CODE", "BLOCKERS"], [[blocker.code, Array.isArray(blocker.blockers) ? blocker.blockers.join(",") : blocker.reason]]), "", "NEXT", ` quick-verify: ${next.quickVerify ?? "-"}`, ` report: ${next.report ?? "-"}`, ` maintenance-start: ${next.maintenanceStart ?? "-"}`, "", "DISCLOSURE", " validate checks /api/health, /metrics, indexed analyze report and publicExposure without printing tokens.", ].join("\n"); } function renderReportResult(result: Record): string { const report = record(result.report); const body = record(report.bodyJson); const run = record(body.run); return [ String(result.command), "", table(["NODE", "LANE", "STATUS", "VIEW", "RUN"], [[result.node, result.lane, report.ok ? "ok" : "blocked", body.view ?? "-", run.id ?? "-"]]), "", table(["HTTP", "ERROR", "REPORT"], [[report.httpStatus, body.error ?? report.error ?? "-", short(run.report_json_sha256)]]), "", "DISCLOSURE", " report reads sentinel indexed analyze summaries/views only; it does not resample, rerun analyze, or read Workbench.", ].join("\n"); } function sentinelPipelineRunName(state: SentinelCicdState): string { const commit = state.sourceHead.commit ?? "source"; return `hwlab-web-probe-sentinel-${commit.slice(0, 12)}`; } function renderImageResult(result: Record): string { const source = record(result.source); const image = record(result.image); const registry = record(result.registry); const publish = record(result.publish); const blocker = record(result.blocker); const next = record(result.next); const warnings = Array.isArray(result.warnings) ? result.warnings : []; return [ String(result.command), "", table(["NODE", "LANE", "STATUS", "MODE", "MUTATION"], [[result.node, result.lane, result.ok === true ? "ok" : "blocked", result.mode, result.mutation]]), "", table(["SOURCE_REPO", "BRANCH", "COMMIT", "LOCAL_HEAD"], [[source.repository, source.branch, short(source.commit), short(source.localHead)]]), "", table(["IMAGE", "BASE", "ENTRYPOINT", "DOCKERFILE"], [[image.ref, image.baseImage, image.entrypoint, short(image.dockerfileSha256)]]), "", Object.keys(registry).length === 0 ? "REGISTRY\n-" : table(["PROBED", "PRESENT", "DIGEST"], [[record(registry.probe).url ?? "-", record(registry.probe).present ?? "-", short(record(registry.probe).digest)]]), "", Object.keys(publish).length === 0 ? "PUBLISH\n-" : table(["OK", "PHASE", "JOB", "DIGEST", "GITOPS"], [[publish.ok, publish.phase, publish.jobName, short(record(publish.payload).digestRef), short(record(publish.payload).gitopsCommit)]]), "", warnings.length === 0 ? "WARNINGS\n-" : ["WARNINGS", ...warnings.map((item) => `- ${text(item)}`)].join("\n"), "", Object.keys(blocker).length === 0 ? "BLOCKER\n-" : table(["CODE", "REASON"], [[blocker.code, blocker.reason]]), "", "NEXT", ` status: ${next.status ?? "-"}`, ` dry-run: ${next.dryRun ?? "-"}`, ` confirm: ${next.confirm ?? "-"}`, ` trigger: ${next.controlPlaneTrigger ?? "-"}`, ` control-plane: ${next.controlPlanePlan ?? "-"}`, "", "DISCLOSURE", " valuesRedacted=true; image status shows refs, hashes and object names only.", ].join("\n"); } function renderControlPlaneResult(result: Record): string { const source = record(result.source); const image = record(result.image); const gitops = record(result.gitops); const argo = record(result.argo); const validation = record(result.validation); const observed = record(result.observed); const publish = record(result.publish); const flush = record(result.flush); const publicExposureApply = record(result.publicExposureApply); const publicExposureCaddy = record(publicExposureApply.caddy); const argoApply = record(result.argoApply); const blocker = record(result.blocker); const targetValidation = record(result.targetValidation); const next = record(result.next); const warnings = Array.isArray(result.warnings) ? result.warnings : []; return [ String(result.command), "", table(["NODE", "LANE", "STATUS", "MODE", "PIPELINERUN"], [[result.node, result.lane, result.ok === true ? "ok" : "blocked", result.mode, result.pipelineRun]]), "", table(["SOURCE", "COMMIT", "IMAGE", "MANIFEST"], [[`${source.repository}@${source.branch}`, short(source.commit), image.ref, short(gitops.manifestSha256)]]), "", table(["GITOPS_PATH", "ARGO_APP", "TARGET_REV", "OBJECTS"], [[gitops.path, argo.applicationName, gitops.targetRevision, gitops.manifestObjects]]), "", table(["SCENARIO", "MAX_SECONDS", "SECOND_PATH"], [[validation.scenarioId, validation.maxSeconds, validation.automaticSecondPath]]), "", renderObservedStatus(observed), "", Object.keys(targetValidation).length === 0 ? "TARGET_VALIDATION\n-" : table(["OK", "STATUS", "SCENARIO", "RUN", "OBSERVER", "REPORT", "FINDINGS", "ARTIFACTS"], [[ targetValidation.ok, targetValidation.status, targetValidation.scenarioId, targetValidation.runId, targetValidation.observerId, short(targetValidation.reportJsonSha256), targetValidation.findingCount, targetValidation.artifactCount, ]]), "", Object.keys(publish).length === 0 ? "PUBLISH\n-" : table(["OK", "PHASE", "JOB", "DIGEST", "GITOPS"], [[publish.ok, publish.phase, publish.jobName, short(record(publish.payload).digestRef), short(record(publish.payload).gitopsCommit)]]), "", Object.keys(flush).length === 0 ? "FLUSH\n-" : table(["OK", "EXIT", "TIMED_OUT", "PREVIEW"], [[flush.ok, record(flush.result).exitCode, record(flush.result).timedOut, record(flush.result).stdoutPreview]]), "", Object.keys(publicExposureApply).length === 0 ? "PUBLIC_EXPOSURE_APPLY\n-" : table(["OK", "SECRET", "CADDY", "HOST"], [[publicExposureApply.ok, record(publicExposureApply.secret).ok, record(publicExposureApply.caddy).ok, publicExposureApply.hostname]]), "", Object.keys(publicExposureCaddy).length === 0 || publicExposureCaddy.ok === true ? "CADDY_APPLY_DETAIL\n-" : table(["PY", "VALIDATE", "RELOAD", "BLOCK", "ACTIVE", "ERROR", "STDOUT", "STDERR"], [[publicExposureCaddy.pythonExitCode, publicExposureCaddy.validateExitCode, publicExposureCaddy.reloadExitCode, publicExposureCaddy.afterBlockPresent, publicExposureCaddy.active, short(publicExposureCaddy.errorPreview), short(record(publicExposureCaddy.result).stdoutPreview), short(record(publicExposureCaddy.result).stderrPreview)]]), "", Object.keys(argoApply).length === 0 ? "ARGO_APPLY\n-" : table(["OK", "EXIT", "PREVIEW"], [[argoApply.ok, record(argoApply.result).exitCode, record(argoApply.result).stdoutPreview]]), "", warnings.length === 0 ? "WARNINGS\n-" : ["WARNINGS", ...warnings.map((item) => `- ${text(item)}`)].join("\n"), "", Object.keys(blocker).length === 0 ? "BLOCKER\n-" : table(["CODE", "REASON"], [[blocker.code, blocker.reason]]), "", "NEXT", ` plan: ${next.plan ?? "-"}`, ` status: ${next.status ?? "-"}`, ` image: ${next.image ?? "-"}`, ` trigger-current: ${next.triggerCurrent ?? "-"}`, "", "DISCLOSURE", " default view is a bounded CI/CD summary; full manifest content is represented by object counts and sha256.", " sentinel unavailable policy is structured-failure; no automatic second execution path is rendered.", ].join("\n"); } function renderObservedStatus(observed: Record): string { const rows = [ observedStatusRow("source", observed.sourceMirror), observedStatusRow("registry", observed.registry), observedStatusRow("git-mirror", observed.gitMirror), observedStatusRow("gitops", observed.gitops), observedStatusRow("argo", observed.argo), observedStatusRow("runtime", observed.runtime), ].filter((row) => row !== null); if (rows.length === 0) return "OBSERVED\n-"; return table(["CHECK", "OK", "DETAIL", "EXIT", "TIMED_OUT", "PREVIEW"], rows); } function observedStatusRow(name: string, value: unknown): unknown[] | null { const item = record(value); if (Object.keys(item).length === 0) return null; const result = record(item.result); return [name, item.ok, observedDetail(name, item), result.exitCode, result.timedOut, result.stdoutPreview]; } function observedDetail(name: string, item: Record): string { if (name === "source") return `${record(item.probe).mode ?? "mirror"} ${short(record(item.probe).commit)}/${short(record(item.probe).expectedCommit)}`; if (name === "registry") return `${record(item.probe).present === true ? "present" : "missing"} ${short(record(item.probe).digest)}`; if (name === "gitops") return `${short(item.revision)} image=${short(item.image)}`; if (name === "argo") return `${item.syncStatus ?? "-"} ${item.healthStatus ?? "-"} ${short(item.revision)}/${short(item.expectedRevision)}`; if (name === "runtime") { const probe = record(item.probe); const deployment = record(probe.deployment); return `ready=${deployment.readyReplicas ?? "-"} image=${short(deployment.image)}/${short(deployment.expectedImage)}`; } return "-"; } function renderAsyncJobResult(result: Record): string { const job = record(result.job); const next = record(result.next); return [ String(result.command), "", table(["NODE", "LANE", "MODE", "MUTATION", "JOB"], [[result.node, result.lane, result.mode, result.mutation, job.id]]), "", table(["STATUS", "NAME", "CREATED"], [[job.status, job.name, job.createdAt]]), "", "NEXT", ` status: ${next.status ?? "-"}`, ` wait: ${next.wait ?? "-"}`, "", "DISCLOSURE", " confirmed operation is delegated to UniDesk job status to keep interactive calls bounded.", ].join("\n"); } function rendered(ok: boolean, command: string, text: string): RenderedCliResult { return { ok, command, renderedText: `${text.trimEnd()}\n`, contentType: "text/plain" }; } function readConfigRefTarget(ref: string): unknown { const file = configRefFile(ref); const path = configRefPath(ref); return valueAtPath(readConfigFile(file), path); } function readConfigFile(file: string): unknown { if (file.startsWith("/") || file.includes("..") || !file.startsWith("config/")) throw new Error(`unsafe configRef file: ${file}`); const abs = rootPath(file); if (!existsSync(abs)) throw new Error(`${file} does not exist`); return Bun.YAML.parse(readFileSync(abs, "utf8")) as unknown; } function configRefFile(ref: string): string { const [file, path, extra] = ref.split("#"); if (extra !== undefined || file === undefined || path === undefined || file.length === 0 || path.length === 0) throw new Error(`invalid configRef: ${ref}`); return file; } function configRefPath(ref: string): string { const [, path] = ref.split("#"); if (path === undefined || path.length === 0) throw new Error(`invalid configRef: ${ref}`); return path; } function valueAtPath(value: unknown, path: string): unknown { let current: unknown = value; for (const segment of path.split(".")) { const match = /^([A-Za-z0-9_-]+)?(?:\[(\d+)\])?$/u.exec(segment); if (match === null) return undefined; if (match[1] !== undefined) { if (!isRecord(current)) return undefined; current = current[match[1]]; } if (match[2] !== undefined) { if (!Array.isArray(current)) return undefined; current = current[Number(match[2])]; } } return current; } function stringAt(value: unknown, path: string): string { const found = valueAtPath(value, path); if (typeof found !== "string" || found.length === 0) throw new Error(`${path} must be a non-empty string`); return found; } function nonEmptyString(value: unknown): string | null { return typeof value === "string" && value.length > 0 ? value : null; } function stringField(value: Record, path: string): string { return stringAt(value, path); } function stringTarget(value: unknown, label: string): string { if (typeof value !== "string" || value.length === 0) throw new Error(`${label} must resolve to a non-empty string`); return value; } function numberAt(value: unknown, path: string): number { const found = valueAtPath(value, path); if (typeof found !== "number" || !Number.isFinite(found)) throw new Error(`${path} must be a number`); return found; } function arrayAt(value: unknown, path: string): unknown[] { const found = valueAtPath(value, path); if (!Array.isArray(found)) throw new Error(`${path} must be an array`); return found; } function recordTarget(value: unknown, label: string): Record { if (!isRecord(value)) throw new Error(`${label} must resolve to an object`); return value; } function record(value: unknown): Record { return isRecord(value) ? value : {}; } function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } function manifestObjectSummary(items: readonly Record[]): readonly Record[] { return items.map((item) => ({ kind: item.kind ?? null, name: record(item.metadata).name ?? null, namespace: record(item.metadata).namespace ?? null, })); } function compactCommand(result: CommandResult): CompactCommandResult { return { exitCode: result.exitCode, timedOut: result.timedOut, stdoutBytes: Buffer.byteLength(result.stdout), stderrBytes: Buffer.byteLength(result.stderr), stdoutPreview: result.stdout.trim().slice(0, 500), stderrPreview: result.stderr.trim().slice(0, 500), }; } function parseJsonObject(text: string): Record | null { const trimmed = text.trim(); if (trimmed.length === 0) return null; try { const parsed = JSON.parse(trimmed) as unknown; return isRecord(parsed) ? parsed : null; } catch { return null; } } function table(headers: string[], rows: unknown[][]): string { const normalized = [headers, ...rows.map((row) => row.map(text))]; const widths = headers.map((_, index) => Math.max(...normalized.map((row) => text(row[index] ?? "").length))); return normalized.map((row) => row.map((cell, index) => text(cell).padEnd(widths[index])).join(" ").trimEnd()).join("\n"); } function text(value: unknown): string { if (value === undefined || value === null || value === "") return "-"; if (typeof value === "boolean") return value ? "true" : "false"; return String(value).replace(/\s+/gu, " ").trim(); } function short(value: unknown): string { const raw = text(value); if (raw === "-") return raw; if (/^sha256:[0-9a-f]{64}$/iu.test(raw)) return `${raw.slice(0, 19)}...`; if (/^[0-9a-f]{40}$/iu.test(raw)) return raw.slice(0, 12); return raw.length > 42 ? `${raw.slice(0, 39)}...` : raw; } function sha256(textValue: string): string { return `sha256:${createHash("sha256").update(textValue).digest("hex")}`; } function shellQuote(value: string): string { return `'${value.replace(/'/gu, "'\\''")}'`; } function tomlEscape(value: string): string { return value.replace(/\\/gu, "\\\\").replace(/"/gu, '\\"'); }