Skip to content

ADR-0086: VALIDATOR.Art — central enrollment + orchestration brain (Phase A)

  • Status: Accepted (2026-05-24) — Phase A scaffold (V-1..V-4) shipped to main in PRs #1046 / #1047 / #1048 / this PR
  • Date: 2026-05-24
  • Driver: discuss_module_validator_tbi_2026_05_10 locked VALIDATOR.Art as the orchestration-brain MÓDULO underpinning AI/ML-ZTP. project_validator_vs_cloner_clarification_2026_05_10 locked it as distinct from CLONER.Art (decision engine vs network plumber). discuss_validator_tech_stack_2026_05_10 locked 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-manager via the persona-ca-issuer ClusterIssuer (already in platform/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 distinction
  • discuss_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