# YAML-first 异构分布式运维

## 配置引用问题的最短查找路径

- 适用问题：
  - YAML 引用的密钥、密码、token 或连接配置在哪里；
  - 应修改哪个来源文件；
  - 来源内容会写入哪个运行时键；
  - 修改后应走哪个受控入口。
- 必要输入只有两个：
  - 领域 `skill`；
  - 服务的 owning YAML。
- 字段解释：
  - `sourceRef` 是可修改的本地 Secret 来源或外部 Secret 引用；
  - `sourceKey` 是结构化来源内被读取的键，缺省时按领域 parser 声明的整文件语义处理；
  - `targetKey` 是 Secret、环境变量或渲染配置中的接收键，不代表用户名，也不包含 Secret 值；
  - `transform` 描述下发前的具名转换，不能据此反向恢复原值；
  - `configRef` 指向另一个 owning YAML 对象时，只沿该引用读取一次目标对象，不扫描整个仓库。
  - Secret 分发对象同时提供 `configRef`、`declarationId` 和 target scope 时，标准受控入口是 `bun scripts/cli.ts secrets sync --config <configRef 文件> --scope <target scope> --confirm`。
- 标准回答必须包含：
  - owning YAML 的文件路径和对象路径；
  - `sourceRef`、可选 `sourceKey`、`targetKey`；
  - 修改动作是编辑 `sourceRef` 所指来源，而不是修改 `targetKey` 或运行面对象；
  - 修改后使用领域 skill 已声明的 `plan/status/sync/apply` 受控入口；
  - 不披露 Secret 值、完整 DSN、token 或可复制凭据。
- 停止条件：
  - 上述字段齐全时立即回答并停止；
  - 不继续读取应用源码、数据库实现、Pod Secret、环境变量、日志或运行面；
  - 不通过字符串中 `@`、`:`、`/` 等分隔符猜测用户名或密码；
  - 只有相对 `sourceRef` 的解析基准未声明时，才定点读取领域 parser；
  - 只有用户要求验证生效或执行下发时，才进入运行面操作。
- 回答模板：

  ```text
  owning YAML：<file>#<object.path>
  修改来源：<sourceRef>（sourceKey：<key 或“整文件”>）
  注入目标：<targetKey>
  受控入口：<domain CLI plan/status/sync/apply>
  说明：targetKey 不是用户名或密码；未读取或打印 Secret 值。
  ```

- 最小 smoke 示例只用于证明通用规则，不构成第二配置真相：
  - 读取 `config/pikaoa.yaml#releaseRuntime.targets.NC01.runtime.administrator.password`；
  - 可直接得到管理员用户名、统一密码 `sourceRef` 与 `targetKey`；
  - 修改密码应编辑 `/root/.unidesk/.env/unified-admin-password.txt`，再走 PikaOA/Secret 分发的领域受控入口；
  - 到此停止，无需读取 PikaOA 源码、数据库或 Kubernetes Secret。

本文定义 UniDesk 的 YAML-first 异构分布式运维架构，是将节点、lane、服务、Secret、公开暴露、数据库、rollout 和探针决策收敛为声明式配置与可复用 CLI 执行的长期参考。具体数值归属于 `config/` 下的 YAML；本文只定义归属关系和架构。

## Scope

YAML-first ops applies to UniDesk-owned distributed runtime management across heterogeneous targets: host services, Windows-hosted GUI bridge and collector pairs, k3s namespaces, public exposure bridges, external databases, app runtime Secrets, CI/CD control-plane bootstrap, workflow services and managed service probes.

It is not a new global orchestrator. Existing domain ownership stays intact:

- Platform shared services keep their truth in the existing platform infra YAML family.
- Platform database state keeps its truth in platform database YAML.
- Runtime lane services keep their truth in their existing node/lane YAML.
- Agent execution infrastructure keeps its truth in its own infrastructure YAML.

Add a new top-level YAML registry only after multiple existing domains share the same lifecycle, owner and command model, and after the common blocks have already proven reusable. The default path is to extend the owning domain YAML and shared ops helpers, not to create another parallel control plane.

## Source Of Truth

UniDesk-owned distributed ops choices must enter through YAML:

