/* RFC-0866: leitstand.certify command handler — produces GateDecisionV1 JSON via certification orchestration primitives. Does not perform deployment — that is the responsibility of deploy-execution.ts. Does not define the GateDecisionV1 schema — that lives in certification/contracts/decisions.ts. Does not register the command — that lives in leitstand.module.ts. RFC-1249: wire werkstatt.commands.validate into packages.check + retire CMD-OUTPUT debt RFC-1284: crash-safe access protection — suspend-before-measure via durable accessPinSuspended marker, stale-marker entry resume (flag=true), finally-restore, pinManagement {suspended,resumed,staleResumed,restoreError}, isProtectedLiveTarget gating, cache-clone marker store (Q-6), flag=false+suspended proceeds without resume (Q-7) RFC-1295 RFC-1325: provisional workers.dev as first-class site host Single host predicate module in werkstatt-shared; provisional registration derives dev/alt/main workers.dev channels from the account subdomain; first dev deploy auto-protects provisional hosts; provisional main responses carry noindex; dns-aid skips provisional sites; site.domain.adopt cuts over to a custom domain. RFC-1324: certify diagnostics surface the inner command summary — mission.check / editorial.review.verify failures name the real reason instead of a bare exit code RECOVERY-2026, REVIEW-CODE-2026, RFC-0866, RFC-0867, RFC-0929, RFC-0938, RFC-1097, RFC-1126, RFC-1149, RFC-1190, RFC-1207 */ import path from "node:path"; import type { KernelCommandInput, KernelRuntimeContext, KernelCommandResult, } from "@warpgogol/werkstatt-engine/kernel"; import { writeFileIfChanged } from "@warpgogol/werkstatt-engine/kernel"; import type { Sha256Digest } from "@warpgogol/werkstatt-shared/fingerprint"; import type { GateDecisionV1, MainVerificationDecisionV1, } from "../certification/contracts/decisions.ts"; import type { GateChannel } from "../certification/contracts/identifiers.ts"; import type { DeploymentOperationState } from "../certification/state-machine.ts"; import { astroCertificationProfile } from "../certification/profile/astro-profile.ts"; import { makeR2ConfigFromEnv, resolveArtifactHash, flagSite } from "./deploy-helpers.ts"; import { provisionCandidatePreview, type CandidatePreview, type CertifyTarget, } from "./candidate-preview.ts"; import { resolveCacheClonePath, readSystemState, readSystemConfigSmart, } from "../sternsystem/registry-io.ts"; import { cacheCloneCommit } from "../mission/mission-git-commit.ts"; import { gitExec } from "../werkstatt/git-exec.ts"; import { createR2StorageAdapter } from "../certification/storage/r2-adapter.ts"; import { planProducers, executeProducers, DEFAULT_PRODUCER_CONFIG, type ProducerDependencyNodeV1, type ProducerExecutionHandlerV1, type ProducerExecutionInputV1, } from "../certification/orchestration/orchestrator.ts"; import { evaluateCertificationDecision } from "../certification/aggregation.ts"; import type { EvidenceEnvelopeV1 } from "../certification/contracts/evidence.ts"; import { evidenceEnvelopeV1Schema } from "../certification/contracts/evidence.ts"; import type { ReleaseCandidateV1 } from "../certification/contracts/candidate.ts"; import type { CertificationPolicyBundleV1 } from "../certification/contracts/policy-bundle.ts"; import { gateDecisionV1Schema } from "../certification/contracts/decisions.ts"; import { createDefaultIO, type WorkspaceIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io"; import { isDevOrAltHost } from "@warpgogol/werkstatt-shared/middleware/site-host"; const PIN_DELETION_PROPAGATION_DELAY_MS = 5000; // 2026-10-04 recovery fix: a cold mission.check (110+ captures, fresh evidence // dir) takes ~7-11 minutes — the previous inner 5min race / outer 300s // orchestrator timeout always expired mid-capture, racing a "timed out" fail // decision against an "incomplete" no-decision return while the abandoned // in-process mission.check kept running. 25min covers worst-case capture; the // orchestrator timeout derives from it (+60s) so the inner race resolves first // and still writes a diagnostic-bearing fail decision rather than no decision. const MISSION_CHECK_TIMEOUT_MS = 25 * 60 * 1000; function flagString(input: KernelCommandInput, key: string): string | undefined { const v = input.flags[key]; return typeof v === "string" ? v : undefined; } function flagBoolean(input: KernelCommandInput, key: string): boolean { const v = input.flags[key]; return v === true || v === "true"; } interface EvidenceCacheEntry { artifactHash: Sha256Digest; evidence: EvidenceEnvelopeV1[]; producedAt: string; freshnessExpiresAt: string; sourceGate: GateChannel; } interface EvidenceCacheSidecar { schema: "werkstatt/evidence-cache@1"; releaseId: string; artifactHash: Sha256Digest; evidence: EvidenceEnvelopeV1[]; producedAt: string; producedByGate: GateChannel; } async function tryReuseEvidence( cacheCloneDir: string, releaseId: string, artifactHash: Sha256Digest, forceRequested: boolean, io: WorkspaceIO = createDefaultIO().io, ): Promise { // RFC-0867: The sequential deployment pipeline (dev → alt → main) uses the same // certification profile and policy bundle for all gates of a given release. // The artifact hash (policyBundleRoot) match is therefore sufficient to guarantee // that reused evidence satisfies the current gate's requirements. If the profile // or policy bundle changes between gates, a superseding RFC must add a profile-hash // check here. if (forceRequested) return null; const gateDecisionsDir = path.join(cacheCloneDir, "gate-decisions"); let entries: string[]; try { entries = (await io.readdir(gateDecisionsDir)).map((e) => e.name); } catch { return null; } const now = new Date(); const gateSuffixes = ["dev", "alt", "main"]; for (const gate of gateSuffixes) { const decisionFile = entries.find((e) => e === `${releaseId}-${gate}.json`); if (!decisionFile) continue; let decision: GateDecisionV1; try { const content = await io.readFile(path.join(gateDecisionsDir, decisionFile)); const parsed = JSON.parse(content); const result = gateDecisionV1Schema.safeParse(parsed); if (!result.success) continue; decision = result.data; } catch { continue; } if (decision.status !== "pass") continue; if (decision.policyBundleRoot !== artifactHash) continue; const sidecarFile = entries.find((e) => e === `${releaseId}-evidence.json`); if (!sidecarFile) continue; let sidecar: EvidenceCacheSidecar; try { const sidecarContent = await io.readFile(path.join(gateDecisionsDir, sidecarFile)); const sidecarParsed = JSON.parse(sidecarContent); if ( sidecarParsed.schema !== "werkstatt/evidence-cache@1" || sidecarParsed.artifactHash !== artifactHash || !Array.isArray(sidecarParsed.evidence) ) { continue; } const validatedEvidence: EvidenceEnvelopeV1[] = []; for (const env of sidecarParsed.evidence) { const envResult = evidenceEnvelopeV1Schema.safeParse(env); if (!envResult.success) continue; validatedEvidence.push(envResult.data); } if (validatedEvidence.length === 0) continue; sidecar = { schema: sidecarParsed.schema, releaseId: sidecarParsed.releaseId, artifactHash: sidecarParsed.artifactHash, evidence: validatedEvidence, producedAt: sidecarParsed.producedAt, producedByGate: sidecarParsed.producedByGate, }; } catch { continue; } const freshEvidence = sidecar.evidence.filter((env) => { if (!env.freshness?.expiresAt) return false; return new Date(env.freshness.expiresAt) > now; }); if (freshEvidence.length === 0) continue; const earliestExpiry = freshEvidence.map((env) => env.freshness.expiresAt).sort()[0]; return { artifactHash, evidence: freshEvidence, producedAt: sidecar.producedAt, freshnessExpiresAt: earliestExpiry, sourceGate: gate as GateChannel, }; } return null; } async function writeEvidenceSidecar( cacheCloneDir: string, releaseId: string, artifactHash: Sha256Digest, evidence: EvidenceEnvelopeV1[], producedAt: string, producedByGate: GateChannel, io: WorkspaceIO = createDefaultIO().io, ): Promise { const sidecar: EvidenceCacheSidecar = { schema: "werkstatt/evidence-cache@1", releaseId, artifactHash, evidence, producedAt, producedByGate, }; const outputDir = path.join(cacheCloneDir, "gate-decisions"); await io.mkdir(outputDir); const sidecarPath = path.join(outputDir, `${releaseId}-evidence.json`); await writeFileIfChanged(sidecarPath, JSON.stringify(sidecar, null, 2) + "\n"); return sidecarPath; } export interface CertifyInput { systemId: string; gate: "dev" | "alt" | "main"; candidateId: string; artifactHash: Sha256Digest; releaseId: string; } export interface PinManagement { autoManaged: boolean; suspended: boolean; resumed: boolean; staleResumed: boolean; restoreError: string | null; } export interface CertifyResult { command: "leitstand.certify"; systemId: string; gate: string; decisionId: string; status: "pass" | "fail" | "stale" | "incomplete"; outputPath: string; mainVerificationPath?: string; producerCount: number; evidenceCount: number; pinManagement?: PinManagement; // RFC-1207: measurement-target provenance — which artifact the gate evaluated. measurementTarget?: "candidate-preview" | "channel" | "explicit"; measuredReleaseId?: string; /** Candidate-preview identity. Present on every candidate-mode run; baseUrl/port only when a preview was actually provisioned (evidence-reuse runs skip the boot). */ preview?: { artifactHash: string; baseUrl?: string; port?: number; }; } export async function runLeitstandCertify( input: KernelCommandInput, context: KernelRuntimeContext, ): Promise> { const systemId = flagSite(input); if (!systemId) throw new Error("[leitstand.certify] --site is required"); const gate = flagString(input, "gate"); if (!gate) throw new Error("[leitstand.certify] --gate is required (dev | alt | main)"); if (!["dev", "alt", "main"].includes(gate)) { throw new Error(`[leitstand.certify] --gate must be dev, alt, or main (got: ${gate})`); } const candidateId = flagString(input, "candidate-id") ?? systemId; const artifactHashFlag = flagString(input, "artifact-hash"); const releaseId = flagString(input, "release"); if (!releaseId) throw new Error("[leitstand.certify] --release is required"); const releaseDir = path.join(context.workspaceRoot, "releases", releaseId); const { hash: artifactHash, source: artifactHashSource, path: artifactHashPath, } = await resolveArtifactHash(artifactHashFlag, releaseDir); context.logger.info( `[certify] artifact hash resolved from ${artifactHashSource}${artifactHashPath ? ` (${artifactHashPath})` : ""}: ${artifactHash}`, ); const baseUrlFlag = flagString(input, "base-url"); const gateChannel = gate as GateChannel; // RFC-1207: measurement target resolution — `--base-url` is the explicit // override (verbatim, unchanged); `--target channel` keeps the legacy // channel-measuring audit path; the default `candidate` mode provisions a // local `wrangler dev` preview of the candidate artifact inside certifyCore. const targetFlag = flagString(input, "target"); if (targetFlag !== undefined && targetFlag !== "candidate" && targetFlag !== "channel") { throw new Error( `[leitstand.certify] --target must be "candidate" or "channel" (got: ${targetFlag})`, ); } const certifyTarget: CertifyTarget = targetFlag === "channel" ? "channel" : "candidate"; // RFC-0938: Auto-manage access PIN. Default: true. Post-RFC-1284 the flag // controls suspend-before-measure / resume-after and crash recovery — under // flag=false an entry suspension marker is left in place and the run // proceeds on the already-open channel (REVIEW-CODE-2026-10-15-02 F2, // decisions Q-7). const autoManagePinRaw = input.flags["auto-manage-pin"]; const autoManagePin = autoManagePinRaw === false || autoManagePinRaw === "false" ? false : true; // RFC-1284: crash recovery on entry. A stale accessPinSuspended marker means // a previous certify died between secret deletion and restore — the channel // is currently open and desired protection is unmet. Resume unconditionally // when --auto-manage-pin is in effect so the next certify always re-asserts // the projection. The cache clone owns the marker — non-smart state reads // keep every access-touching command on one store (decisions Q-6). const systemState = await readSystemState(context.workspaceRoot, systemId); const recordedPin = systemState.accessPin; // `?? null` — loose-schema extension keys may be undefined in un-normalized reads. let suspensionActive = (systemState.accessPinSuspended ?? null) !== null; let pinSuspended = false; let pinResumed = false; let staleResumed = false; let pinRestoreError: string | null = null; const { executeKernelCommand } = await import("@warpgogol/werkstatt-engine/kernel"); if (suspensionActive && autoManagePin) { context.logger.info( `[leitstand.certify] stale access-PIN suspension detected (by ${systemState.accessPinSuspended?.by} at ${systemState.accessPinSuspended?.at}) — resuming before certification`, ); let resumeFailedDetail: string | null = null; try { const resumeResult = await executeKernelCommand({ workspaceRoot: context.workspaceRoot, commandName: "leitstand.access.resume", argv: [`--site=${systemId}`], }); const resumeReports = Array.isArray(resumeResult) ? resumeResult : [resumeResult]; if ( resumeReports.some((r: { ok?: boolean; exitCode?: number }) => !r.ok || r.exitCode !== 0) ) { resumeFailedDetail = "leitstand.access.resume reported failure"; } } catch (err) { resumeFailedDetail = err instanceof Error ? err.message : String(err); } if (resumeFailedDetail !== null) { const msg = `[leitstand.certify] failed to resume access PIN suspension for site '${systemId}': ${resumeFailedDetail}. ` + `The site may still be unprotected. Run: pnpm exec werkstatt run leitstand.access.resume --site ${systemId}`; return { exitCode: 1, summary: msg, nextSteps: [{ action: msg, kind: "required" as const }], data: { command: "leitstand.certify", systemId, gate, decisionId: "", status: "fail", outputPath: "", producerCount: 0, evidenceCount: 0, pinManagement: { autoManaged: autoManagePin, suspended: false, resumed: false, staleResumed: false, restoreError: resumeFailedDetail, }, } as CertifyResult, } as unknown as KernelCommandResult; } staleResumed = true; suspensionActive = false; } else if (suspensionActive && !autoManagePin) { context.logger.info( `[leitstand.certify] access-PIN suspension marker present (by ${systemState.accessPinSuspended?.by} at ${systemState.accessPinSuspended?.at}) — proceeding on the open channel; --auto-manage-pin=false leaves secrets untouched`, ); } const cacheCloneDir = resolveCacheClonePath(context.workspaceRoot, systemId); const candidateServerDir = path.join(releaseDir, "dist", "server"); // Candidate mode fails closed when the built artifact is missing — the // authorizing gate never silently measures the incumbent channel. The check // runs before PIN management so an absent artifact does not churn the access // PIN. Explicit --base-url and --target channel bypass it. if (!baseUrlFlag && certifyTarget === "candidate") { const earlyIo = context.io; if (!(await earlyIo.exists(candidateServerDir))) { const msg = `candidate artifact missing: ${candidateServerDir} not found. ` + `The gate measures the candidate artifact through a local preview (RFC-1207) — build it first.`; return { data: { command: "leitstand.certify", systemId, gate, decisionId: "", status: "fail", outputPath: "", producerCount: 0, evidenceCount: 0, measurementTarget: "candidate-preview", measuredReleaseId: releaseId, } as CertifyResult, summary: `[leitstand.certify] ${msg}`, exitCode: 2, nextSteps: [ { action: `Build the release artifact: pnpm exec werkstatt run release.prepare --mission `, kind: "required", }, ], } as unknown as KernelCommandResult; } } const forceRequested = flagBoolean(input, "force"); // RFC-1284: suspend only when the measurement target is a protected live // host — a `--target channel` run, or an explicit --base-url pointing at a // configured dev/alt channel hostname. candidate-preview (the RFC-1207 // default) never touches the channel and never suspends. const operationId = `certify-${new Date() .toISOString() .toLowerCase() .replace(/[:.]/g, "-")}-${Math.random().toString(36).slice(2, 8)}`; const protectedLiveTarget = await isProtectedLiveTarget( baseUrlFlag, certifyTarget, context.workspaceRoot, systemId, ); if (recordedPin !== null && autoManagePin && protectedLiveTarget) { context.logger.info( `[leitstand.certify] access PIN active on protected live target — suspending for certification`, ); const suspendResult = await executeKernelCommand({ workspaceRoot: context.workspaceRoot, commandName: "leitstand.access.suspend", argv: [`--site=${systemId}`, `--by=leitstand.certify`, `--operation-id=${operationId}`], }); const suspendReports = Array.isArray(suspendResult) ? suspendResult : [suspendResult]; const suspendFailed = suspendReports.some( (r: { ok?: boolean; exitCode?: number }) => !r.ok || r.exitCode !== 0, ); if (suspendFailed) { const msg = `[leitstand.certify] failed to suspend access PIN for site '${systemId}'. ` + `Producers would hit the protected channel. ` + `Run: pnpm exec werkstatt run leitstand.access.suspend --site ${systemId}`; return { exitCode: 1, summary: msg, nextSteps: [{ action: msg, kind: "required" as const }], data: { command: "leitstand.certify", systemId, gate, decisionId: "", status: "fail", outputPath: "", producerCount: 0, evidenceCount: 0, pinManagement: { autoManaged: true, suspended: false, resumed: false, staleResumed, restoreError: null, }, } as CertifyResult, } as unknown as KernelCommandResult; } pinSuspended = true; // Cloudflare Workers may retain the deleted secret for a few seconds. // Wait before running discovery to avoid 401 race condition. console.info( `[leitstand.certify] waiting ${PIN_DELETION_PROPAGATION_DELAY_MS / 1000}s for PIN deletion to propagate…`, ); await new Promise((resolve) => setTimeout(resolve, PIN_DELETION_PROPAGATION_DELAY_MS)); } else if (recordedPin !== null && !autoManagePin && !suspensionActive) { // RFC-0929/RFC-1284: fail-early fires only when the pin is active and no // suspension is in effect — a suspended system already has an open channel. const msg = `[leitstand.certify] site '${systemId}' has access PIN protection active. ` + `Run: pnpm exec werkstatt run leitstand.access.suspend --site ${systemId}`; return { exitCode: 1, summary: msg, nextSteps: [{ action: msg, kind: "required" as const }], data: { command: "leitstand.certify", systemId, gate, decisionId: "", status: "fail", outputPath: "", producerCount: 0, evidenceCount: 0, pinManagement: { autoManaged: false, suspended: false, resumed: false, staleResumed, restoreError: null, }, } as CertifyResult, } as unknown as KernelCommandResult; } // Inner function containing all certification logic. The try/finally wrapper // below ensures the PIN is restored even on early returns or thrown errors. const certifyCore = async ( io: WorkspaceIO = createDefaultIO().io, ): Promise> => { // RFC-1207: resolve the measurement target inside certifyCore so the // preview lifecycle stays scoped to the evaluation. Explicit --base-url // wins verbatim; channel mode keeps the legacy deployment-record path; // candidate mode provisions wrangler dev lazily — only when producers // actually run (evidence reuse skips the boot entirely). let baseUrl: string | undefined; let measuredReleaseId: string | undefined; let measurementTarget: "candidate-preview" | "channel" | "explicit" | undefined; let measuredMismatch: string | null = null; let preview: CandidatePreview | null = null; if (baseUrlFlag) { baseUrl = baseUrlFlag; measurementTarget = "explicit"; } else if (certifyTarget === "channel") { const resolvedBase = await resolveDevDeploymentRecord(cacheCloneDir, undefined); baseUrl = resolvedBase?.deploymentUrl; measuredReleaseId = resolvedBase?.releaseId; measurementTarget = "channel"; // RECOVERY-2026-10-05: stale-channel detection. The gate crawls the // latest *succeeded dev* deployment record's URL — which may host a // different release than the candidate. Surface that upfront so a // misleading verdict self-explains instead of being chased as a // regression. Audit-only mode — ship never passes --target channel. measuredMismatch = detectMeasuredReleaseMismatch(undefined, measuredReleaseId, releaseId); if (measuredMismatch) { context.logger.warn( `[leitstand.certify] gate=${gate} measures the recorded dev deployment of ${measuredMismatch}, not candidate ${releaseId} — findings reflect the deployed channel artifact. ` + `For candidate-true certification serve the release artifact locally and pass --base-url (RECOVERY-2026-10-04).`, ); } } else { measurementTarget = "candidate-preview"; measuredReleaseId = releaseId; } const reuseEntry = await tryReuseEvidence( cacheCloneDir, releaseId, artifactHash, forceRequested, ); // RFC-1295: producer set derives from the profile's requirements — the // editorial-signoff producer joins mission.check for editorial-localization. const producerNodes: ProducerDependencyNodeV1[] = [ ...new Set(astroCertificationProfile.requirements.map((r) => r.producerId)), ].map((producerId) => ({ producerId, dependsOn: [] })); const plan = planProducers(producerNodes); if (!plan.ok) { return { exitCode: 1, summary: `[leitstand.certify] producer planning failed: ${plan.ruleId} — ${plan.message}`, nextSteps: [ { action: `[leitstand.certify] producer planning failed: ${plan.ruleId} — ${plan.message}`, kind: "required" as const, }, ], data: { command: "leitstand.certify", systemId, gate, decisionId: "", status: "incomplete", outputPath: "", producerCount: 0, evidenceCount: 0, }, diagnostics: [ { ruleId: plan.ruleId, severity: "error", message: plan.message, evidence: [] }, ], } as unknown as KernelCommandResult; } const now = new Date().toISOString(); // operationId is generated at run head (RFC-1284) so the access.suspend // marker and producer attempts share one run identity. const candidate: ReleaseCandidateV1 = { schema: "werkstatt/release-candidate@1", candidateId, systemId, releaseVersion: releaseId, sourceHash: artifactHash, contentHash: artifactHash, artifactHash, buildConfig: { schema: "werkstatt/build-config@1", buildConfigHash: artifactHash, toolchainId: "astro", sourceRef: "src/content", contentHash: artifactHash, }, deploymentPlan: { schema: "werkstatt/deployment-plan@1", deploymentPlanHash: artifactHash, channel: gateChannel, target: systemId, environmentRefs: [], }, policyBundleRoot: artifactHash, toolchainId: "astro", observedEnvironment: { schema: "werkstatt/observed-environment@1", environment: "dev", environmentIdentityHash: artifactHash, observedAt: now, }, observedAt: now, }; const policyBundle: CertificationPolicyBundleV1 = { schema: "werkstatt/certification-policy-bundle@1", policyBundleId: "astro-default-bundle", version: "1.0.0", profileId: astroCertificationProfile.id, resolvedRequirements: astroCertificationProfile.requirements.map((req) => ({ requirementId: req.id, source: "astro-certification-profile", description: req.title, mandatory: req.classification === "required", })), producerManifests: Object.values(astroCertificationProfile.producers).map((p) => ({ schema: "werkstatt/producer-manifest@1", producerId: p.id, version: "1.0.0", capabilityId: p.id, schemaHash: artifactHash, })), rubricManifests: [], toolchainManifests: [], issuerManifests: [], riskPolicy: { maxStale: 0, maxIncomplete: 0, blockOnFail: true }, retention: { minRetentionDays: astroCertificationProfile.retentionPolicy.minRetentionDays, maxRetentionDays: astroCertificationProfile.retentionPolicy.maxRetentionDays, }, materializedAt: now, }; const handler: ProducerExecutionHandlerV1 = async ( execInput: ProducerExecutionInputV1, io: WorkspaceIO = createDefaultIO().io, ): Promise => { const evidenceId = `ev-${execInput.producerId}-${now.toLowerCase().replace(/[:.]/g, "-")}`; const diagnostics: Array<{ ruleId: string; severity: "error" | "warning" | "info"; message: string; evidence: Array<{ kind: "source" | "config" | "rule" | "rendered" | "cache" | "runtime" }>; }> = []; if (execInput.producerId === "astro-mission-check" && baseUrl) { try { const { executeKernelCommand } = await import("@warpgogol/werkstatt-engine/kernel"); let missionId: string | undefined; try { const buildIdentityPath = path.join( context.workspaceRoot, "releases", releaseId, "dist", "client", ".well-known", "build-identity.json", ); const identity = JSON.parse(await io.readFile(buildIdentityPath)); missionId = identity.missionId; } catch { // build-identity.json not yet written or malformed — missionId unavailable } const argv = [ `--site=${systemId}`, "--external-preview", `--base-url=${baseUrl}`, `--channel=${gateChannel}`, "--no-report", ]; if (missionId) argv.push(`--mission=${missionId}`); let timer: ReturnType | undefined; const timeoutPromise = new Promise((_, reject) => { timer = setTimeout( () => reject( new Error(`mission.check timed out after ${MISSION_CHECK_TIMEOUT_MS / 1000}s`), ), MISSION_CHECK_TIMEOUT_MS, ); }); try { const result = await Promise.race([ executeKernelCommand({ workspaceRoot: context.workspaceRoot, commandName: "mission.check", argv, }), timeoutPromise, ]); const reports = Array.isArray(result) ? result : [result]; const failed = reports.find((r) => !r.ok || r.exitCode !== 0); if (failed) { diagnostics.push({ ruleId: "MISSION-CHECK-01", severity: "error", message: `mission.check exited with code ${failed.exitCode}${failed.summary ? `: ${failed.summary}` : ""}`, evidence: [], }); } else { diagnostics.push({ ruleId: "MISSION-CHECK-01", severity: "info", message: "mission.check passed", evidence: [], }); } } finally { if (timer) clearTimeout(timer); } } catch (err) { diagnostics.push({ ruleId: "MISSION-CHECK-01", severity: "error", message: `mission.check failed: ${err instanceof Error ? err.message : String(err)}`, evidence: [], }); } } else if (execInput.producerId === "astro-mission-check" && !baseUrl) { diagnostics.push({ ruleId: "MISSION-CHECK-01", severity: "warning", message: "mission.check skipped — no dev deployment URL available (first deploy or no open mission)", evidence: [], }); } else if (execInput.producerId === "editorial-signoff") { // RFC-1295: human editorial sign-off is evaluated at promote-main only. // At earlier gates the requirement defers with an info diagnostic — // never enforced below main, never silently satisfied. if (gate !== "main") { diagnostics.push({ ruleId: "EDIT-00", severity: "info", message: "editorial sign-off is evaluated at the main gate only — deferred", evidence: [], }); } else { try { const { executeKernelCommand } = await import("@warpgogol/werkstatt-engine/kernel"); const result = await executeKernelCommand({ workspaceRoot: context.workspaceRoot, commandName: "editorial.review.verify", siteName: systemId, argv: [`--release=${releaseId}`], outputFormat: "json", siteExplicit: true, }); const reports = Array.isArray(result) ? result : [result]; let emitted = false; for (const report of reports) { const diags = (report.data as { diagnostics?: unknown } | undefined)?.diagnostics ?? []; for (const d of diags as Array<{ ruleId?: string; severity?: "error" | "warning" | "info"; message?: string; }>) { emitted = true; diagnostics.push({ ruleId: d.ruleId ?? "EDIT-00", severity: d.severity ?? "error", message: d.message ?? "editorial.review.verify diagnostic", evidence: [], }); } } const failed = reports.find((r) => !r.ok || r.exitCode !== 0); if (failed && !emitted) { diagnostics.push({ ruleId: "EDIT-01", severity: "error", message: `editorial.review.verify exited with code ${failed.exitCode}${failed.summary ? `: ${failed.summary}` : ""}`, evidence: [], }); } else if (!failed && !emitted) { diagnostics.push({ ruleId: "EDIT-00", severity: "info", message: "editorial sign-off verified", evidence: [], }); } } catch (err) { diagnostics.push({ ruleId: "EDIT-01", severity: "error", message: `editorial.review.verify failed: ${err instanceof Error ? err.message : String(err)}`, evidence: [], }); } } } return { schema: "werkstatt/evidence-envelope@1", evidenceId, candidateId, producerId: execInput.producerId, producerAttemptId: `${operationId}-0`, producedAt: now, result: { schema: "werkstatt/evidence-result@1", producerId: execInput.producerId, producerAttemptId: `${operationId}-0`, diagnostics, bindingHash: artifactHash, applicability: { appliesTo: execInput.profile.requirements .filter((r) => r.producerId === execInput.producerId) .map((r) => r.id), scope: "site", }, }, payloads: [], redaction: { schema: "werkstatt/redaction-report@1", policyVersion: "1.0.0", detectedSecrets: 0, detectedPii: 0, resolved: true, unresolvedSecrets: 0, unresolvedPii: 0, }, authorityAdmission: { schema: "werkstatt/authority-admission@1", authoritySequence: 1, admittedAt: now, admittedBy: "leitstand.certify", }, freshness: { expiresAt: new Date(Date.now() + 30 * 60 * 1000).toISOString(), staleAfter: new Date(Date.now() + 60 * 60 * 1000).toISOString(), }, }; }; let evidence: EvidenceEnvelopeV1[]; let producerCount = producerNodes.length; if (reuseEntry) { console.info( `[leitstand.certify] reusing evidence from ${reuseEntry.sourceGate} gate (artifact hash match)`, ); evidence = reuseEntry.evidence; producerCount = 0; } else { // RFC-1207: candidate mode boots the preview only here — evidence-reuse // paths never spawn wrangler dev, and PIN-failure early returns precede // certifyCore entirely. if (measurementTarget === "candidate-preview") { try { preview = await provisionCandidatePreview({ serverDir: candidateServerDir, releaseId, artifactHash, logger: context.logger, }); baseUrl = preview.baseUrl; } catch (err) { const msg = err instanceof Error ? err.message : String(err); return { data: { command: "leitstand.certify", systemId, gate, decisionId: "", status: "fail", outputPath: "", producerCount: 0, evidenceCount: 0, measurementTarget, measuredReleaseId, } as CertifyResult, summary: `[leitstand.certify] candidate preview failed (infrastructure): ${msg}`, exitCode: 2, nextSteps: [ { action: `Verify ${candidateServerDir} exists and wrangler is installed (pnpm exec wrangler --version), then re-run the gate`, kind: "required", }, ], } as unknown as KernelCommandResult; } } try { const execResult = await executeProducers(plan, handler, { candidate, profile: astroCertificationProfile, policyBundle, config: { ...DEFAULT_PRODUCER_CONFIG, operationId, timestamp: now, timeoutMs: MISSION_CHECK_TIMEOUT_MS + 60_000, maxRetries: 1, }, }); if (!execResult.ok) { return { exitCode: 1, summary: `[leitstand.certify] producer execution failed: ${execResult.ruleId} — ${execResult.message}`, nextSteps: [ { action: `[leitstand.certify] producer execution failed: ${execResult.ruleId} — ${execResult.message}`, kind: "required" as const, }, ], data: { command: "leitstand.certify", systemId, gate, decisionId: "", status: "incomplete", outputPath: "", producerCount: producerNodes.length, evidenceCount: 0, measurementTarget, measuredReleaseId, } as CertifyResult, diagnostics: [ { ruleId: execResult.ruleId, severity: "error", message: execResult.message, evidence: [], }, ], } as unknown as KernelCommandResult; } evidence = execResult.evidence; await writeEvidenceSidecar( cacheCloneDir, releaseId, artifactHash, evidence, now, gateChannel, ); } finally { // RFC-1207 (AC-7): preview teardown is finally-guaranteed — a failed // evaluation leaves no orphan wrangler dev. A crashed teardown warns // but does not flip the verdict. if (preview) { try { await preview.stop(); } catch (stopErr) { context.logger.warn( `[leitstand.certify] candidate preview teardown failed: ${stopErr instanceof Error ? stopErr.message : String(stopErr)}`, ); } } } } const evaluationResult = evaluateCertificationDecision({ candidateId, policyBundle, evidence, evaluationCutSequence: 1, authorityTime: now, gate: gateChannel, decidedAt: now, }); if (!evaluationResult.ok) { return { exitCode: 1, summary: `[leitstand.certify] evaluation failed: ${evaluationResult.code} — ${evaluationResult.message}`, nextSteps: [ { action: `[leitstand.certify] evaluation failed: ${evaluationResult.code} — ${evaluationResult.message}`, kind: "required" as const, }, ], data: { command: "leitstand.certify", systemId, gate, decisionId: "", status: "incomplete", outputPath: "", producerCount: producerCount, evidenceCount: evidence.length, measurementTarget, measuredReleaseId, } as CertifyResult, diagnostics: [ { ruleId: evaluationResult.code, severity: "error", message: evaluationResult.message, evidence: [], }, ], } as unknown as KernelCommandResult; } const decisionId = `dec-${now.toLowerCase().replace(/[:.]/g, "-")}-${Math.random().toString(36).slice(2, 8)}`; const gateDecision: GateDecisionV1 = { schema: "werkstatt/gate-decision@1", decisionId, candidateId, policyBundleRoot: artifactHash, gate: gateChannel, evaluationCut: 1, selectedEvidence: evaluationResult.selectedEvidence.map((sel) => ({ evidenceId: sel.evidenceId, evidenceHash: sel.evidenceHash, selectedAt: now, })), status: evaluationResult.status, coverage: evaluationResult.coverage, reasons: [ ...evaluationResult.reasons, ...(measuredReleaseId ? [ measuredMismatch ? `measured-release=${measuredReleaseId} differs from candidate=${releaseId} — gate evaluated the deployed channel artifact, not the candidate` : `measured-release=${measuredReleaseId}`, ] : []), ], actionPackRef: null, decidedAt: now, // RFC-1207 (AC-2): provenance persisted on the decision record itself — // not only the command result — so audits can tell which artifact the // verdict measured. measurementTarget, measuredReleaseId, }; const outputDir = path.join(cacheCloneDir, "gate-decisions"); await io.mkdir(outputDir); const outputPath = path.join(outputDir, `${releaseId}-${gate}.json`); await writeFileIfChanged(outputPath, JSON.stringify(gateDecision, null, 2) + "\n"); // When gate=main and status=pass, also write MainVerificationDecisionV1 let mainVerificationPath: string | undefined; if (gate === "main" && evaluationResult.status === "pass") { const mvDecisionId = `dec-mv-${now.toLowerCase().replace(/[:.]/g, "-")}-${Math.random().toString(36).slice(2, 8)}`; const mainVerification: MainVerificationDecisionV1 = { schema: "werkstatt/main-verification-decision@1", decisionId: mvDecisionId, candidateId, policyBundleRoot: artifactHash, gate: "main", evaluationCut: 1, selectedEvidence: gateDecision.selectedEvidence, status: "pass", coverage: evaluationResult.coverage, reasons: gateDecision.reasons, actionPackRef: null, rootDossierRef: artifactHash, priorOperationRef: null, decidedAt: now, }; mainVerificationPath = path.join(outputDir, `${releaseId}-main-verification.json`); await writeFileIfChanged( mainVerificationPath, JSON.stringify(mainVerification, null, 2) + "\n", ); } const relPath = path.join("gate-decisions", `${releaseId}-${gate}.json`); const sidecarRelPath = path.join("gate-decisions", `${releaseId}-evidence.json`); const sidecarExists = !reuseEntry; try { await gitExec(cacheCloneDir, `add ${relPath}`); if (sidecarExists) { await gitExec(cacheCloneDir, `add ${sidecarRelPath}`); } if (mainVerificationPath) { await gitExec( cacheCloneDir, `add ${path.join("gate-decisions", `${releaseId}-main-verification.json`)}`, ); } await cacheCloneCommit( cacheCloneDir, `gate-decisions: ${releaseId}-${gate} (${gateDecision.status})`, ); const branch = (await gitExec(cacheCloneDir, "rev-parse --abbrev-ref HEAD")).trim(); await gitExec(cacheCloneDir, `push origin ${branch}`); } catch { // Best-effort commit+push — file is written, sync will propagate later } const r2Config = makeR2ConfigFromEnv(process.env as Record); if (r2Config) { try { const adapter = createR2StorageAdapter(r2Config); const gateDecisionBytes = Buffer.from(JSON.stringify(gateDecision, null, 2) + "\n", "utf8"); await adapter.putObject({ digest: artifactHash, bytes: new Uint8Array(gateDecisionBytes), mediaType: "application/json", }); } catch (err) { console.warn( `[leitstand.certify] durable sync upload failed: ${err instanceof Error ? err.message : String(err)}`, ); } } return { data: { command: "leitstand.certify", systemId, gate, decisionId, status: evaluationResult.status, outputPath, mainVerificationPath, producerCount: producerCount, evidenceCount: evidence.length, measurementTarget, measuredReleaseId, // Preview identity rides every candidate-mode result — including // evidence-reuse runs that never booted a preview — so leitstand.ship // can journal candidatePreview.artifactHash for resume attribution. preview: measurementTarget === "candidate-preview" ? { artifactHash, ...(preview ? { baseUrl: preview.baseUrl, port: preview.port } : {}), } : undefined, }, summary: `[leitstand.certify] gate=${gate} status=${evaluationResult.status} decision=${decisionId} output=${outputPath}` + (measuredMismatch && evaluationResult.status === "fail" ? ` — WARN: gate measured the deployed ${measuredMismatch} artifact, not candidate ${releaseId}; findings may be stale-channel state. Re-certify the candidate via --base-url (RECOVERY-2026-10-04), or deploy it first and re-run.` : ""), exitCode: 0, nextSteps: evaluationResult.status === "pass" ? gate === "alt" ? [ { action: `Promote to main: pnpm exec werkstatt run leitstand.promote --site ${systemId} --release ${releaseId}`, kind: "optional", }, ] : [ { action: `Deployment pipeline complete. Close the mission: pnpm exec werkstatt run mission.close --mission `, kind: "optional", }, ] : [ { action: `Review the evaluation failures in ${outputPath}, fix the issues, then re-run: pnpm exec werkstatt run leitstand.certify --release ${releaseId} --gate ${gate}`, kind: "required", }, ], } as unknown as KernelCommandResult; }; // end certifyCore let result: KernelCommandResult; try { result = await certifyCore(); } finally { if (pinSuspended) { console.info(`[leitstand.certify] resuming access PIN after certification`); try { const resumeResult = await executeKernelCommand({ workspaceRoot: context.workspaceRoot, commandName: "leitstand.access.resume", argv: [`--site=${systemId}`], }); const resumeReports = Array.isArray(resumeResult) ? resumeResult : [resumeResult]; const resumeFailed = resumeReports.some( (r: { ok?: boolean; exitCode?: number }) => !r.ok || r.exitCode !== 0, ); if (resumeFailed) { pinRestoreError = `PIN resume failed for site '${systemId}'`; } else { pinResumed = true; } } catch (err) { pinRestoreError = err instanceof Error ? err.message : String(err); } } } // Post-finally: if PIN resume failed, override to hard fail (exitCode 1). // The suspension marker stays written — desired state remains declared and // the next certify/deploy/validate converges it (RFC-1284). if (pinSuspended && pinRestoreError !== null) { const msg = `[leitstand.certify] CRITICAL: access PIN resume failed: ${pinRestoreError}. ` + `Site '${systemId}' stays suspended. Run: pnpm exec werkstatt run leitstand.access.resume --site ${systemId}`; return { exitCode: 1, summary: msg, nextSteps: [{ action: msg, kind: "required" as const }], data: { ...(result.data ?? {}), command: "leitstand.certify", systemId, gate, pinManagement: { autoManaged: true, suspended: true, resumed: false, staleResumed, restoreError: pinRestoreError, }, } as CertifyResult, } as unknown as KernelCommandResult; } // Attach pinManagement for auto-manage paths and entry-recovery runs. if ((autoManagePin || staleResumed) && result.data) { (result.data as CertifyResult).pinManagement = { autoManaged: autoManagePin, suspended: pinSuspended, resumed: pinResumed, staleResumed, restoreError: null, }; } return result; } export interface DevDeploymentRecord { deploymentUrl: string; releaseId?: string; } export async function resolveDevDeploymentRecord( cacheCloneDir: string, baseUrlFlag: string | undefined, io: WorkspaceIO = createDefaultIO().io, ): Promise { if (baseUrlFlag) return { deploymentUrl: baseUrlFlag }; const opsDir = path.join(cacheCloneDir, "deployment-operations"); let latest: DevDeploymentRecord | undefined; try { const entries = await io.readdir(opsDir); let latestAt = ""; for (const entry of entries) { if (!entry.name.endsWith(".json")) continue; try { const content = await io.readFile(path.join(opsDir, entry.name)); const record = JSON.parse(content) as { channel?: string; state?: DeploymentOperationState; timestamp?: string; deploymentUrl?: string; releaseId?: string; }; if (record.channel === "dev" && record.state === "succeeded" && record.deploymentUrl) { const at = record.timestamp ?? ""; if (!latest || at > latestAt) { latestAt = at; latest = { deploymentUrl: record.deploymentUrl, releaseId: record.releaseId }; } } } catch { // Skip malformed records } } } catch { // No deployment-operations directory yet } return latest; } export async function resolveDevBaseUrl( cacheCloneDir: string, baseUrlFlag: string | undefined, io: WorkspaceIO = createDefaultIO().io, ): Promise { return (await resolveDevDeploymentRecord(cacheCloneDir, baseUrlFlag, io))?.deploymentUrl; } // RFC-1284: does this run's measurement target hit a protected live host? // Channel mode measures the recorded dev deployment — always the protected // channel. Explicit --base-url is protected when its hostname matches a // configured dev/alt channel URL (covers custom subdomains and // dev-..workers.dev), with a ^(dev|alt)[.-] prefix fallback // for hosts absent from channel config. candidate-preview never qualifies. export async function isProtectedLiveTarget( baseUrlFlag: string | undefined, certifyTarget: CertifyTarget, workspaceRoot: string, systemId: string, ): Promise { if (certifyTarget === "channel") return true; if (!baseUrlFlag) return false; let hostname: string; try { hostname = new URL(baseUrlFlag).hostname.toLowerCase(); } catch { return false; } // Unreadable/absent config degrades to the prefix fallback — a missing // channel list must never crash the PIN pre-flight. let channelUrls: Array = []; try { const systemConfig = await readSystemConfigSmart(workspaceRoot, systemId); channelUrls = [ systemConfig.deployment?.channels.dev.url, systemConfig.deployment?.channels.alt.url, ]; } catch { channelUrls = []; } const channelHosts = channelUrls .map((url) => { try { return url ? new URL(url).hostname.toLowerCase() : null; } catch { return null; } }) .filter((h): h is string => h !== null); if (channelHosts.includes(hostname)) return true; return isDevOrAltHost(hostname); } // RECOVERY-2026-10-05: stale-channel detection. A remote certify without // --base-url measures whatever artifact the dev channel currently serves — // which may be an older release than the candidate (poisoned channel: the // gate's findings then belong to the deployed artifact, not the candidate). // Returns the measured releaseId when it differs from the candidate, else null. export function detectMeasuredReleaseMismatch( baseUrlFlag: string | undefined, measuredReleaseId: string | undefined, candidateReleaseId: string, ): string | null { if (baseUrlFlag || !measuredReleaseId || measuredReleaseId === candidateReleaseId) { return null; } return measuredReleaseId; }