/*
RFC-0354 §7.3: sternsystem.validate — validate registry invariants and bundle contract.
- Do not introduce app-specific runtime composition or deployment behavior into this reusable package source file.
- RFC-1149: migrate ambient filesystem and process IO to the WorkspaceIO port across engine and shared
- RFC-1254: drop unused io-migration leftovers
- RFC-1284: ACCESS-01/02 diagnostics in sternsystem.validate
RFC-1284 step 5: checkAccessProtection compares desired accessPin to
observed channel answers — a proven non-401 is a violation, transport
failures downgrade to warnings, suspended systems skip the probe, and
stale markers (>1h) warn via ACCESS-02.
- RFC-1284: fix review REVIEW-CODE-2026-10-15-02: RFC-1284 — single-store marker, flag=false proceed, surfaced reconcile
F1: marker store unified on the git-tracked cache clone — access commands,
certify entry check, and deploy reconcile all use non-smart
readSystemState/writeSystemState (decisions Q-6); the mission-window
blindness and the DNA-106 false-resume are gone.
F2: --auto-manage-pin=false + suspension at entry skips entry-resume and
proceeds on the open channel (decisions Q-7) — RFC/AC-8 text amended.
F3: accessReconcile surfaced in data + summary of dev-deploy/propagate.
F4: CHANGE_SUMMARY items on all touched files.
F5: sternsystem.validate declares ACCESS-01/02 + system-state.yaml reads.
F6: isProtectedLiveTarget exported and covered (AC-12 probe repointed).
F7: fresh-read ordering proven — suspension opened mid-deploy suppresses
the re-assert.
- RFC-1297: LIVE-01/02 live-channel drift checks alongside ACCESS-01/02 — same transport-failure-warns semantics.
ADR-0082, ADR-0088, ADR-0091, DRIFT-01, DRIFT-02, RFC-0354, RFC-0480, RFC-0520, RFC-0561, RFC-0648, RFC-0792, RFC-0822, RFC-0870, RFC-0902, RFC-0966, RFC-0968, RFC-1097, RFC-1149
*/
import path from "node:path";
import { type WorkspaceIO } from "@warpgogol/werkstatt-shared/kernel/workspace-io";
import { parse as parseYaml } from "yaml";
import type {
KernelCommandInput,
KernelCommandResult,
KernelRuntimeContext,
} from "@warpgogol/werkstatt-engine/kernel";
import { StarCatalog } from "@warpgogol/werkstatt-shared/ontology/cosmic";
import {
systemPinSchema,
type SystemConfig,
type SystemState,
} from "@warpgogol/werkstatt-engine/schemas";
import {
discoverSystems,
hasAppsCollision,
resolveMirrors,
resolveMirrorPath,
resolveWorkpiecePath,
readSystemState,
isGitAccessible,
} from "./registry-io.ts";
import { evaluateExternalEditGate } from "./external-edit-guard.ts";
import {
collectExternalEditInputs,
bordbuchFileExists,
bordbuchPathFor,
} from "./external-edit-collector.ts";
import { collectEnvFiles } from "../mission/env-persist.ts";
import { hasTldSuffix } from "@warpgogol/werkstatt-shared/ontology/operations";
import { readPassport } from "./registry-io.ts";
import { readLiveChannelStates, liveDriftDiagnostics } from "./live-drift.ts";
import { verifyPassport, buildPassportPayload, computePassportHash } from "./passport.ts";
import {
isBordbuchDriftBookkeepingOnly,
dropTrailingBookkeepingEntries,
} from "./passport-staleness.ts";
import { readAuthorization, isAuthorizationExpired } from "./handover.ts";
import { readBordbuch } from "../bordbuch/bordbuch-io.ts";
import { readJournal } from "../journal/jsonl.ts";
import type { JournalRecord } from "../journal/types.ts";
import { probeAccessProtection } from "@warpgogol/werkstatt-shared/node/access-probe";
// RFC-1284: suspension is a transient measured in minutes — a marker older
// than this is almost certainly orphaned (ACCESS-02 warning threshold).
const ACCESS_SUSPENSION_STALE_MS = 60 * 60 * 1000;
export interface SternsystemValidateData {
validated: number;
violations: SternsystemViolation[];
warnings: Array<{ systemId: string; field: string; message: string }>;
withOwner: number;
withoutOwner: number;
}
type SternsystemViolation = { systemId: string; rule: string; message: string };
function flagString(input: KernelCommandInput, key: string): string | undefined {
const v = input.flags[key];
return typeof v === "string" ? v : undefined;
}
export const FORBIDDEN_PATTERNS = [
"dist",
"node_modules",
"packages",
"package.json",
"pnpm-lock.yaml",
"astro.config.mjs",
"astro.config.ts",
"tsconfig.json",
"wrangler.toml",
"wrangler.jsonc",
];
// RFC-0870: Committed generated manifest paths checked in cache clone HEAD.
// These are registry-only generated files committed to git for drift detection.
// Only checked when tracked in git — new systems without manifests are not flagged.
// Source of truth: toOwnershipEntries(context.ownershipMap ?? []) in packages/werkstatt-site/src/checks/generator-ownership.ts
// (filter for markerPolicy="registry-only", conditional=true, path starts with "src/").
// This list is hardcoded here (not dynamically imported) to respect DNA-64 —
// the engine must not statically import from the site plugin.
// When adding a new registry-only conditional manifest to toOwnershipEntries(context.ownershipMap ?? []),
// also add it here.
const COMMITTED_MANIFEST_PATHS = [
"src/image-variants.generated.yaml",
"src/video-manifest.generated.yaml",
"src/live-video-manifest.generated.yaml",
];
// Generated files allowed to be committed but NOT required to persist in HEAD.
// Unlike COMMITTED_MANIFEST_PATHS (registry-only manifests that must stay
// tracked once committed — enforced by STERN-MANIFEST-01), these are migration
// artifacts that may be deleted once their purpose is fulfilled: the
// shrink-only list-punctuation baseline is removed when all LP violations are
// fixed.
const COMMITTED_GENERATED_ALLOWED = ["list-punctuation.baseline.generated.yaml"];
/**
* RFC-0870: Check that committed generated manifests are present in the cache
* clone git HEAD. Uses `git ls-tree HEAD -- ` to determine if a file is
* tracked. Only files that ARE tracked but missing from HEAD trigger
* STERN-MANIFEST-01 — untracked files (new systems) are not flagged.
*/
async function checkManifestPresence(
cacheDir: string,
systemId: string,
io: WorkspaceIO,
): Promise {
const violations: SternsystemViolation[] = [];
for (const manifestPath of COMMITTED_MANIFEST_PATHS) {
try {
// Check if the file is tracked in git history (any commit)
const trackedResult = await io.exec("git", ["log", "--oneline", "-1", "--", manifestPath], {
cwd: cacheDir,
});
if (trackedResult.exitCode !== 0 || !trackedResult.stdout.trim()) continue; // Not tracked — new system, skip
// Check if the file exists in HEAD
const headResult = await io.exec("git", ["ls-tree", "HEAD", "--", manifestPath], {
cwd: cacheDir,
});
if (headResult.exitCode === 0 && !headResult.stdout.trim()) {
violations.push({
systemId,
rule: "STERN-MANIFEST-01",
message: `Committed generated manifest ${manifestPath} is missing from cache clone HEAD — run the generating command in a mission workpiece and commit via mission.git.commit`,
});
}
} catch {
// Git command failed — skip this manifest
}
}
return violations;
}
/**
* ADR-0088: JRN-01 — every `op-started` record in the cache clone's
* `operations/*.jsonl` journals MUST carry a non-empty `missionId` so the
* ADR-0084 close-time sweep can attribute the operation to a mission.
* Unreadable journal files produce a warning — they may hide violations.
*/
async function checkJournalAttribution(
cacheDir: string,
systemId: string,
warnings: Array<{ systemId: string; field: string; message: string }>,
io: WorkspaceIO,
): Promise {
const violations: SternsystemViolation[] = [];
const operationsDir = path.join(cacheDir, "operations");
if (!(await io.exists(operationsDir))) return violations;
let files: string[];
try {
files = (await io.readdir(operationsDir))
.map((e) => e.name)
.filter((f) => f.endsWith(".jsonl"));
} catch (err) {
warnings.push({
systemId,
field: "JRN-01",
message: `operations/: cannot list journal directory — attribution check skipped (${(err as Error).message})`,
});
return violations;
}
for (const file of files) {
const journalPath = path.join(operationsDir, file);
let records: JournalRecord[];
try {
records = await readJournal(journalPath);
} catch (err) {
warnings.push({
systemId,
field: "JRN-01",
message: `operations/${file}: unreadable journal — cannot verify mission attribution (${(err as Error).message})`,
});
continue;
}
for (const rec of records) {
if (rec.kind === "op-started" && (!rec.missionId || rec.missionId.trim() === "")) {
violations.push({
systemId,
rule: "JRN-01",
message: `operations/${file}: op-started '${rec.opId}' (${rec.op}) has empty missionId — cache-clone journal ops must be mission-attributed (ADR-0088)`,
});
}
}
}
return violations;
}
async function checkBundleContract(
cacheDir: string,
systemId: string,
io: WorkspaceIO,
): Promise {
const violations: SternsystemViolation[] = [];
if (!(await io.exists(cacheDir))) return violations;
let trackedFiles: string[] = [];
const lsResult = await io.exec("git", ["ls-files"], { cwd: cacheDir });
if (lsResult.exitCode === 0) {
trackedFiles = lsResult.stdout.split("\n").filter(Boolean);
} else {
return violations;
}
for (const rel of trackedFiles) {
const name = path.basename(rel);
if (FORBIDDEN_PATTERNS.includes(name)) {
violations.push({
systemId,
rule: "bundle-contract",
message: `${systemId}: cache clone contains forbidden file: ${rel}`,
});
}
if (
name.includes(".generated.") &&
!COMMITTED_MANIFEST_PATHS.includes(rel) &&
!COMMITTED_GENERATED_ALLOWED.includes(rel)
) {
violations.push({
systemId,
rule: "bundle-contract",
message: `${systemId}: cache clone contains generated file: ${rel}`,
});
}
}
return violations;
}
async function validateYamlFiles(
cacheDir: string,
systemId: string,
io: WorkspaceIO,
): Promise {
const violations: SternsystemViolation[] = [];
if (!(await io.exists(cacheDir))) return violations;
const entries = await io.readdir(cacheDir);
for (const entry of entries) {
if (!entry.isFile) continue;
if (!entry.name.endsWith(".yaml") && !entry.name.endsWith(".yml")) continue;
const filePath = path.join(cacheDir, entry.name);
try {
const raw = await io.readFile(filePath);
parseYaml(raw);
} catch (err) {
violations.push({
systemId,
rule: "yaml-syntax-error",
message: `${entry.name}: YAML syntax error: ${(err as Error).message}`,
});
}
}
return violations;
}
/**
* RFC-1149 phase 1b: route generated.drift.validate diagnostics — DRIFT-02
* (generator lacks dryRun support) is a blocking violation now that all
* DRIFT-02 generators are migrated to context.io; DRIFT-01 (content drift)
* and other rules stay warnings per the ADR-0082 cleanup caveat.
*/
export function routeDriftDiagnostics(
diagnostics: Array<{ ruleId?: string; file?: string; message?: string }>,
systemId: string,
violations: SternsystemViolation[],
warnings: Array<{ systemId: string; field: string; message: string }>,
): void {
for (const diag of diagnostics) {
const message = diag.message ?? `cache clone generated drift in ${diag.file ?? "unknown file"}`;
if (diag.ruleId === "DRIFT-02") {
violations.push({ systemId, rule: "DRIFT-02", message });
} else {
warnings.push({ systemId, field: diag.ruleId ?? "DRIFT", message });
}
}
}
/**
* RFC-1284: ACCESS-01/02 — compare desired protection (system-state.yaml
* accessPin) to observed channel behavior.
*
* ACCESS-01 — protection drift: pin declared and no suspension marker → every
* configured dev/alt channel URL must answer 401 to an anonymous request. A
* proven non-401 response is a violation; transport failures are warnings,
* never violations (probe inconclusive). Suspended systems skip the probe.
* ACCESS-02 — stale suspension: a marker older than ACCESS_SUSPENSION_STALE_MS
* is almost certainly orphaned → warning.
*/
export async function checkAccessProtection(
entry: SystemConfig,
accessState: Pick,
violations: SternsystemViolation[],
warnings: Array<{ systemId: string; field: string; message: string }>,
probe: typeof probeAccessProtection = probeAccessProtection,
): Promise {
const desiredPin = accessState.accessPin;
const suspension = accessState.accessPinSuspended ?? null;
if (suspension !== null) {
const suspendedAt = Date.parse(suspension.at);
if (Number.isNaN(suspendedAt)) {
warnings.push({
systemId: entry.id,
field: "ACCESS-02",
message: `accessPinSuspended.at is unparseable ("${suspension.at}", by ${suspension.by}) — age unknown; run: pnpm exec werkstatt run leitstand.access.resume --site ${entry.id}`,
});
} else if (Date.now() - suspendedAt > ACCESS_SUSPENSION_STALE_MS) {
warnings.push({
systemId: entry.id,
field: "ACCESS-02",
message: `accessPinSuspended since ${suspension.at} (by ${suspension.by}) — stale > 1h; run: pnpm exec werkstatt run leitstand.access.resume --site ${entry.id}`,
});
}
}
if (desiredPin !== null && suspension === null) {
for (const channelName of ["dev", "alt"] as const) {
const channelUrl = entry.deployment?.channels[channelName]?.url;
if (!channelUrl) continue;
const result = await probe(channelUrl);
if (result.outcome === "transport-error") {
warnings.push({
systemId: entry.id,
field: "ACCESS-01",
message: `access protection probe inconclusive for ${channelUrl}: ${result.error}`,
});
} else if (result.status !== 401) {
violations.push({
systemId: entry.id,
rule: "ACCESS-01",
message: `accessPin declared but ${channelUrl} returned ${result.status} (expected 401) — run: pnpm exec werkstatt run leitstand.access.resume --site ${entry.id}`,
});
}
}
}
}
export async function runSternsystemValidate(
input: KernelCommandInput,
context: KernelRuntimeContext,
): Promise> {
const { workspaceRoot, logger } = context;
const filterId = flagString(input, "id");
const { systems: allSystems, errors: discoveryErrors } = await discoverSystems(workspaceRoot);
const violations: SternsystemViolation[] = [];
const warnings: Array<{ systemId: string; field: string; message: string }> = [];
if (discoveryErrors.length > 0) {
for (const err of discoveryErrors) {
violations.push({
systemId: err.id,
rule: "discovery-error",
message: `Failed to read system-config.yaml for '${err.id}': ${err.error}`,
});
}
}
const systems = filterId ? allSystems.filter((s) => s.id === filterId) : allSystems;
if (filterId && systems.length === 0) {
throw new Error(`[sternsystem.validate] id '${filterId}' not found in ../systems-cache/`);
}
const seenIds = new Set();
const seenStars = new Set();
let withOwner = 0;
let withoutOwner = 0;
for (const entry of systems) {
// Registry invariants
if (seenIds.has(entry.id)) {
violations.push({
systemId: entry.id,
rule: "unique-id",
message: `duplicate id '${entry.id}' in registry`,
});
}
seenIds.add(entry.id);
if (entry.owner) {
withOwner++;
} else {
withoutOwner++;
warnings.push({
systemId: entry.id,
field: "owner",
message: "owner field not set; Studio Gate cannot verify ownership for this site",
});
}
if (entry.status !== "archived" && seenStars.has(entry.cosmicStar)) {
violations.push({
systemId: entry.id,
rule: "unique-cosmicStar",
message: `cosmicStar '${entry.cosmicStar}' is used by multiple active/registered systems`,
});
}
seenStars.add(entry.cosmicStar);
if (!(StarCatalog as readonly string[]).includes(entry.cosmicStar)) {
violations.push({
systemId: entry.id,
rule: "valid-cosmicStar",
message: `cosmicStar '${entry.cosmicStar}' is not in StarCatalog`,
});
}
// RFC-0902: TLD suffix check
if (hasTldSuffix(entry.id)) {
violations.push({
systemId: entry.id,
rule: "STERN-ID-TLD",
message: `Sternsystem ID '${entry.id}' ends in a TLD suffix — use the business ID without domain TLD (e.g. 'warpgogol' not 'warpgogol-com')`,
});
}
// apps/ collision
if (await hasAppsCollision(workspaceRoot, entry.id, context.io)) {
violations.push({
systemId: entry.id,
rule: "apps-collision",
message: `id '${entry.id}' matches existing apps/${entry.id}/ — extraction incomplete`,
});
}
// Bundle contract (if cache clone exists)
const cacheDir = resolveMirrors(workspaceRoot, entry).cachePath;
const bundleViolations = await checkBundleContract(cacheDir, entry.id, context.io);
violations.push(...bundleViolations);
// Pin file validation (if cache clone exists)
const pinPath = path.join(cacheDir, "system.pin.json");
if (await context.io.exists(pinPath)) {
try {
const raw = await context.io.readFile(pinPath);
const parsed = JSON.parse(raw);
systemPinSchema.parse(parsed);
const pin = parsed as { platform: { version: string }; systemId: string };
if (pin.platform.version !== entry.pinnedPlatform) {
violations.push({
systemId: entry.id,
rule: "pin-version-mismatch",
message: `system.pin.json platform.version '${pin.platform.version}' does not match registry pinnedPlatform '${entry.pinnedPlatform}'`,
});
}
if (pin.systemId !== entry.id) {
violations.push({
systemId: entry.id,
rule: "pin-id-mismatch",
message: `system.pin.json systemId '${pin.systemId}' does not match registry id '${entry.id}'`,
});
}
} catch (err) {
violations.push({
systemId: entry.id,
rule: "pin-parse",
message: `system.pin.json parse failed: ${(err as Error).message}`,
});
}
} else if (entry.status === "active") {
violations.push({
systemId: entry.id,
rule: "pin-missing",
message: `active system '${entry.id}' has no system.pin.json in cache clone`,
});
}
// RFC-0666: secretsFile field is removed — reject if still present in any channel
if (entry.deployment?.channels) {
for (const [ch, chConfig] of Object.entries(entry.deployment.channels)) {
if (chConfig?.secretsFile) {
violations.push({
systemId: entry.id,
rule: "secretsFile-removed",
message: `channel '${ch}' still contains 'secretsFile' field — remove it. See RFC-0666.`,
});
}
}
}
// RFC-0574: validate mirror topology — mirrors[0] is cache, mirrors[1] is bare, mirrors[2+] are external
if (entry.mirrors.length < 1) {
violations.push({
systemId: entry.id,
rule: "mirrors-empty",
message: `system '${entry.id}' has no mirrors — at least 1 mirror (cache clone) is required`,
});
}
if (entry.mirrors.length >= 1 && entry.mirrors[0].storageType !== "non-bare") {
violations.push({
systemId: entry.id,
rule: "cache-must-be-non-bare",
message: `mirrors[0] must have storageType 'non-bare' (got '${entry.mirrors[0].storageType}') — cache clone must be a working tree for git push`,
});
}
for (let i = 0; i < entry.mirrors.length; i++) {
const m = entry.mirrors[i];
if (m.storageType === "bundle" && isGitAccessible(m.path)) {
violations.push({
systemId: entry.id,
rule: "bundle-no-git-protocol",
message: `mirrors[${i}] has storageType 'bundle' but uses git-accessible protocol '${m.path}' — bundle mirrors must not use git protocols`,
});
}
}
if (entry.mirrors.length > 1) {
const bareMirror = entry.mirrors[1];
const bareRepoPath = resolveMirrorPath(workspaceRoot, bareMirror.path);
if (await context.io.exists(bareRepoPath)) {
try {
// sternsystem.sync names the remote for mirrors[2] `mirror-0`
// (mirror- per external mirror); older systems also carry a
// legacy `mirror` remote — accept either, prefer the sync name.
let remoteResult = await context.io.exec("git", ["remote", "get-url", "mirror-0"], {
cwd: bareRepoPath,
});
if (remoteResult.exitCode !== 0) {
remoteResult = await context.io.exec("git", ["remote", "get-url", "mirror"], {
cwd: bareRepoPath,
});
}
if (remoteResult.exitCode !== 0) throw new Error(remoteResult.stderr.trim());
const remoteUrl = remoteResult.stdout.trim();
if (entry.mirrors.length > 2 && remoteUrl !== entry.mirrors[2].path) {
violations.push({
systemId: entry.id,
rule: "mirror-remote-mismatch",
message: `mirror remote URL '${remoteUrl}' does not match registry mirrors[2] '${entry.mirrors[2].path}'`,
});
}
} catch {
violations.push({
systemId: entry.id,
rule: "mirror-remote-missing",
message: `mirror remote not configured in bare repo at ${bareRepoPath}`,
});
}
}
if (entry.mirrors.length > 2) {
const externalMirror = entry.mirrors[2].path;
if (/https:\/\/[^:]+:[^@]+@/.test(externalMirror)) {
violations.push({
systemId: entry.id,
rule: "mirror-credentials",
message: `external mirror URL contains embedded credentials — use SSH URL instead`,
});
}
}
}
// RFC-0648: branch-convention rule — cache clone and bare repo must use 'main' branch
const cacheGitDir = path.join(cacheDir, ".git");
if (await context.io.exists(cacheGitDir)) {
try {
const branchResult = await context.io.exec("git", ["symbolic-ref", "HEAD"], {
cwd: cacheDir,
});
if (branchResult.exitCode !== 0) throw new Error(branchResult.stderr.trim());
const cacheBranch = branchResult.stdout.replace("refs/heads/", "").trim();
if (cacheBranch !== "main") {
violations.push({
systemId: entry.id,
rule: "branch-convention",
message: `cache clone branch is '${cacheBranch}', expected 'main' — run: git -C ${cacheDir} branch -m ${cacheBranch} main`,
});
}
} catch {
violations.push({
systemId: entry.id,
rule: "branch-convention",
message: `cache clone at ${cacheDir} has no resolvable HEAD (detached or corrupt) — expected branch 'main'`,
});
}
}
if (entry.mirrors.length > 1) {
const bareMirror = entry.mirrors[1];
const bareRepoPath = resolveMirrorPath(workspaceRoot, bareMirror.path);
if (await context.io.exists(bareRepoPath)) {
try {
const bareBranchResult = await context.io.exec("git", ["symbolic-ref", "HEAD"], {
cwd: bareRepoPath,
});
if (bareBranchResult.exitCode !== 0) throw new Error(bareBranchResult.stderr.trim());
const bareBranch = bareBranchResult.stdout.replace("refs/heads/", "").trim();
if (bareBranch !== "main") {
violations.push({
systemId: entry.id,
rule: "branch-convention",
message: `bare repo branch is '${bareBranch}', expected 'main' — run: git -C ${bareRepoPath} symbolic-ref HEAD refs/heads/main`,
});
}
} catch {
violations.push({
systemId: entry.id,
rule: "branch-convention",
message: `bare repo at ${bareRepoPath} has no resolvable HEAD (detached or corrupt) — expected branch 'main'`,
});
}
}
}
// RFC-0520: Bordbuch-vs-git-log check delegated to evaluateExternalEditGate
if (await bordbuchFileExists(cacheDir, context.io)) {
try {
const bordbuchPath = bordbuchPathFor(cacheDir);
const { bordbuchEntries, rangeShas, gitLogShas, gitLogSubjects, externalEditBaselineSha } =
await collectExternalEditInputs(cacheDir, bordbuchPath, context.io);
const guardResult = evaluateExternalEditGate({
systemId: entry.id,
bordbuchEntries,
gitLogShas,
gitLogSubjects,
rangeShas,
externalEditBaselineSha,
});
if (guardResult.verdict === "fail") {
for (const v of guardResult.violations) {
violations.push({ systemId: v.systemId!, rule: v.rule, message: v.message });
}
}
} catch {
// Bordbuch read failed — skip
}
}
// RFC-0792: YAML syntax checking for all top-level YAML files in cache clone
const yamlViolations = await validateYamlFiles(cacheDir, entry.id, context.io);
violations.push(...yamlViolations);
// RFC-0870: Manifest presence check — committed generated manifests must exist in cache clone HEAD
const manifestViolations = await checkManifestPresence(cacheDir, entry.id, context.io);
violations.push(...manifestViolations);
// ADR-0088: JRN-01 — journal mission attribution in cache clone operations/
const journalViolations = await checkJournalAttribution(
cacheDir,
entry.id,
warnings,
context.io,
);
violations.push(...journalViolations);
// ADR-0082: cache-clone generated drift — committed generated files in the
// cache clone are drift-checked via generated.drift.validate --target=cache-clone.
// RFC-1149 phase 1b: DRIFT-02 (generator lacks dryRun support) is blocking —
// all DRIFT-02 generators are migrated to context.io. DRIFT-01 (content
// drift) stays a warning: pre-existing drift needs a one-time cleanup pass
// before it can go blocking (see ADR consequences).
if (await context.io.exists(cacheDir)) {
try {
const { executeKernelCommand } = await import("@warpgogol/werkstatt-engine/kernel");
const driftReport = (await executeKernelCommand({
workspaceRoot,
commandName: "generated.drift.validate",
argv: [`--site=${entry.id}`, "--target=cache-clone"],
outputFormat: "json",
})) as {
data?: {
diagnostics?: Array<{
ruleId?: string;
file?: string;
message?: string;
}>;
};
};
const driftDiagnostics = driftReport?.data?.diagnostics ?? [];
routeDriftDiagnostics(driftDiagnostics, entry.id, violations, warnings);
} catch {
// Non-fatal — drift check skipped (site plugin or generators unavailable)
}
}
// RFC-0822: ENV-PERSIST-01 warning — cache clone lacks .env* but workpiece has them
try {
const cacheEnvFiles = await collectEnvFiles(cacheDir);
if (cacheEnvFiles.length === 0) {
const state = await readSystemState(workspaceRoot, entry.id);
if (state.currentMission) {
const workpieceDir = resolveWorkpiecePath(workspaceRoot, state.currentMission);
if (await context.io.exists(workpieceDir)) {
const workpieceEnvFiles = await collectEnvFiles(workpieceDir);
if (workpieceEnvFiles.length > 0) {
warnings.push({
systemId: entry.id,
field: "ENV-PERSIST-01",
message: `Cache clone for system '${entry.id}' has no .env files but workpiece has ${workpieceEnvFiles.length} — run mission.close to persist secrets`,
});
}
}
}
}
} catch {
// Non-fatal — env check skipped
}
// RFC-0966: PASSPORT-01/02/03 — signed site passport checks
try {
const passportDoc = await readPassport(workspaceRoot, entry.id);
if (!passportDoc) {
const state = await readSystemState(workspaceRoot, entry.id);
if (state.passportRequired) {
violations.push({
systemId: entry.id,
rule: "PASSPORT-01",
message: `passport.json missing but passportRequired is true — run: pnpm exec werkstatt run sternsystem.passport.generate --id ${entry.id}`,
});
} else {
warnings.push({
systemId: entry.id,
field: "PASSPORT-01",
message: `passport.json not found — run: pnpm exec werkstatt run sternsystem.passport.generate --id ${entry.id}`,
});
}
} else {
const { valid, errors } = await verifyPassport(passportDoc);
if (!valid) {
for (const err of errors) {
violations.push({
systemId: entry.id,
rule: "PASSPORT-02",
message: err,
});
}
}
// PASSPORT-03: resource drift check (non-fatal warning)
try {
const freshPayload = await buildPassportPayload({
systemId: entry.id,
werkstattRoot: workspaceRoot,
creatorIdentity: passportDoc.payload.creator.identity,
creatorPublicKey: passportDoc.payload.creator.publicKey,
});
freshPayload.provenance.createdAt = passportDoc.payload.provenance.createdAt;
freshPayload.provenance.generatedAt = passportDoc.payload.provenance.generatedAt;
const freshHash = computePassportHash(freshPayload);
if (freshHash !== passportDoc.passportHash) {
let isToleratedDrift = false;
try {
const entries = await readBordbuch(workspaceRoot, entry.id);
const recordedHead = passportDoc.payload.provenance.bordbuchHead;
// ADR-0092: a head drift caused only by bookkeeping entries
// (mirror-sync — propagation of already-committed state) does not
// stale the passport. Reproducing the signed hash with the recorded
// head pinned proves nothing else drifted alongside.
if (
freshPayload.provenance.bordbuchHead !== recordedHead &&
isBordbuchDriftBookkeepingOnly(entries, recordedHead)
) {
const tolerated = {
...freshPayload,
provenance: {
...freshPayload.provenance,
bordbuchHead: recordedHead,
},
};
isToleratedDrift = computePassportHash(tolerated) === passportDoc.passportHash;
}
if (!isToleratedDrift) {
// Tolerate stale bordbuchHead right after a handover — the passport is
// generated before the handover bordbuch entry is appended, so the head
// legitimately doesn't include it. Check if the only difference is the
// bordbuch head pointing to the entry before the latest handover event.
// ADR-0092: trailing bookkeeping entries (mirror-sync) are stripped
// before locating the handover, so a sync after handover still counts.
const significant = dropTrailingBookkeepingEntries(entries);
if (
significant.length > 0 &&
significant[significant.length - 1].kind === "handover"
) {
// The latest state-significant bordbuch entry is a handover event —
// check if the passport's bordbuchHead matches the entry just before it
const prevEntry = significant[significant.length - 2];
const expectedHead = prevEntry ? prevEntry.hash : "";
if (recordedHead === expectedHead) {
isToleratedDrift = true;
}
}
}
} catch {
// Bordbuch read failed — can't determine, treat as real drift
}
if (!isToleratedDrift) {
warnings.push({
systemId: entry.id,
field: "PASSPORT-03",
message: `passport content drift detected — bordbuchHead or resources changed since last generation. Run: pnpm exec werkstatt run sternsystem.passport.generate --id ${entry.id}`,
});
}
}
} catch {
// Non-fatal — drift check skipped
}
}
} catch {
// Non-fatal — passport check skipped
}
// RFC-0967: OWNERSHIP-01 — fleet ownership registry check
try {
const registryUrl = process.env.FLEET_OWNERSHIP_REGISTRY_URL;
if (!registryUrl) {
// Registry not configured — skip
} else {
const { verifyOwnership, OwnershipError } = await import("../fleet/ownership-registry.ts");
try {
const result = await verifyOwnership({
systemId: entry.id,
werkstattRoot: workspaceRoot,
registryUrl,
});
if (!result.registered) {
const state = await readSystemState(workspaceRoot, entry.id);
if (state.ownershipRequired) {
violations.push({
systemId: entry.id,
rule: "OWNERSHIP-01",
message: `Site not registered in fleet ownership registry but ownershipRequired is true — run: pnpm exec werkstatt run fleet.ownership.register --id ${entry.id}`,
});
} else {
warnings.push({
systemId: entry.id,
field: "OWNERSHIP-01",
message: `Site not registered in fleet ownership registry — run: pnpm exec werkstatt run fleet.ownership.register --id ${entry.id}`,
});
}
} else if (result.conflictsWith) {
warnings.push({
systemId: entry.id,
field: "OWNERSHIP-01",
message: `Ownership conflict: site registered to instance ${result.conflictsWith}, not this instance (OWNERSHIP-03)`,
});
}
} catch (err) {
if (err instanceof OwnershipError) {
warnings.push({
systemId: entry.id,
field: "OWNERSHIP-01",
message: `Registry check skipped (${err.code}): ${err.message}`,
});
}
}
}
} catch {
// Non-fatal — ownership check skipped
}
// RFC-0968: HANDOVER-01/02/03/04/05 — sternsystem handover protocol
try {
const authorization = await readAuthorization(workspaceRoot, entry.id);
if (authorization) {
// HANDOVER-01: pending handover blocks missions
violations.push({
systemId: entry.id,
rule: "HANDOVER-01",
message: `pending handover — missions blocked; run sternsystem.handover.complete or sternsystem.handover.cancel`,
});
// HANDOVER-02: authorization expired
if (isAuthorizationExpired(authorization.payload.expiresAt)) {
violations.push({
systemId: entry.id,
rule: "HANDOVER-02",
message: `authorization expired at ${authorization.payload.expiresAt} — sender must re-issue`,
});
}
// HANDOVER-03: passport hash mismatch
const currentPassport = await readPassport(workspaceRoot, entry.id);
if (currentPassport) {
if (authorization.payload.passportHash !== currentPassport.passportHash) {
violations.push({
systemId: entry.id,
rule: "HANDOVER-03",
message: `passport changed after authorization — sender must re-issue`,
});
}
}
// HANDOVER-04: recipient key mismatch (warning, not blocking)
const privateKeyEnv = process.env.SIGNING_PRIVATE_KEY;
const privateKeyPath = process.env.SIGNING_PRIVATE_KEY_PATH;
if (privateKeyEnv || privateKeyPath) {
try {
const { loadPrivateKey } = await import("@warpgogol/werkstatt-engine/signing");
const { derivePublicKey } = await import("./passport.ts");
let privateKeyBytes: Uint8Array;
if (privateKeyEnv) {
privateKeyBytes = await loadPrivateKey({ pem: privateKeyEnv });
} else {
privateKeyBytes = await loadPrivateKey({
filePath: privateKeyPath!,
encoding: "pem",
});
}
const derivedPubKey = await derivePublicKey(privateKeyBytes);
if (authorization.payload.recipient.publicKey !== derivedPubKey) {
warnings.push({
systemId: entry.id,
field: "HANDOVER-04",
message: `recipient key mismatch — this instance is not the authorized recipient`,
});
}
} catch {
// Non-fatal — key derivation failed
}
}
} else {
// HANDOVER-05: no authorization file but passport creator doesn't match bordbuch's latest handover event
try {
const entries = await readBordbuch(workspaceRoot, entry.id);
const handoverEntries = entries.filter((e) => e.kind === "handover");
if (handoverEntries.length > 0) {
const latestHandover = handoverEntries[handoverEntries.length - 1];
const currentPassport = await readPassport(workspaceRoot, entry.id);
if (currentPassport) {
const metadata = latestHandover.metadata as Record | undefined;
const expectedNewPassportHash = metadata?.newPassportHash as string | undefined;
if (
expectedNewPassportHash &&
expectedNewPassportHash !== currentPassport.passportHash
) {
violations.push({
systemId: entry.id,
rule: "HANDOVER-05",
message: `incomplete handover — passport regenerated but bordbuch event references a different passport hash; re-run sternsystem.handover.complete`,
});
}
}
}
} catch {
// Non-fatal — bordbuch read failed
}
}
} catch {
// Non-fatal — handover check skipped
}
// RFC-1284: ACCESS-01/02 — desired-vs-live access protection checks.
try {
const accessState = await readSystemState(workspaceRoot, entry.id, context.io);
await checkAccessProtection(entry, accessState, violations, warnings);
} catch {
// Non-fatal — access protection checks skipped
}
// RFC-1297: LIVE-01/02 — recorded (system-state.yaml lastPropagated) vs
// live (build-identity.json) release drift. Same transport-failure
// semantics as ACCESS-01: inconclusive probes warn, never violate.
try {
const liveStates = await readLiveChannelStates({
workspaceRoot,
systemId: entry.id,
io: context.io,
});
for (const d of liveDriftDiagnostics(liveStates, entry.id)) {
if (d.severity === "error") {
violations.push({
systemId: entry.id,
rule: d.ruleId,
message: d.fixHint ? `${d.message} — ${d.fixHint}` : d.message,
});
} else {
warnings.push({ systemId: entry.id, field: d.ruleId, message: d.message });
}
}
} catch {
// Non-fatal — live drift checks skipped
}
// RFC-1034: STERN-CONFIG-RESOLVE-01 — check if cache clone's kernel.config.ts can be loaded
try {
const cachePath = resolveMirrors(workspaceRoot, entry).cachePath;
if (cachePath) {
const configPath = path.join(cachePath, "tools", "kernel.config.ts");
if (await context.io.exists(configPath)) {
try {
const { loadAppRuntime } = await import("../kernel/runtime/registry.ts");
await loadAppRuntime(workspaceRoot, {
name: entry.id,
directory: cachePath,
configPath: "tools/kernel.config.ts",
toolsDirectory: path.join(cachePath, "tools"),
});
} catch (configErr) {
warnings.push({
systemId: entry.id,
field: "STERN-CONFIG-RESOLVE-01",
message: `cache clone kernel.config.ts failed to load: ${configErr instanceof Error ? configErr.message : String(configErr)} — run pnpm install in the cache clone or re-materialize`,
});
}
}
}
} catch {
// Non-fatal — config resolution check skipped
}
}
const validated = systems.length;
if (violations.length === 0) {
logger.success(
`[sternsystem.validate] ${validated} system${validated === 1 ? "" : "s"} validated, 0 violations`,
);
} else {
logger.error(
`[sternsystem.validate] ${validated} system${validated === 1 ? "" : "s"} validated, ${violations.length} violation${violations.length === 1 ? "" : "s"}`,
);
for (const v of violations) {
logger.error(` [${v.rule}] ${v.systemId}: ${v.message}`);
}
}
for (const w of warnings) {
logger.warn(` [${w.field}] ${w.systemId}: ${w.message}`);
}
return {
data: { validated, violations, warnings, withOwner, withoutOwner },
exitCode: violations.length > 0 ? 1 : 0,
summary: `[sternsystem.validate] ${validated} system${validated === 1 ? "" : "s"} validated, ${violations.length} violation${violations.length === 1 ? "" : "s"}, ${withOwner} with owner, ${withoutOwner} without owner`,
nextSteps:
violations.length > 0
? [
{
action: `Fix the ${violations.length} validation violation${violations.length === 1 ? "" : "s"} above, then re-run: pnpm exec werkstatt run sternsystem.validate`,
kind: "required",
},
]
: undefined,
};
}