/*
RFC-0899/RFC-1284: Leitstand access protection commands — protect,
unprotect, suspend, resume, status. Manages 4-digit PIN secrets for dev/alt
subdomain Workers via wrangler secret put/delete. system-state.yaml accessPin
is the authoritative desired declaration; accessPinSuspended marks legitimate
transient divergence of the projection (RFC-1284, DNA-111).
- Do not set secrets on the main channel Worker — main domain is never protected.
- Do not store the PIN in git — system-state.yaml stores only null or the 4-digit string.
- Do not use wrangler.toml — use temporary wrangler.jsonc only.
- leitstand.access.suspend is the only writer of accessPinSuspended besides the
normalizing clear in protect/unprotect/resume — no other command touches the marker.
- RFC-1249: wire werkstatt.commands.validate into packages.check + retire CMD-OUTPUT debt
- RFC-1254: drop unused io-migration leftovers
- RFC-1284: suspend/resume commands, crash-safe marker, status tri-state
accessPin becomes desired-state authority: suspend deletes the projection
secret then writes accessPinSuspended (marker only when every channel delete
succeeds — a kill in between leaves ACCESS-01-visible drift); resume re-puts
the pin and clears the marker only on full success; protect/unprotect
normalize stale markers; status reports protected|suspended|unprotected.
- 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-1327: review r1 — lock-safe reconcile gate, residue sweep, honest idempotency classes
RFC-0899, RFC-1065, RFC-1097
*/
import { join } from "node:path";
import type {
KernelCommandInput,
KernelCommandResult,
KernelRuntimeContext,
WorkspaceIO,
} from "@warpgogol/werkstatt-engine/kernel";
import type { SystemConfig, SystemState } from "@warpgogol/werkstatt-engine/schemas";
import {
readSystemConfigSmart,
readSystemState,
writeSystemState,
resolveCacheClonePath,
} from "../sternsystem/registry-io.ts";
import { sourceDotenv, filterEnv } from "./adapters/index.ts";
import { runWranglerSecretPut, runWranglerSecretDelete } from "./adapters/wrangler-secrets.ts";
type ChannelName = "dev" | "alt";
type AccessProtectionStatus = "protected" | "suspended" | "unprotected";
interface ChannelSecretResult {
channel: ChannelName;
workerName: string;
success: boolean;
error?: string;
}
function validatePin(pin: string): void {
if (!/^\d{4}$/.test(pin)) {
throw new Error(`[leitstand.access] PIN must be exactly 4 digits, got: "${pin}"`);
}
}
function generateRandomPin(): string {
return Math.floor(1000 + Math.random() * 9000).toString();
}
function flagString(input: KernelCommandInput, name: string): string | undefined {
const v = input.flags[name];
return typeof v === "string" ? v : undefined;
}
function requireSite(input: KernelCommandInput, commandName: string): string {
const systemId = flagString(input, "site") ?? flagString(input, "system");
if (!systemId) throw new Error(`[${commandName}] --site is required`);
return systemId;
}
// Type helper for channel iteration
type deploymentChannelSchema_t = {
channel: ChannelName;
workerName: string;
url: string;
secretsFile?: string;
};
function accessChannels(
systemConfig: SystemConfig,
commandName: string,
systemId: string,
): deploymentChannelSchema_t[] {
if (!systemConfig.deployment) {
throw new Error(
`[${commandName}] system '${systemId}' has no deployment config in system-config.yaml`,
);
}
return [
{ channel: "dev" as const, ...systemConfig.deployment.channels.dev },
{ channel: "alt" as const, ...systemConfig.deployment.channels.alt },
];
}
async function buildSecretsEnv(
workspaceRoot: string,
systemId: string,
io: WorkspaceIO,
): Promise> {
const cacheCloneDir = resolveCacheClonePath(workspaceRoot, systemId);
const envPath = join(cacheCloneDir, ".env");
const secretsEnv = (await io.exists(envPath)) ? await sourceDotenv(envPath) : {};
return {
...filterEnv(process.env as Record),
...secretsEnv,
};
}
async function putPinOnChannels(
channels: deploymentChannelSchema_t[],
pin: string,
env: Record,
workspaceRoot: string,
): Promise {
const results: ChannelSecretResult[] = [];
for (const ch of channels) {
const result = await runWranglerSecretPut(ch.workerName, "ACCESS_PIN", pin, env, workspaceRoot);
const channelResult: ChannelSecretResult = {
channel: ch.channel,
workerName: ch.workerName,
success: result.exitCode === 0,
};
if (result.exitCode !== 0) {
channelResult.error =
result.stderr.trim() || result.stdout.trim() || `exit code ${result.exitCode}`;
}
results.push(channelResult);
}
return results;
}
async function deletePinFromChannels(
channels: deploymentChannelSchema_t[],
env: Record,
workspaceRoot: string,
): Promise {
const results: ChannelSecretResult[] = [];
for (const ch of channels) {
const result = await runWranglerSecretDelete(ch.workerName, "ACCESS_PIN", env, workspaceRoot);
const channelResult: ChannelSecretResult = {
channel: ch.channel,
workerName: ch.workerName,
// wrangler secret delete returns non-zero if secret doesn't exist — treat as success (idempotent)
success:
result.exitCode === 0 ||
result.stderr.includes("not found") ||
result.stdout.includes("not found"),
};
if (!channelResult.success && result.exitCode !== 0) {
channelResult.error =
result.stderr.trim() || result.stdout.trim() || `exit code ${result.exitCode}`;
}
results.push(channelResult);
}
return results;
}
export async function runLeitstandAccessProtect(
input: KernelCommandInput,
context: KernelRuntimeContext,
): Promise> {
const systemId = requireSite(input, "leitstand.access.protect");
const pinArg = flagString(input, "pin");
const workspaceRoot = context.workspaceRoot;
// RFC-1327: re-running without an explicit --pin while a PIN is already
// recorded must not silently rotate the secret — converge to a no-op.
if (!pinArg) {
const existing = await readSystemState(workspaceRoot, systemId);
if (existing.accessPin) {
return {
data: { pin: existing.accessPin, channels: [] },
summary: `[leitstand.access.protect] ${systemId} already protected — no-op (pass --pin to rotate)`,
exitCode: undefined,
};
}
}
const pin = pinArg ?? generateRandomPin();
validatePin(pin);
const systemConfig = await readSystemConfigSmart(workspaceRoot, systemId);
const channels = accessChannels(systemConfig, "leitstand.access.protect", systemId);
const env = await buildSecretsEnv(workspaceRoot, systemId, context.io);
const results = await putPinOnChannels(channels, pin, env, workspaceRoot);
const state = await readSystemState(workspaceRoot, systemId);
state.accessPin = pin;
// RFC-1284: normalize stale suspension — a freshly asserted pin is not suspended.
state.accessPinSuspended = null;
await writeSystemState(workspaceRoot, systemId, state);
const allSuccess = results.every((r) => r.success);
const summary = allSuccess
? `[leitstand.access.protect] PIN set on ${results.length} channel(s) for ${systemId}`
: `[leitstand.access.protect] PIN set with failures: ${results
.filter((r) => !r.success)
.map((r) => r.channel)
.join(", ")}`;
return {
data: { pin, channels: results },
summary,
exitCode: allSuccess ? undefined : 1,
};
}
export async function runLeitstandAccessUnprotect(
input: KernelCommandInput,
context: KernelRuntimeContext,
): Promise> {
const systemId = requireSite(input, "leitstand.access.unprotect");
const workspaceRoot = context.workspaceRoot;
const systemConfig = await readSystemConfigSmart(workspaceRoot, systemId);
const channels = accessChannels(systemConfig, "leitstand.access.unprotect", systemId);
const env = await buildSecretsEnv(workspaceRoot, systemId, context.io);
const results = await deletePinFromChannels(channels, env, workspaceRoot);
const state = await readSystemState(workspaceRoot, systemId);
state.accessPin = null;
// RFC-1284: permanent unprotection dissolves any suspension marker.
state.accessPinSuspended = null;
await writeSystemState(workspaceRoot, systemId, state);
const allSuccess = results.every((r) => r.success);
const summary = allSuccess
? `[leitstand.access.unprotect] PIN removed from ${results.length} channel(s) for ${systemId}`
: `[leitstand.access.unprotect] PIN removal with failures: ${results
.filter((r) => !r.success)
.map((r) => r.channel)
.join(", ")}`;
return {
data: { channels: results },
summary,
exitCode: allSuccess ? undefined : 1,
};
}
// RFC-1284: transient suspension — deletes the projection secret FIRST, then
// writes the marker only when every channel deleted. A kill in between leaves
// detectable drift (pin declared, secret absent, no marker) that ACCESS-01
// raises — the inversion errs toward alarm, not silence.
export async function runLeitstandAccessSuspend(
input: KernelCommandInput,
context: KernelRuntimeContext,
): Promise<
KernelCommandResult<{
channels: ChannelSecretResult[];
suspended: NonNullable | null;
}>
> {
const systemId = requireSite(input, "leitstand.access.suspend");
const workspaceRoot = context.workspaceRoot;
// Marker authority is the git-tracked cache clone — non-smart reads/writes
// keep the contract single-store even while a mission workpiece shadows
// system-state.yaml (REVIEW-CODE-2026-10-15-02 F1, decisions Q-6).
const state = await readSystemState(workspaceRoot, systemId);
if (state.accessPin === null) {
throw new Error(
`[leitstand.access.suspend] system '${systemId}' has accessPin: null — nothing to suspend. ` +
`Set a pin first: pnpm exec werkstatt run leitstand.access.protect --site ${systemId}`,
);
}
const systemConfig = await readSystemConfigSmart(workspaceRoot, systemId);
const channels = accessChannels(systemConfig, "leitstand.access.suspend", systemId);
const env = await buildSecretsEnv(workspaceRoot, systemId, context.io);
const results = await deletePinFromChannels(channels, env, workspaceRoot);
const allSuccess = results.every((r) => r.success);
let suspended: NonNullable | null = null;
if (allSuccess) {
// Idempotent: re-suspending refreshes the marker.
suspended = {
at: new Date().toISOString(),
by: flagString(input, "by") ?? "operator",
operationId: flagString(input, "operation-id") ?? null,
};
state.accessPinSuspended = suspended;
await writeSystemState(workspaceRoot, systemId, state);
}
const summary = allSuccess
? `[leitstand.access.suspend] PIN suspended on ${results.length} channel(s) for ${systemId} (by ${suspended!.by})`
: `[leitstand.access.suspend] suspension failed — no marker written, drift visible via ACCESS-01: ${results
.filter((r) => !r.success)
.map((r) => r.channel)
.join(", ")}`;
return {
data: { channels: results, suspended },
summary,
exitCode: allSuccess ? undefined : 1,
};
}
export async function runLeitstandAccessResume(
input: KernelCommandInput,
context: KernelRuntimeContext,
): Promise> {
const systemId = requireSite(input, "leitstand.access.resume");
const workspaceRoot = context.workspaceRoot;
const state = await readSystemState(workspaceRoot, systemId);
if (state.accessPinSuspended === null) {
return {
data: { channels: [], resumed: false },
summary: `[leitstand.access.resume] ${systemId}: not suspended — nothing to resume`,
exitCode: 0,
};
}
if (state.accessPin === null) {
throw new Error(
`[leitstand.access.resume] system '${systemId}' is suspended but accessPin is null — ` +
`the desired pin is unknown and cannot be resumed. ` +
`Run: pnpm exec werkstatt run leitstand.access.protect --site ${systemId} --pin <4-digit>`,
);
}
const systemConfig = await readSystemConfigSmart(workspaceRoot, systemId);
const channels = accessChannels(systemConfig, "leitstand.access.resume", systemId);
const env = await buildSecretsEnv(workspaceRoot, systemId, context.io);
const results = await putPinOnChannels(channels, state.accessPin, env, workspaceRoot);
const allSuccess = results.every((r) => r.success);
if (allSuccess) {
// Clear the marker only when the projection is fully re-asserted — a
// partial restore keeps the suspension declared so ACCESS-01 stays muted
// and ACCESS-02 keeps aging toward a warning.
state.accessPinSuspended = null;
await writeSystemState(workspaceRoot, systemId, state);
}
const summary = allSuccess
? `[leitstand.access.resume] PIN restored on ${results.length} channel(s) for ${systemId}`
: `[leitstand.access.resume] restore failed — suspension marker kept: ${results
.filter((r) => !r.success)
.map((r) => r.channel)
.join(", ")}`;
return {
data: { channels: results, resumed: allSuccess },
summary,
exitCode: allSuccess ? undefined : 1,
};
}
export async function runLeitstandAccessStatus(
input: KernelCommandInput,
context: KernelRuntimeContext,
): Promise<
KernelCommandResult<{
systemId: string;
status: AccessProtectionStatus;
accessPin: string | null;
suspended: SystemState["accessPinSuspended"];
}>
> {
const systemId = requireSite(input, "leitstand.access.status");
const workspaceRoot = context.workspaceRoot;
const state = await readSystemState(workspaceRoot, systemId);
const status: AccessProtectionStatus =
state.accessPin === null
? "unprotected"
: state.accessPinSuspended !== null
? "suspended"
: "protected";
return {
data: {
systemId,
status,
accessPin: state.accessPin,
suspended: state.accessPinSuspended,
},
exitCode: 0,
summary: `[leitstand.access.status] ${systemId}: ${status}`,
};
}