- target route and execution plane
- namespace, workload, service, Secret and ConfigMap identifiers
- image references, versions and pull policy
- public URL, DNS expectation, FRP/Caddy edge settings and probe endpoints
- database host, role/database declarations, Secret exports and connection mode
- Secret source references, key mappings, transforms and rollout triggers
- readiness, validation and smoke probe shape
- retention, cadence, timeout and policy values when they are UniDesk-owned choices

代码可以校验 YAML 是否存在、类型正确、语法有效且可以渲染。
代码不得成为节点名、服务名、namespace、端口、镜像标签、Secret 名称、URL、
账号列表、容量、冷却时间或重试窗口的隐藏来源。
这些值必须来自 YAML 或显式外部工具与运行时 API。

### 缺失配置的阻塞边界

- 核心配置缺失时必须 fail closed：
  - 认证、权限与 SecretRef；
  - 无法唯一确定的 target、lane、session scope 或数据 authority；
  - mutation target、公开入口身份以及损害预防配置。
- 非核心业务配置缺失或非法时允许继续：
  - 诊断标签、展示元数据与可选 capability；
  - 不影响请求正确性、权限或数据归属的 timeout、采样和观测参数；
  - 领域明确列为可选的适配器参数。
- 非核心回退必须满足：
  - 默认值在领域 parser、共享 baseline 或 owning YAML 模板中显式定义且可审阅；
  - CLI、日志或 artifact 输出配置路径、回退原因、采用的默认值与
    `blocking=false`；
  - warning 不得阻止核心动作、禁用输入、覆盖成功结果或切换 authority；
  - owning YAML 后续补齐该字段时必须覆盖默认值，不形成第二配置真相。
- 不能确定字段是否核心时，按实际损害风险分类：
  - 继续执行可能造成越权、写错目标、数据混淆或不可逆损害时按核心处理；
  - 只影响诊断完整度、显示或可选优化时按非核心处理。

External formats such as JSON, TOML, env files, Kubernetes YAML, Caddyfile, systemd units or app-specific config files may still be generated or consumed at the edge when the external tool requires them. They are inputs or rendered artifacts, not UniDesk desired-state truth.

## Service Deployment Declarations

UniDesk-managed service deployment declarations must not live in service repository JSON such as `deploy.json`. A service repository may keep application source, build inputs, migrations, API/spec documentation and app-native runtime config required by the process. Node/lane selection, runtime namespace, image artifact selection, GitOps branch/path, public exposure, external database wiring, Secret mapping, service account, probes, rollout and cleanup settings belong in the owning UniDesk YAML and are rendered by UniDesk CLI.

Generated GitOps YAML, image catalogs, env files, Kubernetes manifests or external tool config may be committed as rendered artifacts when the runtime requires them. They must carry enough provenance to point back to the owning YAML/source commit and must not become a second editable desired-state truth.

## Architecture Layers

YAML-first ops uses five layers.

1. Domain YAML

The owning `config/**/*.yaml` file declares the desired runtime state and all tunable values. A domain YAML may contain reusable blocks such as `publicExposure`, `externalDatabase`, `runtimeSecrets`, `rollout`, `probes`, `staging`, `retention` or `controlPlane`, but the exact block is owned by the domain until it is promoted into a shared helper.

2. 领域解析器

每个领域使用 parser 解析选中的 target，并校验形状、字段类型、核心必填字段和可渲染性。
parser 可以校验 Kubernetes 资源名、route token、URL、镜像引用、
相对 sourceRef 和 key 名等通用语法。
核心字段不得硬编码当前策略值或静默补值。
非核心字段可以使用显式领域默认值，但必须输出配置路径、回退值和
`blocking=false` warning。

3. Common Ops Library

Shared behavior belongs in reusable modules under `scripts/src/`, not in service-specific command files. The existing reusable seeds are the platform infra public-service helpers and the platform infra ops library. New common helpers should be extracted when the same operation appears in more than one domain, especially for:

- route execution and bounded capture
- YAML parsing primitives
- redacted output, fingerprints and compact evidence
- Secret source loading and source path redaction
- Kubernetes Secret apply from local source material
- rollout restart/status from YAML-declared workload refs
- public exposure rendering through FRP/Caddy
- manifest staging, dry-run and server-side apply wrappers
- probe execution and response summarization
- async job submission and short polling for long operations

4. Thin Domain CLI

