feat: add d601 recovery guardrails

Adds read-only D601 recovery diagnostics, fixture coverage, CLI wiring, and recovery hotfix runbook updates. Validated with recovery contract, check --files, scripts tsc, artifact matrix direct contract, and read-only live diagnostic.
This commit is contained in:
Lyon
2026-05-23 21:18:44 +08:00
committed by GitHub
parent 6c44f66289
commit e2646763c0
9 changed files with 1406 additions and 5 deletions
+1 -1
View File
@@ -45,7 +45,7 @@ UniDesk 是一个以主 server 为统一入口的分布式工作平台;本文
- `bun scripts/cli.ts help`:输出所有可用命令的 JSON 索引,详细规范见 `docs/reference/cli.md`
- `bun scripts/cli.ts --main-server-ip <ip> <command>`:默认通过公网 frontend 登录态远程执行调试、用户服务(底层命令名 `microservice`)、Code Queue 查询与节点自测命令,不要求主 server SSH key,详细规范见 `docs/reference/cli.md`
- `bun scripts/cli.ts config show`:校验并展示根目录 `config.json`,配置来源规则见 `docs/reference/config.md`
- `bun scripts/cli.ts check [--full|--files|--scripts-typecheck|--components|--compose|--logs|--rust]`:默认只运行轻量配置和 TypeScript 语法检查Rust backend-core 检查只能在 D601 CI/dev execution 中用 `UNIDESK_D601_RUST_CHECK=1` 开启,规则见 `docs/reference/dev-environment.md`
- `bun scripts/cli.ts check [--full|--files|--scripts-typecheck|--components|--compose|--logs|--recovery-guardrails|--rust]` / `bun scripts/cli.ts check recovery-guardrails`:默认只运行轻量配置和 TypeScript 语法检查;`check recovery-guardrails` 只读低噪声报告 D601 reboot 后 k3s/Code Queue hostPath、`/proc/mounts`、CRI sandbox 和 ContainerCreating 风险Rust backend-core 检查只能在 D601 CI/dev execution 中用 `UNIDESK_D601_RUST_CHECK=1` 开启,规则见 `docs/reference/dev-environment.md``docs/reference/devops-hygiene.md`
- `bun scripts/cli.ts server start`:以异步 job 启动 database、backend-core、frontend、provider-gateway、code-queue-mgr 和主 server 用户服务,部署规则见 `docs/reference/deployment.md`
- `bun scripts/cli.ts server status`:查询固定端口、swap 摘要、容器状态、健康检查和访问 URL,包含生产 frontend、dev frontend proxy 和 provider ingress,判定标准见 `docs/reference/deployment.md``docs/reference/dev-environment.md`
- `bun scripts/cli.ts server swap status|ensure [--path /swapfile] [--size 2GiB] [--dry-run]`:以 JSON 查看或幂等创建主 server swapfile`ensure` 输出 before/after、动作、持久化状态和 degraded/failed 详情,规则见 `docs/reference/deployment.md`
+1 -1
View File
@@ -10,7 +10,7 @@ CLI 可以从 `master` 快速演进,但必须兼容 `deploy.json` 固定的 CI
- 每个 CLI 命名空间必须支持 `help``--help``-h` 并返回 JSON,不得为了打印帮助而访问 runtime 服务、拉起交互会话或执行长时任务。
- `--main-server-ip <ip> <command>` 默认通过公网 frontend 登录态调用主 server 的同源 API 代理,不要求计算节点持有主 server SSH key;显式提供 `--main-server-key``--main-server-transport ssh` 时才使用旧 SSH 传输。
- `config show` 读取并校验根目录 `config.json`,不从环境变量、默认值或隐藏文件静默补配置。
- `check` 默认只执行轻量配置校验、Bun 版本检查和 Bun Transpiler 语法解析(覆盖 CLI 入口、主要 `scripts/` 模块和核心组件入口,不做类型推导);关键文件存在性、`scripts/` TypeScript 类型检查、`src/components/` TypeScript 类型检查、Docker Compose config日志轮转策略扫描默认不启用,分别通过 `--files``--scripts-typecheck``--components``--compose``--logs` 开启,或用 `--full` 一次性开启`--rust` 只允许在 D601 CI/dev execution 中配合 `UNIDESK_D601_RUST_CHECK=1` 使用,长期规则见 `docs/reference/dev-environment.md`
- `check` 默认只执行轻量配置校验、Bun 版本检查和 Bun Transpiler 语法解析(覆盖 CLI 入口、主要 `scripts/` 模块和核心组件入口,不做类型推导);关键文件存在性、`scripts/` TypeScript 类型检查、`src/components/` TypeScript 类型检查、Docker Compose config日志轮转策略扫描和 D601 recovery guardrails 默认不启用,分别通过 `--files``--scripts-typecheck``--components``--compose``--logs``--recovery-guardrails` 开启,或用 `--full` 一次性开启。`check recovery-guardrails` 是同一诊断的低噪声直接入口,报告 malformed `/proc/mounts`、kubelet validation risk、stale CRI sandbox count、Code Queue worktree/symlink、Code Queue/MDTODO hostPath 和 `ContainerCreating` 分类;它不得重启 k3s、删除 CRI sandbox、修改 hostPath、deploy/rollout 或 prune/reset`--rust` 只允许在 D601 CI/dev execution 中配合 `UNIDESK_D601_RUST_CHECK=1` 使用,长期规则见 `docs/reference/dev-environment.md``docs/reference/devops-hygiene.md`
- `server start` 创建异步 job,在后台执行 Docker 构建和启动;命令本身只负责返回 job id、日志路径和启动命令。
- `server stop` 创建异步 job,在后台停止固定 Compose project 中的全部 UniDesk 服务。
- `server status` 查询公开端口、受限宿主端口、内部端口、主机 swap 摘要、Compose 容器、core/frontend/dev-frontend/provider/database 健康检查和访问 URLD601 Code Queue 使用的 PostgreSQL/OA Event Flow host mapping 必须出现在受限宿主端口而不是无条件公开入口中。低内存主 server 上 `swap.warning` 非空时,先执行 `server swap status``server swap ensure`
+23
View File
@@ -35,6 +35,7 @@ Manual operations are allowed only when they are narrow, visible and followed by
- `deploy apply --service k3sctl-adapter` may update the k3s control bridge catalog through the documented local manifest exception in `docs/reference/deploy.md`.
- Host SSH/provider-gateway dispatch may start the `ci run-dev-e2e` short launcher described in `docs/reference/dev-ci-runner.md`; it must not carry large shell bodies or become a general deployment path.
- Read-only smoke checks such as `curl http://74.48.78.17:18083/health`, `server status`, `microservice proxy .../health` and `kubectl get` may validate state, but they do not replace desired-state and live-commit verification.
- `bun scripts/cli.ts check recovery-guardrails` and the equivalent `check --recovery-guardrails` gate are commander-safe read-only recovery diagnostic surfaces for D601 reboot incidents. They may read `/proc/mounts`, inspect local path metadata, parse committed k3s manifests, and run bounded read-only `kubectl get pods -A -o json` / `crictl pods -o json` probes when the tools are present. They must not restart k3s, delete pods or CRI sandboxes, apply manifests, rollout workloads, mutate hostPath directories, prune Docker state, or repair symlinks automatically.
- File Browser recovery may use the existing provider-local image-only/docker-run path only as a bounded repair path. Standardization requires first resolving `docker.io/filebrowser/filebrowser:v2.63.3` to an upstream manifest digest or a digest-verified local mirror, then validating the running container through the UniDesk private proxy.
Manual Secret/env/rollout repair is allowed only as a bounded runtime recovery path. It must have explicit authorization for the target environment and service, a narrow object scope, redacted evidence, an issue review trail and a durable source fix. Acceptable evidence includes object names, revision changes, health status, rollout status and redacted key presence; it must not include secret values, tokens, full env dumps or copy-pastable sensitive mutation commands.
@@ -43,6 +44,28 @@ Any manual repair that changes live credentials, env wiring, DNS/egress assumpti
If a manual repair is needed to unblock the platform, the durable fix must be committed and pushed, then redeployed or revalidated through the normal path. Do not preserve the repair only as hidden runtime state.
## D601 Recovery Hotfix Exception
D601 reboot recovery has a narrow hotfix exception because k3s, Code Queue and hostPath readiness can fail before normal UniDesk proxy/CD surfaces are healthy. The exception authorizes diagnosis and carefully scoped host repair only; it does not make live host edits a new deployment path.
Allowed read-only recovery checks:
- `bun scripts/cli.ts check recovery-guardrails` or `bun scripts/cli.ts check --recovery-guardrails` on the host or runner environment.
- `/proc/mounts` inspection for malformed Docker Desktop `/Docker/host` 9p rows that may break kubelet mount-table validation.
- `kubectl get/describe/logs/events` and `crictl pods -o json` as bounded observation.
- Path metadata checks for `/home/ubuntu/unidesk-code-queue-deploy`, `/home/ubuntu/cq-deploy`, Code Queue hostPath directories, `.codex` files, `.ssh`, `.agents/skills`, and MDTODO workspace/log paths.
Manual host hotfix may be considered only after the read-only output identifies a concrete redline and the operator has reviewed whether the target is source checkout, credential material, log/cache state, or user data. Examples include restoring a missing Git worktree from pushed remote state, recreating an intended compatibility symlink after confirming its target, restoring a missing runtime Secret source, or fixing a Docker Desktop/WSL mount-table condition. The repair must be recorded in the relevant issue or commander brief and followed by a source or runbook update when the root cause is durable.
Forbidden automatic recovery actions:
- `systemctl restart k3s`, `service k3s restart`, `kubectl delete pod`, `crictl rmp`, `crictl rm`, `docker system prune`, `docker volume prune`, recursive chmod/chown/rm under `/home/ubuntu`, and `git reset --hard` of a live worktree.
- Deleting CRI sandboxes or Kubernetes Pods because a diagnostic counted stale sandboxes.
- Creating, deleting or replacing MDTODO workspace content from Code Queue or the generic CLI. MDTODO hostPaths contain user-authored Markdown data.
- Treating `DirectoryOrCreate` as permission to mass-create parent trees or credential directories. Kubelet may create the final directory only when mount validation and parent permissions are already healthy; humans still decide user-data boundaries.
ClaudeQQ or direct user approval is required before any high-risk host action that restarts k3s/kubelet/Docker Desktop, deletes pods/sandboxes, changes credential or SSH paths, touches MDTODO workspace data, force-resets a worktree, changes production rollout state, or could interrupt active Code Queue tasks. If ClaudeQQ is unavailable, the operator must stop at the written plan and record the approval gap instead of silently executing the action.
## CI And Private Source Auth
Private repository access is part of the CI contract. `ci run` must not rely on unauthenticated HTTPS clone, an operator's local dirty worktree or an ad-hoc secret copied by hand into one PipelineRun.
+1
View File
@@ -296,6 +296,7 @@ D601 上必须显式使用原生 k3s kubeconfig`KUBECONFIG=/etc/rancher/k3s/k
D601 是 Windows + WSL Ubuntu + Docker Desktop 节点,Docker Desktop 当前 `LiveRestore=false` 时,机器或 Docker daemon 重启会停止容器,恢复链路必须同时覆盖 Windows 登录、WSL keepalive、Docker daemon ready、provider-gateway 和业务用户服务:
- 首选只读诊断入口:`bun scripts/cli.ts check recovery-guardrails`。该入口报告 malformed `/proc/mounts` Docker Desktop `/Docker/host` 9p 行、kubelet mount-table validation risk、stale CRI sandbox count、Code Queue deploy worktree/`/home/ubuntu/cq-deploy` symlink readiness、Code Queue/MDTODO k3s hostPath readiness、MDTODO adjacent hostPath 透明度和 `ContainerCreating` hostPath 分类。输出只给 safe read-only 证据、manual host hotfix redlines 和禁止自动动作;不得据此自动重启 k3s、删除 CRI sandbox、删除 Pod、修改 live hostPath、执行 deploy/rollout 或 destructive prune/reset。完整 hotfix exception、用户数据边界和 ClaudeQQ/用户请示条件见 `docs/reference/devops-hygiene.md`
- Windows 登录任务:计划任务 `UniDesk-D601-Autostart` 在用户 `DESKTOP-1MHOD9I\liang` 登录时运行 `C:\WINDOWS\System32\cmd.exe /c ""C:\Users\liang\AppData\Local\UniDesk\d601-autostart.cmd""`,工作目录为 `C:\Users\liang\AppData\Local\UniDesk`
- Windows launcher`C:\Users\liang\AppData\Local\UniDesk\d601-autostart.cmd` 先启动 `%ProgramFiles%\Docker\Docker\Docker Desktop.exe`,再执行 `C:\Windows\System32\wsl.exe -d Ubuntu -u ubuntu -- /bin/bash -lc "/home/ubuntu/.local/bin/unidesk-d601-autostart task"`D601 的 WSL distro 名必须写 `Ubuntu`,不能写成未验证的 `Ubuntu-22.04`
- WSL keepalive`/home/ubuntu/.local/bin/unidesk-d601-autostart` 使用 `~/.state/unidesk/d601-autostart.lock` 防重复,启动 WSL `sshd`,等待 Docker Desktop daemon 和原生 k3s 就绪,把 `unidesk-provider-gateway-D601` 修正为 `restart always` 且 running,然后调用 `/home/ubuntu/.local/bin/unidesk-microservice-autorecover boot`;进入常驻 watchdog 后每 300 秒重复检查 provider-gateway、Docker 直管服务和 k3s 代管服务。
+7 -1
View File
@@ -4,7 +4,7 @@ import { isRebuildableService, rebuildService, stackLogs, stackStatus, startStac
import { parseE2ERunOptions, runE2E } from "./src/e2e";
import { emitError, emitJson } from "./src/output";
import { jobWithTail, listJobs, listJobsSummary, readJob, runJob } from "./src/jobs";
import { checkHelp, parseCheckOptions, runChecks } from "./src/check";
import { checkHelp, parseCheckOptions, runChecks, runRecoveryGuardrailsCheck } from "./src/check";
import { runSsh } from "./src/ssh";
import { autoRemoteCiPublishUserServiceDryRunPlan, extractRemoteCliOptions, runRemoteCli } from "./src/remote";
import { runMicroserviceCommand } from "./src/microservices";
@@ -291,6 +291,12 @@ async function main(): Promise<void> {
emitJson(commandName, checkHelp());
return;
}
if (sub === "recovery-guardrails") {
const result = runRecoveryGuardrailsCheck(config);
emitJson(commandName, result, result.ok);
if (!result.ok) process.exitCode = 1;
return;
}
const result = runChecks(config, parseCheckOptions(args.slice(1)));
emitJson(commandName, result, result.ok);
if (!result.ok) process.exitCode = 1;
@@ -0,0 +1,157 @@
import { readConfig } from "./src/config";
import { extractHostPathEntries, parseContainerCreatingReport, parseProcMounts, runD601RecoveryGuardrails, type RecoveryGuardrailsFixture } from "./src/recovery-guardrails";
type JsonRecord = Record<string, unknown>;
function assertCondition(condition: unknown, message: string, detail: unknown = {}): void {
if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`);
}
const malformedProcMounts = [
"proc /proc proc rw,nosuid,nodev,noexec,relatime 0 0",
"drvfs /Docker/host 9p rw,dirsync,aname=drvfs;path=C:\\Program Files\\Docker\\Docker Desktop\\host 0 0",
].join("\n");
const manifest = `
apiVersion: apps/v1
kind: Deployment
metadata:
name: code-queue-scheduler-dev
namespace: unidesk-dev
spec:
template:
spec:
volumes:
- name: repo
hostPath:
path: /home/ubuntu/unidesk-dev-code-queue-deploy/code-queue
type: Directory
- name: state
hostPath:
path: /home/ubuntu/unidesk-dev-code-queue-deploy/state/code-queue
type: DirectoryOrCreate
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: mdtodo-dev
namespace: unidesk-dev
spec:
template:
spec:
volumes:
- name: workspace
hostPath:
path: /home/ubuntu/unidesk-dev-mdtodo-workspace
type: Directory
- name: logs
hostPath:
path: /home/ubuntu/cq-deploy/.state/mdtodo-dev/logs
type: DirectoryOrCreate
`;
const kubectlPods = {
items: [
{
metadata: { namespace: "unidesk-dev", name: "code-queue-scheduler-dev-abc" },
status: {
containerStatuses: [{
name: "code-queue",
state: {
waiting: {
reason: "ContainerCreating",
message: "MountVolume.SetUp failed for volume \"repo\": hostPath type check failed: /home/ubuntu/unidesk-dev-code-queue-deploy/code-queue is not a directory",
},
},
}],
},
},
{
metadata: { namespace: "unidesk-dev", name: "mdtodo-dev-def" },
status: {
containerStatuses: [{
name: "mdtodo",
state: { waiting: { reason: "ContainerCreating", message: "" } },
}],
},
},
],
};
const staleCriPods = {
items: [
{
id: "sandbox-stale",
metadata: { name: "code-queue-scheduler-dev-abc", namespace: "unidesk-dev" },
state: "SANDBOX_NOTREADY",
createdAt: "2026-05-23T09:00:00.000Z",
},
],
};
export function runD601RecoveryGuardrailsContract(): JsonRecord {
const procMounts = parseProcMounts(malformedProcMounts, "fixture:/proc/mounts");
assertCondition(procMounts.ok === false, "malformed /proc/mounts fixture should fail", procMounts);
assertCondition(procMounts.malformedLines.length === 1, "one malformed mount line expected", procMounts);
assertCondition(procMounts.malformedLines[0]?.dockerDesktopHost9p === true, "malformed line should be classified as Docker Desktop /Docker/host 9p", procMounts);
const entries = extractHostPathEntries("fixture.k8s.yaml", manifest);
assertCondition(entries.some((entry) => entry.hostPath === "/home/ubuntu/unidesk-dev-code-queue-deploy/code-queue"), "hostPath parser should extract repo path", entries);
assertCondition(entries.some((entry) => entry.hostPath === "/home/ubuntu/unidesk-dev-mdtodo-workspace"), "hostPath parser should extract MDTODO workspace path", entries);
const fixture: RecoveryGuardrailsFixture = {
observedAt: "2026-05-23T10:00:00.000Z",
procMountsText: malformedProcMounts,
criPodsJsonText: JSON.stringify(staleCriPods),
kubectlPodsJsonText: JSON.stringify(kubectlPods),
deployWorktreePath: "/home/ubuntu/unidesk-code-queue-deploy",
compatibilitySymlinkPath: "/home/ubuntu/cq-deploy",
manifestPaths: ["fixture.k8s.yaml"],
manifestTexts: { "fixture.k8s.yaml": manifest },
pathStates: {
"/home/ubuntu/unidesk-code-queue-deploy": { kind: "missing" },
"/home/ubuntu/cq-deploy": { kind: "symlink", target: "/home/ubuntu/unidesk-code-queue-deploy", targetExists: false },
"/home/ubuntu/unidesk-dev-code-queue-deploy/code-queue": { kind: "missing" },
"/home/ubuntu/unidesk-dev-code-queue-deploy/state/code-queue": { kind: "missing" },
"/home/ubuntu/unidesk-dev-mdtodo-workspace": { kind: "missing" },
"/home/ubuntu/cq-deploy/.state/mdtodo-dev/logs": { kind: "missing" },
},
};
const result = runD601RecoveryGuardrails(readConfig(), fixture);
assertCondition(result.scope.liveMutationAllowed === false && result.mutation === false, "guardrail result must be read-only", result.scope);
assertCondition(result.ok === false, "fixture should produce red guardrails", result.redlineSummary);
assertCondition(result.redlineSummary.requiresManualHostHotfix.includes("proc-mounts-malformed-kubelet-risk"), "malformed mount risk should require manual host hotfix", result.redlineSummary);
assertCondition(result.redlineSummary.requiresManualHostHotfix.includes("code-queue-deploy-worktree-not-ready"), "missing worktree/symlink target should require manual host hotfix", result.redlineSummary);
assertCondition(result.redlineSummary.requiresManualHostHotfix.includes("required-hostpaths-not-ready"), "missing hostPath should require manual host hotfix", result.redlineSummary);
assertCondition(result.redlineSummary.requiresManualHostHotfix.includes("opaque-containercreating-hostpath-risk"), "ContainerCreating hostPath risk should require manual host hotfix", result.redlineSummary);
assertCondition(result.checks.codeQueueDeployWorktree.canonicalPath.exists === false, "canonical deploy worktree should report missing", result.checks.codeQueueDeployWorktree);
assertCondition(result.checks.codeQueueDeployWorktree.compatibilitySymlink.isSymlink === true, "compat symlink should be visible", result.checks.codeQueueDeployWorktree);
assertCondition(result.checks.codeQueueDeployWorktree.compatibilitySymlink.targetExists === false, "compat symlink target should report missing", result.checks.codeQueueDeployWorktree);
assertCondition(result.checks.hostPaths.missing.some((problem) => problem.entry.hostPath === "/home/ubuntu/unidesk-dev-code-queue-deploy/code-queue"), "missing Directory hostPath should be red", result.checks.hostPaths.missing);
assertCondition(result.checks.hostPaths.directoryOrCreateMissing.some((problem) => problem.entry.hostPath === "/home/ubuntu/unidesk-dev-code-queue-deploy/state/code-queue"), "DirectoryOrCreate missing should be surfaced separately", result.checks.hostPaths.directoryOrCreateMissing);
assertCondition(result.checks.mdtodoAdjacentHostPaths.opaqueRisk === true, "MDTODO adjacent hostPath readiness should be opaque when workspace/logs missing", result.checks.mdtodoAdjacentHostPaths);
assertCondition(result.checks.criSandboxes.staleNotReadyCount === 1, "stale CRI sandbox should be counted", result.checks.criSandboxes);
assertCondition(result.checks.containerCreating.hostPathContainerCreating.length === 1, "explicit hostPath ContainerCreating should be classified", result.checks.containerCreating);
assertCondition(result.checks.containerCreating.opaqueContainerCreating.length === 1, "empty-message ContainerCreating should be classified as opaque when hostPaths are missing", result.checks.containerCreating);
assertCondition(result.redlineSummary.forbiddenAutomaticActions.includes("crictl rmp"), "CRI deletion should be explicitly forbidden", result.redlineSummary);
assertCondition(result.redlineSummary.forbiddenAutomaticActions.includes("systemctl restart k3s"), "k3s restart should be explicitly forbidden", result.redlineSummary);
const containerCreatingOnly = parseContainerCreatingReport(JSON.stringify(kubectlPods), result.checks.hostPaths, "fixture", null);
assertCondition(containerCreatingOnly.ok === false, "ContainerCreating parser should fail when opaque/hostPath findings exist", containerCreatingOnly);
return {
ok: true,
checks: [
"malformed /proc/mounts Docker Desktop /Docker/host 9p line detected",
"missing Code Queue worktree symlink and target reported",
"missing hostPath and DirectoryOrCreate readiness reported",
"opaque and explicit hostPath ContainerCreating classified",
"stale CRI sandbox count reported without cleanup",
"destructive prune/reset/restart/delete actions forbidden",
],
};
}
if (import.meta.main) {
process.stdout.write(`${JSON.stringify(runD601RecoveryGuardrailsContract(), null, 2)}\n`);
}
+36 -1
View File
@@ -3,6 +3,7 @@ import { extname } from "node:path";
import { runCommand } from "./command";
import { type UniDeskConfig, repoRoot, rootPath } from "./config";
import { composeConfig } from "./docker";
import { compactD601RecoveryGuardrails, runD601RecoveryGuardrails } from "./recovery-guardrails";
interface CheckItem {
name: string;
@@ -27,6 +28,7 @@ const syntaxFiles = [
"scripts/src/e2e.ts",
"scripts/src/help.ts",
"scripts/src/commander.ts",
"scripts/src/recovery-guardrails.ts",
"scripts/src/server-cleanup.ts",
"scripts/src/remote.ts",
"scripts/host-codex-commander-contract-test.ts",
@@ -42,6 +44,7 @@ const syntaxFiles = [
"scripts/code-queue-submit-summary-contract-test.ts",
"scripts/code-queue-cli-read-terminal-contract-test.ts",
"scripts/code-queue-gh-auth-redaction-contract-test.ts",
"scripts/d601-recovery-guardrails-contract-test.ts",
"scripts/microservice-health-output-contract-test.ts",
"scripts/code-queue-supervisor-disclosure-contract-test.ts",
"scripts/code-queue-commander-view-contract-test.ts",
@@ -70,6 +73,7 @@ export interface CheckOptions {
components: boolean;
compose: boolean;
logs: boolean;
recoveryGuardrails: boolean;
rust: boolean;
}
@@ -80,6 +84,7 @@ const defaultCheckOptions: CheckOptions = {
components: false,
compose: false,
logs: false,
recoveryGuardrails: false,
rust: false,
};
@@ -87,7 +92,10 @@ export function checkHelp(): Record<string, unknown> {
return {
ok: true,
command: "check",
usage: "bun scripts/cli.ts check [--syntax-only|--full|--files|--scripts-typecheck|--components|--compose|--logs|--rust]",
usage: [
"bun scripts/cli.ts check [--syntax-only|--full|--files|--scripts-typecheck|--components|--compose|--logs|--recovery-guardrails|--rust]",
"bun scripts/cli.ts check recovery-guardrails",
],
defaultMode: "syntax/config only; Rust is never compiled on the master server by default",
options: [
{ name: "--syntax-only|--basic", description: "Run only config validation, Bun version and TypeScript syntax transpile." },
@@ -97,12 +105,18 @@ export function checkHelp(): Record<string, unknown> {
{ name: "--components", description: "Run component TypeScript typecheck." },
{ name: "--compose", description: "Render Docker Compose config." },
{ name: "--logs", description: "Check unified log rotation policy." },
{ name: "--recovery-guardrails", description: "Run D601 k3s/Code Queue reboot recovery diagnostics in read-only mode." },
{ name: "--rust", description: "Run cargo check only when UNIDESK_D601_RUST_CHECK=1 is set inside D601 CI/dev execution." },
],
rustBoundary: {
masterServer: "do not run cargo check/build here",
d601: "use deploy apply --env dev --service backend-core and CI with UNIDESK_D601_RUST_CHECK=1",
},
recoveryGuardrailsBoundary: {
command: "bun scripts/cli.ts check recovery-guardrails",
mutation: false,
forbidden: ["restart k3s", "delete CRI sandboxes or pods", "modify hostPath directories", "deploy/rollout", "destructive prune/reset"],
},
};
}
@@ -116,6 +130,7 @@ export function parseCheckOptions(args: string[]): CheckOptions {
options.components = true;
options.compose = true;
options.logs = true;
options.recoveryGuardrails = true;
} else if (arg === "--files") {
options.files = true;
} else if (arg === "--scripts-typecheck") {
@@ -126,6 +141,8 @@ export function parseCheckOptions(args: string[]): CheckOptions {
options.compose = true;
} else if (arg === "--logs") {
options.logs = true;
} else if (arg === "--recovery-guardrails") {
options.recoveryGuardrails = true;
} else if (arg === "--rust") {
options.rust = true;
} else if (arg === "--basic" || arg === "--syntax-only") {
@@ -277,6 +294,10 @@ function skippedItem(name: string, reason: string, enableWith: string): CheckIte
return { name, ok: true, detail: { skipped: true, reason, enableWith } };
}
export function runRecoveryGuardrailsCheck(config: UniDeskConfig): ReturnType<typeof compactD601RecoveryGuardrails> {
return compactD601RecoveryGuardrails(runD601RecoveryGuardrails(config));
}
export function runChecks(config: UniDeskConfig, options: CheckOptions = defaultCheckOptions): { ok: boolean; mode: string; options: CheckOptions; items: CheckItem[] } {
const items: CheckItem[] = [
{ name: "config:validated", ok: true, detail: { project: config.project.name, runtime: config.runtime } },
@@ -352,8 +373,10 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default
fileItem("scripts/server-cleanup-plan-contract-test.ts"),
fileItem("scripts/src/artifact-registry.ts"),
fileItem("scripts/src/server-cleanup.ts"),
fileItem("scripts/src/recovery-guardrails.ts"),
fileItem("scripts/src/auth-broker.ts"),
fileItem("scripts/auth-broker-contract-test.ts"),
fileItem("scripts/d601-recovery-guardrails-contract-test.ts"),
fileItem("src/components/microservices/auth-broker/Cargo.toml"),
fileItem("src/components/microservices/auth-broker/Dockerfile"),
fileItem("src/components/microservices/auth-broker/src/main.rs"),
@@ -403,6 +426,7 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default
items.push(commandItem("gh:pr-contract", ["bun", "scripts/gh-cli-pr-contract-test.ts"], 30_000));
items.push(commandItem("playwright:cli-wrapper-contract", ["bun", "scripts/playwright-cli-contract-test.ts"], 30_000));
items.push(commandItem("auth-broker:p0-contract", ["bun", "scripts/auth-broker-contract-test.ts"], 30_000));
items.push(commandItem("d601:recovery-guardrails-contract", ["bun", "scripts/d601-recovery-guardrails-contract-test.ts"], 30_000));
} else {
items.push(skippedItem("typescript:scripts", "scripts TypeScript typecheck is opt-in", "--scripts-typecheck or --full"));
items.push(skippedItem("code-queue:prompt-observation-contract", "prompt observation contract is opt-in with script checks", "--scripts-typecheck or --full"));
@@ -439,12 +463,23 @@ export function runChecks(config: UniDeskConfig, options: CheckOptions = default
items.push(skippedItem("gh:pr-contract", "GitHub PR CLI contract is opt-in with script checks", "--scripts-typecheck or --full"));
items.push(skippedItem("playwright:cli-wrapper-contract", "Playwright wrapper/headless/session contract is opt-in with script checks", "--scripts-typecheck or --full"));
items.push(skippedItem("auth-broker:p0-contract", "Auth Broker P0 skeleton and CLI adapter contract is opt-in with script checks", "--scripts-typecheck or --full"));
items.push(skippedItem("d601:recovery-guardrails-contract", "D601 recovery guardrails fixture contract is opt-in with script checks", "--scripts-typecheck or --full"));
}
if (options.logs) {
items.push(unifiedLogRotationItem());
} else {
items.push(skippedItem("logs:unified-hourly-rotation", "policy scan is opt-in", "--logs or --full"));
}
if (options.recoveryGuardrails) {
const recovery = runRecoveryGuardrailsCheck(config);
items.push({
name: "d601:recovery-guardrails",
ok: recovery.ok,
detail: recovery,
});
} else {
items.push(skippedItem("d601:recovery-guardrails", "D601 reboot recovery diagnostics are opt-in and read-only", "--recovery-guardrails or --full"));
}
if (options.components) {
items.push(commandItem("typescript:components", ["bunx", "tsc", "-p", "src/tsconfig.check.json", "--pretty", "false"], 180_000));
} else {
+1 -1
View File
@@ -9,7 +9,7 @@ export function rootHelp(): unknown {
{ command: "help", description: "List supported commands." },
{ command: "--main-server-ip <ip> <command>", description: "Run selected commands through the public frontend API; use --main-server-key only for legacy SSH transport." },
{ command: "config show", description: "Validate and print config.json as the single source of truth." },
{ command: "check [--full|--files|--scripts-typecheck|--components|--compose|--logs|--rust]", description: "Run the lightweight default syntax/config gate; Rust is opt-in and only allowed from D601 CI/dev execution." },
{ command: "check [--full|--files|--scripts-typecheck|--components|--compose|--logs|--recovery-guardrails|--rust] | check recovery-guardrails", description: "Run the lightweight default syntax/config gate or the low-noise read-only D601 recovery guardrails; Rust is opt-in and only allowed from D601 CI/dev execution." },
{ command: "server start", description: "Fire-and-forget build/start for database, backend-core, frontend, provider gateway, and managed main-server user services." },
{ command: "server stop", description: "Fire-and-forget docker-compose down for the fixed UniDesk stack." },
{ command: "server status", description: "Show fixed ports, containers, service health, and public URLs." },
File diff suppressed because it is too large Load Diff