ADR-0086: VALIDATOR.Art — central enrollment + orchestration brain (Phase A)¶
- Status: Accepted (2026-05-24) — Phase A scaffold (V-1..V-4) shipped to
mainin PRs #1046 / #1047 / #1048 / this PR - Date: 2026-05-24
- Driver:
discuss_module_validator_tbi_2026_05_10locked VALIDATOR.Art as the orchestration-brain MÓDULO underpinning AI/ML-ZTP.project_validator_vs_cloner_clarification_2026_05_10locked it as distinct from CLONER.Art (decision engine vs network plumber).discuss_validator_tech_stack_2026_05_10locked Go primary + Python ML sidecar + PostgreSQL ecosystem - Related: PRs #1046 (V-1), #1047 (V-2), #1048 (V-3), this PR (V-4); ADR-0084 (HAR.Art), ADR-0083 (KALI.Art Phase B), ADR-0085 (TREX.Art) — sibling MÓDULO ADRs
Context¶
The TLSStress.Art product needs ZTP (zero-touch provisioning) for multi-node deploys — the customer's experience after the first single-node demo is "we want this on 4 UCS servers; what do we do?" Today the answer is kubectl + Ansible + lots of YAML per node, which is enterprise-friction and doesn't scale to the 100k-node target hinted by discuss_ai_ml_ztp_cascade_2026_05_10.
The ZTP solution requires two components that the project memos identified separately:
- TBI (TLSStress Bench Image) — the K8s equivalent of an OVA; a custom Ubuntu 24.04 LTS image with embedded ZTP boot agent + k3s pre-installed + gVisor runtime + cached MÓDULO base images. Operator-distributed (USB / PXE / cloud-init / OCI), signed with TLSStress.Art root CA via Sigstore
- VALIDATOR.Art — the orchestration brain that receives /enroll calls from TBI-booting nodes, validates claim tokens issued by the operator, computes role assignments from hardware fingerprint + topology hints + ML cortex, issues per-node mTLS certs (via CA.Art), pushes GitOps configs, tracks lab inventory, detects drift
This ADR scopes ONLY the VALIDATOR.Art Phase A wave (V-1..V-4). TBI image is Phase G+ per the roadmap; ML cortex is Phase E.
Decision¶
Ship MÓDULO VALIDATOR.Art as a single-replica Go HTTP server on OOBI slot .97 (the original memo's .84 was reassigned to api-infra-art before VALIDATOR scaffolded; .97 is the next free slot in the MGMT band). Phase A ships in SimulatedMode by default — /enroll returns synthetic JoinBundles. Phase B (out of scope for V-1..V-4) wires real CA.Art cert issuance + k3s server join + WireGuard mesh.
Materialized in 4 sub-PRs:
| Sub-PR | Scope | Status |
|---|---|---|
| V-1 | pkg/validator-art/ Go scaffold — types (EnrollRequest/Response, NodeRegistration, ClaimToken, HardwareFingerprint, RoleAssignment, JoinBundle, Config), store (RWMutex ring buffer + claim-token map with expiry + one-time-use), server (9 HTTP endpoints), cmd entrypoint. 16 unit tests |
✅ #1046 |
| V-2 | HMAC-SHA256 claim-token Sign/Verify (constant-time compare via subtle, version + expiry checks); role-assignment heuristic Compute (hardware ladder + hint honoring + deployment-mode from occupancy); server /enroll real-mode rewrite. 20 new tests (36 total) |
✅ #1047 |
| V-3 | K8s manifest slot .97 + Dashboard /admin/validator-art (status header + enrollments table + claim-token issuance form) + Drizzle 0040 (validator_art_enrollments + validator_art_claim_tokens) + journal-drift bonus fix (added idx 30-40 entries) |
✅ #1048 |
| V-4 | This ADR + docs/modules/validator-art.{md,pt-BR.md,es.md} expansion + recovery memo project_validator_art_phase_a_complete_2026_05_24.md |
✅ |
Locked decisions (Phase A)¶
| # | Decision | Rationale |
|---|---|---|
| 1 | Distinct MÓDULO from CLONER.Art | CLONER = network plumber (on-prem only, internet egress, data caching); VALIDATOR = orchestration brain (cloud-portable, ML cortex, decisions). Per project_validator_vs_cloner_clarification_2026_05_10 |
| 2 | HMAC-SHA256 claim tokens (not PASETO v4) | PASETO's value is envelope-format rigor; for Phase A we need tamper-evidence + embedded expiry + one-time-use, all of which HMAC+JSON gives in ~80 LoC with zero deps. PASETO migration deferred to Phase B (when Vault rotation lands) |
| 3 | In-memory store (200-entry ring) in the pod | Single-replica MGMT-only Phase A; persistent audit history lives in the Dashboard's Postgres via Drizzle 0040 — same separation as TREX.Art runsummary ring + ADR-0085 §"Why an in-process ring (not Postgres)" |
| 4 | SimulatedMode default = true | CI + bench-up tests can drive /enroll end-to-end without operator-issued tokens; real-mode requires --simulated=false --claim-token-secret=$SECRET |
| 5 | Pure-heuristic role assignment (no ML cortex) | Phase A; ML cortex is Phase E per discuss_module_validator_tbi_2026_05_10. The heuristic is deterministic + has operator-readable Rationale so debugging "why did this node get role X?" is trivial |
| 6 | Slot .97 instead of memo's .84 | Slot .84 was reassigned to api-infra-art before VALIDATOR scaffold. .97 is next free in MGMT band; preserves the rest of the OOBI map |
| 7 | NO TBI image work in Phase A | TBI v0.1 is Phase G per the roadmap. V-1..V-4 ships the receiver only; the boot-agent that talks to it is a future wave |
Hardware-driven role ladder (V-2)¶
| Hardware floor | Assigned role | Plane |
|---|---|---|
| DPDK-capable + ≥ 16 cores + ≥ 16 GB | trex-agent |
DATA |
| GPU detected | ml-cortex |
MGMT |
| ≥ 8 cores + ≥ 32 GB | ngfw-dut |
DATA |
| ≥ 4 cores | agents |
DATA |
| Anything else | infra |
MGMT |
Operator's TopologyHints.PreferredRole wins when the hardware fits; otherwise it falls through with rationale text like "operator hinted trex-agent but hardware did not fit (DPDK=false cores=2 mem=4GB)".
DeploymentMode is inferred from cluster occupancy (1/2/3/≥4 nodes = single/dual/tri/multi), hint-overridable.
K3SRole assignment¶
- First enrolled node →
server(cluster bootstrap) - All subsequent enrollments →
agent
This is the simplest possible split for Phase A; HA k3s with --server quorum lands in Phase H multi-VALIDATOR federation.
Claim-token format¶
Wire format: <payload-b64url>.<sig-b64url>. Payload is JSON {v, id, issued_by, issued_at, expires_at, one_time, topology}; signature is HMAC-SHA256 over the body using the operator-supplied secret. Constant-time compare via subtle.ConstantTimeCompare.
The HMAC alone proves the token was issued by us, but the store proves it hasn't already been spent (one-time-use Consumed bit). Both checks must pass.
Migration journal drift (closed by V-3)¶
dashboard/src/db/migrations/meta/_journal.json was 11 entries behind disk (last idx=29; disk had 40 files). drizzle-kit was silently skipping every migration from 0030 onwards — DoYour escrows, gallery, KALI PIE/audit/homes, HyperBridge, HAR replay sessions/libraries. V-3 added idx 30-40 entries so the runtime custom-migrator (src/lib/migrate.ts) + the build-time drizzle-kit migrator finally agree on the full set.
Patent posture¶
VALIDATOR.Art Phase A does not raise a new patent claim. The defensible novelty lever (ML cortex + role-assignment learning + 6-phase enrollment cascade + TBI multi-format release) lands as Patent Family E — Enrollment Cortex once Phase E is implemented. Phase A is foundational infrastructure.
Total patent claims unchanged: 17 (TREX.Art's #17 is the most recent).
Consequences¶
Positive
- Provides the receiver surface the future TBI boot agent will talk to — first wave that lets a customer demo say "we have ZTP" without lying
- Decouples Dashboard audit history (Drizzle 0040) from the pod's runtime ring (200 entries), matching the TREX.Art runsummary pattern
- SimulatedMode + heuristic role assignment keeps CI green without needing CA.Art / k3s wired up first
- V-3's journal-drift bonus fix unblocks every migration shipped since DoYour.Art Phase A from being applied on a clean CI database
Negative / deferred
- TBI image (Phase G+) — actual ISO/IMG/PXE/OCI multi-format release. Today the validator returns synthetic JoinBundles to a TBI image that doesn't exist yet
- CA.Art real cert issuance (Phase B) — JoinBundle.MtlsCertPEM + CABundlePEM are placeholders. Phase B wires
cert-managervia thepersona-ca-issuerClusterIssuer (already inplatform/pki/) - k3s real server join (Phase B) — K3SJoinToken is a placeholder string. Phase B wires actual k3s server token retrieval
- WireGuard mesh peer pinning (Phase B) — JoinBundle.WireGuardPeers is empty. Phase B exposes wg0 peer config from the live cluster state
- ML cortex (Phase E) — role-assignment heuristic is pure-rules. Phase E replaces with a learned model trained on operator-validated assignments
- GitOps push (Phase B) — JoinBundle.ModuleManifests is empty. Phase B wires ArgoCD App-of-Apps generation per node
- Multi-VALIDATOR federation (Phase H) — single replica today
Alternatives considered¶
- Make VALIDATOR.Art a sub-component of CLONER.Art — rejected per
project_validator_vs_cloner_clarification_2026_05_10. Cloud-split + storage-gravity + attack-surface arguments all push for separation - Use PASETO v4 for claim tokens — rejected for Phase A; HMAC+JSON covers Phase A needs with zero dependencies. PASETO migration when Vault lands in Phase B
- Use Postgres for runtime store too — rejected; single-replica pod doesn't need it. The Dashboard already persists via Drizzle 0040; pod's ring is for /enroll latency
- ML cortex from MVP — rejected per the roadmap; explainable heuristic beats opaque classifier at scale of ~10s of customer benches. Promotes when training data accumulates
- Slot .84 per memo — slot was already taken at scaffold time
Compliance¶
Per CLAUDE.md: "Dashboard is the ONLY operator interface." V-3 ships /admin/validator-art with claim-token issuance + enrollments table — operator never types kubectl against the validator pod.
Per ZTP-prem posture: pod runs readOnlyRootFilesystem: true, runAsNonRoot: true, runAsUser: 2097, drop: [ALL] capabilities. No DATA-plane VLAN attachments (ADR 0042 wants: mgmt-only).
Per ADR-0044 (OOBI Intrusion Detection): the validator pod listens on :8086 cluster-internal only; Dashboard proxy carries the auth surface. Real-mode operator-issued tokens are HMAC-SHA256 signed with a Secret-stored key.
References¶
discuss_module_validator_tbi_2026_05_10— strategic memo (7 core responsibilities, 8 ML functions, 9-phase roadmap)project_validator_vs_cloner_clarification_2026_05_10— architectural distinctiondiscuss_validator_tech_stack_2026_05_10— concrete tech-stack choices- ADR-0083 (KALI.Art Phase B), ADR-0084 (HAR.Art L7 replay), ADR-0085 (TREX.Art) — sibling MÓDULO ADRs
- PRs #1046 (V-1), #1047 (V-2), #1048 (V-3), this PR (V-4)
- Patent claim posture: no new claim in Phase A; Patent Family E reserved for Phase E ML cortex