The domain CLI resolves the target from YAML, calls shared helpers and prints structured JSON. It should not contain large inline shell bodies, duplicated secret-sync scripts, hard-coded service names or app-specific operational workflows. A domain CLI may keep a stable command namespace for compatibility and discoverability, but the implementation should delegate to common helpers.

5. Runtime Executor

Runtime mutation goes through UniDesk CLI and `trans` route execution. Direct `kubectl`, raw SSH, hand-written Caddy edits, direct GitHub API calls or ad hoc shell scripts may be diagnostic or emergency recovery tools only. Repeated operational writes must be promoted into a controlled CLI command that reads YAML and reports redacted structured output.

## Host Bootstrap And Zero-Dependency Proxy

Fresh VPS bootstrap has one allowed host-level exception before the node can become a clean k3s runtime: a zero-dependency host proxy client distributed and configured by YAML-first ops through `trans`. The client must not require Docker, k3s, containerd, Node, Python, Bun, Go, Rust, or package-manager network access to install. Its purpose is to provide outbound connectivity for those dependencies, including k3s installer downloads, container image pulls, `apk`, `apt`, `npm`, Go modules, Git and git-mirror sync/flush.

The proxy source, upstream server identity, local listen address, local port, systemd unit, environment injection targets, health probe and secret source references must be declared in YAML. The target node should receive only rendered artifacts and redacted summaries over `trans`; it must not fetch the proxy client from the public internet before the proxy exists. Values such as proxy server host, credentials, bind port and benchmark profile belong in YAML or Secret source files, not in prose or code defaults.

The default bootstrap order for a new node is:

1. establish provider-gateway only long enough to make `trans` reachable;
2. distribute the zero-dependency proxy client through `trans`;
3. start and validate the host proxy client from YAML-rendered systemd or equivalent host control;
4. configure host egress for k3s/containerd/package managers and Git through that proxy;
5. install k3s and deploy workloads through YAML-first control paths;
6. keep later workload proxy settings as YAML-declared consumers of the host route unless a domain explicitly owns a different proxy boundary.

For fresh VPS bring-up, do not use provider-gateway WebSocket egress as the bulk path for large installers, images or dependency downloads. Provider-gateway and `trans` are the bootstrap control channel; the host proxy client is the data egress path once installed.

Host proxy use has a privacy boundary. A plain `http_proxy=http://...` proxy can observe destination hosts, request metadata and any proxy credential embedded in the URL or process environment; TLS protects encrypted request bodies from the proxy but does not hide the destination metadata from the proxy operator. Do not place private API keys in proxy URLs, logs or CLI output. `NO_PROXY`/`no_proxy` must preserve local, cluster, metadata and explicitly declared direct domains, including `hyueapi.com` and `.hyueapi.com` for Codex direct API access.

## K3s-First Local Registry

Fresh node bootstrap must not require an already-populated host Docker registry in order to deploy the node-local registry itself. The durable path for a new k3s node is to render the registry from YAML as a k3s workload with declared namespace, Deployment, Service, PVC, endpoint and image source. Host-Docker registry containers are legacy or migration sources only; they must not be the source of truth for a new node's ability to come up from zero.

When migrating from a host-Docker registry to a k3s registry, the control path must make the data provenance explicit. If the goal is to prove from-zero deployment, stop the old host registry first and do not seed from it. The status output should say whether the new registry was seeded from an old registry or zero-seeded, whether the PVC is bound, whether the endpoint is ready, and which workload owns the registry. After a zero-seeded migration, required runtime images, BuildKit images and tools images must be re-created or preloaded through YAML-first control commands before claiming the lane can build or roll out.

The registry is not a general reason to keep host Docker as a long-running dependency. Host Docker may still appear as a temporary build or image-transfer tool while a lane is being migrated, but the owning issue should classify it as a remaining Docker dependency and move durable builds toward k3s/Tekton/BuildKit or an equivalent rootless builder. Runtime status should distinguish long-running host Docker services from one-shot controlled build/preload operations.

## Cross-Repository Operational Truth

When UniDesk prepares or supervises runtime data for another repository, the owning YAML still belongs in UniDesk if the data is part of node/lane operations rather than that repository's application source. Examples include HWLAB test/admin account inventories, operator-only API key source references, runtime DB sync inputs, public exposure targets and cross-node workbench preparation. The service repository may contain application code and app-native migrations, but it must not become a second desired-state truth for UniDesk-operated accounts, credentials, nodes, lanes, namespaces or sourceRef bindings.

Cross-repository YAML must name the target repository/runtime explicitly and must route all writes through a UniDesk CLI entrypoint that prints only sourceRef, targetKey, presence, byte counts, fingerprints, object ids and mutation summaries. The CLI may read owner-only local source files or declared platform DB exports; it must not backfill those files from runtime Secrets, pod env, logs or database rows.

## Common Block Rules

Reusable blocks must describe operations in data, not in service-specific code branches.

### Target Blocks

A target block should declare the route, execution plane, namespace and any workload refs required by the operation. Code must not infer these from a node id, lane id or service id by concatenating strings unless that concatenation rule itself is explicitly declared and stable for the domain.

### Secret Blocks

A runtime Secret block should declare source reference, source key, target Secret, target key, optional transform and rollout trigger. Secret values must stay in git-ignored owner-only source files or external Secret stores. CLI output may show sourceRef, target object names, key names, presence, byte counts, fingerprints, mutation and next commands; it must not print secret values, full tokens, decoded base64, passwords or complete connection strings.

App-specific transforms are allowed only as isolated named transform functions. The transform name is data in YAML; the implementation belongs in a shared transform registry or a small domain adapter, not in a one-off reset command.

### Exposure Blocks

Public exposure must be declared as an edge topology, including DNS expectation, public base URL, bridge settings, edge host route and target service. The existing FRP/Caddy path is a reusable public-service primitive. New public exposure code should extend that primitive instead of adding per-service Caddy or FRP scripts.

When several YAML owners render into the same Caddyfile, each owner must write only its own managed site block and merge it with the existing file. A shared writer must preserve other `# BEGIN unidesk managed <owner>` blocks, remove only legacy unmanaged blocks for the domains owned by the current operation, validate the merged Caddyfile before install, and then reload Caddy. A domain CLI must not replace a shared Caddyfile with a file rendered from its own YAML alone.

Shared Caddyfile operations belong in a common helper module under `scripts/src/`. Service-specific CLIs should pass YAML-resolved domains, upstreams and marker names to that helper, then report the managed-block counts and validation result. Full-file Caddy installs are allowed only for a host or file that the command exclusively owns and whose exclusivity is documented in the owning reference.

### Database Blocks

External database consumers must reference the YAML-owned platform database source and exported Secret shape. A consumer should not deploy a new database, copy connection strings by hand, or derive credentials from live runtime objects unless the owning database YAML declares that export.

### Probe Blocks

Probes are validation data, not hidden policy. YAML should declare what endpoint or runtime object proves the operation for that service. CLI code may execute the probe, bound output and classify failure, but should not hard-code current URLs, credentials, namespaces or service paths.

## Reuse, Composition And Variable Rendering

YAML-first does not mean one ever-growing file per node. Reusable configuration should be expressed as small owning YAML documents that can be inherited, composed or referenced by `configRefs`. A node-specific file should normally select a baseline, provide only the concrete overrides for that node, and reference shared blocks for proxy, public exposure, Secret distribution, source truth, runtime dependencies, probes and rollout policy.

Config composition must keep one durable source of truth for each fact. Rendered plans may merge defaults, baselines and overrides for execution, but the merged object must not be written back as a second editable truth. CLI `plan/status --full` should show the reference chain, effective owner, variable inputs, redacted source fingerprints and missing fields instead of dumping all expanded YAML by default.

Variable rendering is allowed for node/lane/service-specific artifact generation. Node and lane identifiers must be data variables, not part of YAML filenames, parser function names, renderer names, TypeScript helper names or long-term schema names. For example, use a neutral template plus `NODE`/`LANE` variables instead of adding files or functions named after a concrete node. Concrete node names may appear only as YAML values, CLI parameters, rendered runtime object names where the external system needs them, or legacy adapters that are explicitly classified.

When a configuration area starts to repeat whole blocks across nodes, treat that as a design smell. Promote the common block into a baseline or shared reference before adding more node copies, and keep node overrides short enough to review. If the owning YAML still grows without a clear owner boundary, split by responsibility: bootstrap host dependencies, proxy, k3s install, platform services, app lane, public exposure, Secret sourceRefs, probes and sentinel schedules should not be forced into one file merely because they target the same node.

## Refactoring Rule

When adding YAML-first ops to an existing domain, follow this order:

1. Inventory the existing YAML, CLI commands and helper modules.
2. Choose the owning domain YAML; do not start with a new global registry.
3. Add or refine a reusable block in that YAML with all concrete values declared there.
4. Extend the domain parser with shape/type/renderability validation only.
5. Extract common execution into shared helper modules before adding domain-specific code.
6. Keep the domain CLI as a thin adapter over the common helper.
7. Validate with the narrowest syntax check and command-shape or original-entry runtime check required by the change.

Large domain command files must be split by responsibility before receiving more operational logic. Typical split boundaries are target resolution, manifest rendering, Secret sync, public exposure, database bridge, rollout, probes, cleanup and status summarization.

## Finite Governance Slices

YAML-first cleanup work must not become an open-ended sequence of rounds. Once an issue is used to close a broad normalization area, freeze a bounded phase list before implementation and keep all child issues inside that list. New findings after the freeze must be classified into the existing scope, kept as domain-specific differences, or parked out of scope; they must not create another phase merely because a search found more candidates.

A shared helper extraction stops when the repeated mechanism has a stable helper, the remaining differences are true domain behavior, or the remaining candidates are outside the frozen scope. Do not continue extracting only to make every domain file look identical. The final audit should list completed changes, kept domain differences, parked risks and validation evidence, then close the issue instead of opening a follow-up round by default.

Closed governance issues are historical records, not standing queues. A later YAML-first pass must start from a fresh code/config inventory and a new bounded issue instead of reopening or appending phases to an already closed issue. For broad cleanup, the phase list should stay small and fixed before implementation; do not exceed five phases unless the user explicitly requests a different bound.

Each frozen phase must deliver a concrete code, configuration or user-facing CLI behavior change. Validation commands, evidence gathering, issue comments, merge/preflight work, progress reporting and docs-only distillation are closeout activities; they may be recorded, but they are not phases. When all frozen phases are complete, close the parent issue and classify remaining discoveries as in-scope leftovers already handled, true domain differences, or out-of-scope parked risks.

## Anti-Patterns

Avoid these patterns:

- creating a per-service reset script when a YAML-declared Secret sync plus rollout block is enough
- adding a second control plane for a service that already has an owning YAML and CLI namespace
- hard-coding node ids, service ids, namespaces, ports, URLs, Secret names or workload names in code
- deriving live state by string conventions when YAML can declare the object directly
- keeping repeated `kubectl apply`, Caddy edits, FRP edits or rollout restarts as runbook shell snippets
- replacing a shared Caddyfile from one YAML owner without preserving other managed blocks
- printing secret values, complete env files, full `DATABASE_URL` values or reusable API keys
- writing long-term docs that duplicate current YAML values as prose
- using contract tests or hidden guards to freeze policy values that should remain YAML-controlled
- preserving legacy command branches after the latest YAML-first path supersedes them
- extending a frozen cleanup issue by appending new rounds instead of classifying discoveries as in-scope, domain-specific or parked
- treating validation, evidence collection, issue lifecycle work, or docs-only closeout as implementation phases

## Documentation Boundary

Long-term references should point to this architecture for common YAML-first ops rules, then document only domain-specific ownership and entrypoints. They should not repeat common Secret, exposure, target, redaction or no-hardcoding rules unless a domain adds a stricter constraint.

When a recurring operation becomes stable, update the owning reference document and the relevant skill with the domain entrypoint and decision boundary. Do not document one-off manual recovery as the standard path; manual repair remains recovery evidence until the YAML and CLI path exists.

## Web 哨兵最短变更路径

- 修改 cadence 前，使用 `web-probe sentinel validate --local` 直接读取当前 worktree 的 owning YAML。
- 本地验证必须输出 `configRef`、`scenarioId`、`cadence`、渲染后的 Cron 表达式和 `mutation=false`，不得读取远端 authority 来替代未提交 YAML。
- 合并后的观察使用 `web-probe sentinel control-plane status --wait`；该入口只读，等待和轮询预算来自 owning YAML。
- 状态必须同时区分 GitOps 期望和运行时事实，并输出 `desiredSourceCommit`、`runtimeSourceCommit`、`desiredSchedule`、`runtimeSchedule`、`converging`、`warning` 与 `blocking=false`。
- 旧 worktree 的本地渲染值不得作为线上 cadence 对齐门禁；线上期望值来自 GitOps 清单。
