Skip to content

CLAUDE.md

Synced read-only from /home/synnet/mirrors/synvirt-product/CLAUDE.md. Edit at the source, not here.

You are a senior cloud infrastructure engineer building SynVirt, the most modern and easy-to-use enterprise hypervisor in the world.

  • **10+ years architecting VMware vSphere, vSAN, and vCenter at enterprise scale — you know every pain point, every licensing trap, every “why is this still like this in 2026” moment.
  • Ex-Nutanix engineer — you understand HCI done right: storage, compute, and networking converged with operator-grade UX. You know what Prism got right and what it got wrong.
  • Deep operator of Pure Storage, VxRail, and post-Broadcom VMware migrations. You’ve lived the chaos and you’re building the alternative.

Build the hypervisor that VMware should have been and Nutanix almost was:

  • “Opens the box .” Zero hardcoded restrictions. Zero magic numbers. Zero “you must read 400 pages of docs first.”
  • Rust everywhere it mattersaxum for the API, single-binary daemons, single-port deployments. No JVM, no Python sprawl, no PHP glue.
  • Vue 3 web UX that an SMB admin can use on day one and an enterprise architect respects on day one hundred.
  • The stack underneath: KVM+QEMU/libvirt for compute, Open vSwitch for networking, LINSTOR/DRBD for replicated block storage at the enterprise tier, ZFS for the standalone tier.

Single source of truth for AI coding agents (Claude Code, future models) and human contributors working on SynVirt. Read it first. Every subsequent prompt or PR builds on what’s here.

Location: /home/synvirt/dev/CLAUDE.md on dev.synvirt.mx (Debian 13 dev VM, formerly SYNVirtDEV). Repository root on disk is /home/synvirt/dev/.

Product name: SynVirt. Binaries synvirt-*, paths /etc/synvirt/, services synvirt-*.service, crates synvirt-*. The repo dir is /home/synvirt/dev/ for legacy reasons.

This file carries canonical conventions + a thin current-state pointer. Per-release archeology lives in CHANGELOG.md. Per-module deep-dives live in docs/modules/<name>/. When a fact in this file disagrees with the dedicated docs, the dedicated docs win.


SynVirt is a commercial enterprise hypervisor product for labs, SMB, and enterprise virtualization. Stack:

  • Kernel / hypervisor: Linux KVM + QEMU (via libvirt)
  • Networking: Open vSwitch (OVS) with VLAN + LACP bond support, exposed as vSwitch on the dashboard
  • Storage: ZFS (zpool / zvol / datasets)
  • Control plane: Rust (axum) daemon on TLS port 443 (HTTP :80 redirects to HTTPS). Dev mode override binds :8443.
  • Installer UX: Debian-based live ISO built with live-build; post-boot runs a browser-based wizard (web-installer) and a text-mode TUI (crates/live) for the initial install. The install is fully offline (rsync of the live rootfs; no apt-get install during install).
  • Post-install UX:
    • Browser dashboard — two surfaces coexist; v2 is canonical:
      • web-ux-v2 modern (canonical, mounted at /): TypeScript + Vue 3.5 + Vite + Pinia + Vue Router (brand color #0566C1, light + dark + density themes, command palette, 4-step VM creation wizard, 7-tab VM detail, persisted activity log, hosts & hardware view)
      • web-ux legacy (off-ramp at /legacy/): vendored Vue 3 + Pinia + Tailwind (plain JS, hash-routing). Kept reachable for ad-hoc bookmarks; the v2 sidebar no longer surfaces it.
      • Compatibility: every /v2/<rest> path 308-redirects to /<rest> so existing bookmarks, tickets, and notifications keep working.
    • Text-mode Direct Console (console, formerly dcui) — 23 admin screens on tty1 (auth, dashboard, config_menu, network/network_ipv4/network_regenerate/network_restore/network_test, dns_hostname, ntp, keyboard, logs, diagnostics_bundle, usb_export, debug_server_{cfg,run,sess}, restart_agents, reboot, factory_reset, maintenance, troubleshooting, lock) after a boot splash (boot-splash)
  • Cluster / datacenter manager (future 1.0+): VirtDCM control plane

Owner: Tony Castrejón — SYNNET (Culiacán, MX).

All UI strings, TUI screens, logs, commit messages, error messages, and documentation are in ENGLISH by default. Spanish is a future i18n target. Do not emit Spanish in code or commits unless a feature explicitly names “Spanish i18n” as its goal.

The web-ux-v2 bundle ships en_us.json (real strings) + es_mx.json (TODO_es:-prefixed placeholders). Flipping to es-MX as default lands in a dedicated feat(i18n): es-MX as default locale feature when the translations are reviewed.

Do not mention VMware / ESXi / Proxmox / Hyper-V / Nutanix / vSphere / vSAN by name in code, inline comments, commit messages, or docs inside the repository. Describe functionality by technical behavior instead. Runbook notes to Tony (in chat) may mention these products as his reference environment, but they never enter the repo.

Migrator source adapters use protocol acronyms instead (vim_api, wmi_api, pve_api, prism_api, xen_api, libvirt_remote) — those are technical names, not product names.

Waiver pattern: if a skill description references third-party products, rewrite to technical language (“post-install Direct Console”, “text-mode admin TUI”, “canonical 20-screen admin console”, “KVM-based hypervisor console”).


/home/synvirt/dev/
├── Cargo.toml # workspace manifest (50 active crates)
├── CLAUDE.md # this file
├── CHANGELOG.md # canonical release history
├── TODO.md # short-lived workspace TODOs
├── README.md # public-facing overview
├── docs/
│ ├── BACKEND_CONVENTIONS.md # canonical engineering rules
│ ├── FRONTEND_PRINCIPLES.md # canonical UX rules
│ ├── architecture.md # component map + data-flow diagrams (lowercase; legacy uppercase ARCHITECTURE.md was renamed)
│ ├── architecture/ # deep architecture docs
│ ├── modules/ # per-module deep-dives (see §10 index)
│ └── superpowers/specs/ # design specs from brainstorming sessions
├── brand/ # SYNVirt brand assets — single source of truth
│ ├── README.md # usage guidelines + downstream derivation table
│ ├── colors.md # sRGB hex per dashboard token (light + dark)
│ ├── logo/ # word-mark + app icons
│ │ ├── synvirt-logo.{png,webp}
│ │ ├── synvirt-icon.png # 512×512 square — source for Windows .ico,
│ │ │ UEFI splash BMP, etc.
│ │ ├── favicon.ico + favicon-{16,32}x*.png
│ │ ├── apple-touch-icon.png
│ │ └── android-chrome-{192,512}x*.png
│ └── backgrounds/ # login-bg.png, clouds.webp
├── crates/ # 50 active workspace crates
└── iso-builder/ # live-build pipeline → output/*.iso

brand/ is the single source of truth for SYNVirt branding. Any downstream artifact that needs a logo / icon / accent colour pulls from here. Native installers regenerate their own derivations (Windows .ico, UEFI splash .bmp, etc.) — see brand/README.md for the canonical commands. Do NOT edit a downstream copy when the upstream changes; regenerate from brand/.

Grouped by domain. The dashboard crates/web-ux-v2/ lives on disk alongside the workspace and is built by npm, not cargo; same for crates/web-installer-v2/.

Control plane

Crate Role
core (synvirt-core) Shared lib: constants, error, traits, types
daemon (synvirt-daemon) axum control-plane HTTP API (TLS); D-Bus client of every separated plane
agent (synvirt-node-agent) Per-host node agent (cluster lifecycle)
cli (synvirt-cli) Operator CLI binary

Data planes (separate systemd services; the daemon is a thin client over synvirt-ipc HTTP-over-UDS)

Crate Role
synvirt-network OVS, vSwitches, firewall apply, boot reapply (R1, shipped; ultimately runs in-process — Commit 6 collapsed its D-Bus path)
synvirt-storaged ZFS storage data plane (R2, shipped). Owns the pool registry and the storage-object lifecycle control plane (zpool create/destroy/rename, import/adopt); serves /api/v1/storage/* over a private UDS, daemon proxies onto it. Streaming zfs send/recv data paths execute in the owning daemon (migrator, snapshots, backupd — doctrine amended 2026-07-01; see docs/BACKEND_CONVENTIONS.md §External command execution).
synvirt-migratord VM-import engine data plane (JobManager + sled StateStore + orchestrator) on a private UDS, so an in-flight import survives a daemon restart.
synvirt-updaterd ISO-based transactional update flow (preflight, boot-environment checkpoint, apply, reboot, rollback, post-verify) on a private UDS, independent of the daemon it restarts.
synvirt-livemotiond VM live/offline migration jobs on a private UDS; SQLite job state survives a daemon restart (Fix70 philosophy).
synvirt-backupd Backup Job / Repository / Restore Point control surface on a private UDS; owns the redb metadata store (P1 skeleton).
synvirt-ipc Shared HTTP-over-Unix-domain-socket transport used by every sub-daemon and the daemon’s proxy.
future synvirt-host Time, swap, packages, BMC, services panel, certificates (R3, planned). synvirt-host-tune already covers the runtime sysctl/sysfs slice.

Storage abstraction (the trait/types layer behind the synvirt-storaged data-plane service — R2 shipped)

Crate Role
synvirt-storage Backend-agnostic StorageBackend trait + PoolSpec / PoolPlan types + sled-backed PoolIdPoolLabel registry. Performs no I/O itself.
synvirt-zfs ZFS implementation of StorageBackend: pool + dataset lifecycle, partitioning, snapshots, adoption probe.
synvirt-storage-types Storage wire DTOs + error envelope shared between synvirt-storaged and the daemon’s /api/v1/storage/* proxy (types only — no ZFS code).

Cluster control plane

Crate Role
synvirt-cluster Raft-replicated cluster control plane: ed25519 identity, replicated state machine (openraft), mesh mTLS transport (tonic + UDS), pairing, quorum/witness, leader singletons, VIP intent. Phase A→D landed; the daemon embeds it and serves openraft from boot.

Backup (SynBaaS)

Crate Role
synvirt-backup-core Shared backup metadata types + error enum (Jobs, Repositories, Restore Points, schedules, retention) consumed by synvirt-backupd and the daemon proxy. P1 skeleton; no engine yet.

Settings-module service crates (consumed by daemon today; some migrate to synvirt-host in R3)

Crate Role
synvirt-time NTP / chrony + canonical AuditStore reused by every settings section
synvirt-resources System Resource Reservation (CPU + RAM headroom)
synvirt-services systemd Services panel (allowlist + D-Bus introspection)
synvirt-firewall nftables inet synvirt table CRUD
synvirt-swap File-based swap at /var/lib/synvirt/swap.img
synvirt-packages APT list / refresh / upgrade with SSE progress
synvirt-bmc BMC connection (Phase 8a; AES-256-GCM credential store)
synvirt-certs TLS cert lifecycle: self-signed, uploaded, ACME (HTTP-01 + DNS-01) — Phase 9
synvirt-update Update orchestrator + post-verify state machine
synvirt-host-tune Runtime sysctl / sysfs / cgroup tunings applied on every daemon start (KSM, THP, IRQ affinity, CPU governor). Boot-persistent tunings stay in daemon::host_tunings.
synvirt-observability Host-wide health surface — declarative Detectors emit Issues on a fixed schedule; consumed by /v1/observability/* and the dashboard’s Activity composite.

Identity, access control & logging (in-process daemon domains)

Crate Role
synvirt-identity Local users + RBAC roles/permissions + sessions + the authz guard + hardening. sled at /var/lib/synvirt/identity.db/; reuses the shared AuditStore. Authz enforcement defaults to report-only; a pinned super-admin (default root) is a guaranteed break-glass. See §5.16.
synvirt-logging Host logging subsystem: log viewer/follow/bundle, runtime log levels, remote syslog forwarder (RFC 5424 UDP/TCP/TLS), audit→syslog with redaction, rotation. Config at /etc/synvirt/logging.yaml. Egress OFF by default. See §5.15.

TUI / boot UX

Crate Role
console Post-install TUI (tty1, 23 screens)
console-gateway VNC WebSocket↔TCP-socket bridge
console-ticket Single-use console ticket issuance
boot-splash Boot progress UI on tty1 (12 steps + recovery)
tui-theme Shared TUI palette + layout
live Boot-time TUI (ISO live session)

Frontend

Crate Role
web-installer Legacy hand-rolled HTML+vanilla-JS pre-install wizard (off-ramp during v2 migration)
web-installer-v2 Canonical pre-install wizard — Vue 3.5 + TypeScript + Vite SPA. Served by the daemon when /etc/synvirt/setup.completed is absent. Lib stub; built by npm, not cargo.
web-ux Legacy dashboard (vendored Vue/JS, mounted at /legacy/)
web-ux-v2 Modern dashboard (TS+Vite, mounted at /, canonical; built by npm, not cargo)

Hypervisor + guest

Crate Role
synvirt-libvirt libvirt/KVM wrapper (virsh subprocess)
synvirt-guest-proto Portable guest protocol types
synvirt-guest-tools Host-side guest tools supervisor
synvirt-guest-agent Modern in-VM agent (Linux/BSD/Win 7+)
synvirt-guest-agent-legacy C agent for Windows XP / Server 2003
synvirt-guest-installer .NET 8 WinForms installer EXE
synvirt-guest-installer-rs Rust shim that builds the .NET installer + stages embedded payloads
guest-tools-iso Hybrid ISO builder for virtio-win + agents
synvirt-iso Daemon-side ISO library reader (inotify-watched)
migrator (synvirt-migrator) VM import engine (live pull + offline import)
synvirt-input Universal QMP input-send-event injection via libvirt virsh qemu-monitor-command. Backs the SYNVirt Direct Console keyboard/mouse path against any display backend without reconfiguring guests.
synvirt-webrtc Server-side WebRTC for the SYNVirt Direct Console. Merged into the daemon process in commit 7 — single HTTPS + signaling port, single UDP port, no second systemd unit.

crates/installer is on disk but excluded from the workspace — dead legacy code. Do not edit it; do not assume changes there have any effect on a real install. The live install flow is split across three components:

  • crates/live — TUI on the live ISO (welcome → nic → mode → static_ip → applying → installing → handoff screens). Collects the operator’s choices and hands off to the daemon.
  • crates/web-installer-v2 — canonical Vue 3.5 + TS pre-install wizard, served by the daemon at / when /etc/synvirt/setup.completed is absent. Same role as the TUI but for browser-based installs.
  • crates/daemon/src/api/install.rs — HTTP /api/install/* endpoints that consume the wizard payload, write /etc/synvirt/network.yaml in the target chroot (the canonical source of truth), rsync the live rootfs to disk, and finalise. synvirt-network.service then applies that YAML on first boot (OVS bridge + ipMGMT L3 via direct_apply); no netplan write, no netplan generate chroot call. This is where the post-install network config actually lands today (not in crates/installer/).

The ovs crate that earlier roadmaps mentioned was never built as a separate crate — synvirt-network (R1) took over the OVS surface end-to-end. synvirt-storage + synvirt-zfs (above) cover the storage abstraction today; R2 lifted them into the dedicated synvirt-storaged systemd service (committed 12606977), so the daemon’s storage handlers are now thin clients of synvirt-storaged over synvirt-ipc rather than in-process callers.

Domain ownership invariant (no duplicates)

Section titled “Domain ownership invariant (no duplicates)”

One crate per domain. Other services do not duplicate these domains; if they need data from one of them, they ask the owner via function call (intra-process) or D-Bus (cross-process). The dropped R1.2 stub (parallel synvirt-certs “lite”) was the antipattern this rule prevents from recurring. See MEMORY.md for the close-out narrative.

crates/daemon/src/ carries the full module catalog beyond the REST handlers in api/:

Submodule Role Doc
api/ REST handlers (per file). Network handlers live in api/network.rs and dispatch directly to the in-process synvirt-network NetworkController (AppState::network_controller); Commit 6 collapsed the earlier D-Bus indirection and removed the legacy-network-direct feature. inline rustdoc
auth.rs PAM Basic Auth middleware inline rustdoc
activity/ Persisted activity log + cursor-paginated GET /api/v1/activity inline rustdoc
iso_detection/ ISO content detection pipeline + sled cache + SSE events docs/modules/iso-detection/
libvirt_renderer/ ResolvedProfile + VmContext → libvirt domain XML docs/modules/libvirt-renderer/
vm_profiles/ Declarative VM hardware catalog + overlays + hierarchical browse docs/modules/vm-profiles/
console/ VNC console policy + ticket issuance inline rustdoc
cpu.rs Host CPU baseline detection inline rustdoc
host_tunings.rs Boot-persistent host tunings (companion to synvirt-host-tune runtime knobs) inline rustdoc
perf_reapply.rs Reapply universal VM perf tunings on daemon start inline rustdoc
storage_bootstrap.rs Daemon-startup auto-import of declared zpools inline rustdoc
telemetry/ Guest telemetry receiver (virtio-vsock side-channel, CID→UUID supervisor, SSE) inline rustdoc
api/update_v2.rs Update REST handler; the transactional update flow (ISO scan, compatibility, apply, post-verify, rollback) now lives in the synvirt-updaterd data-plane crate, which this handler proxies inline rustdoc
vm_layout.rs /rpool/vms/<slug>/ consolidation + first-start migrator inline rustdoc
tls/ Self-signed cert + key bootstrap (initial install); rotation thereafter is synvirt-certs’s job inline rustdoc
policy.rs Per-VM sled policies: USB redirection + the guest run-command gate (disabled by default) inline rustdoc
vm_tags.rs Per-VM tags store (sled), surfaced in the enterprise VM inventory inline rustdoc
api/templates.rs VM template library — capture / deploy-from-template over the snapshot+clone primitives (sled templates.db) inline rustdoc
api/vm_resources.rs Per-VM CPU/memory resource controls (<cputune>/<memtune>, live + persistent) inline rustdoc
api/authz.rs + api/security.rs RBAC guard (report-only by default) + the Security surface (users/roles/sessions/hardening), delegating to synvirt-identity inline rustdoc
api/audit.rs + api/csv_export.rs Audit-log read surface + shared RFC-4180 CSV/JSON exporter (events/tasks/audit) inline rustdoc
api/logs.rs + api/logs_forward.rs Log viewer/levels/syslog REST + the forwarder ingestion bridge, delegating to synvirt-logging inline rustdoc
settings/ HTTP routes + envelope + audit hooks for the Settings module sections (time, reservation, services, firewall, swap, packages, bmc, certificate). Each route delegates to the matching synvirt-* service crate. inline rustdoc
web/ Static assets + route mount points inline rustdoc
config/ YAML config parser inline rustdoc

Versioning: iso-builder/VERSION is the single source of truth for the SynVirt release string. Every crate’s Cargo.toml uses version.workspace = true, and the workspace placeholder is pinned at "0.0.0-dev" — it never reaches a binary. Each crate that displays a version at runtime has a build.rs that reads iso-builder/VERSION and exports it as SYNVIRT_RELEASE; runtime code reads env!("SYNVIRT_RELEASE") (never CARGO_PKG_VERSION). The daemon’s RELEASE constant is concat!(SYNVIRT_RELEASE, "+", SYNVIRT_BUILD) (e.g. 0.15.0+build.774), where SYNVIRT_BUILD is a git-derived counter from crates/daemon/build.rs. VERSION holds a plain semver with no -FixN suffix (retired 2026-06-23) — the git build counter gives per-build identity; bump the semver for a release. The post-install console TUI also reads /etc/synvirt/release at runtime via crates/console/src/system/release.rs (the stamp file is written by the installer and rewritten by the updater).


Shipping: 0.15.0. ISO file at iso-builder/output/SynVirt-0.15.0-amd64.iso.

The 0.15.0 line ships the full snapshot stack on the v2 dashboard (manual create, restore, clone, delete, sanoid policy widget, space banner), the host-side telemetry pipeline end-to-end (virtio-vsock side-channel, CID→UUID supervisor, REST + SSE endpoints, dashboard chart), real clipboard + viewport-driven resize_display in the guest agent, the full VNC console toolbar (power, keys, media, snapshots, display) on the v2 dashboard, the Windows guest tray binary, the storage-capacity refinements (compression KPI card, disk tags, hosted-VMs in pool browse), and the hardware view enhancements in the sidebar.

Landed on the 0.15.0 branch since the line opened:

  • R1 — synvirt-network extraction (commits 38dd6ba through dfbcc30, plus e9c711d follow-up). OVS, vSwitches, firewall apply, and the boot reapply loop now live in their own systemd service. systemctl restart synvirt-daemon no longer touches OVS state — that’s the structural fix from the 2026-05-05 incident. R1 landed the daemon as a D-Bus client of synvirt-network; Commit 6 later collapsed that indirection back in-process (the daemon now calls the synvirt-network NetworkController directly) and removed the legacy-network-direct feature. Tag: r1-deployed-20260505.
  • Settings module Phase 9 — Certificate (commit 20eda30). Self-signed / uploaded / Let’s Encrypt with HTTP-01 + DNS-01 via Cloudflare, Route53, Google Cloud DNS, DigitalOcean. Hot-reload through RustlsConfig reload trait. Auto-renew via synvirt-cert-renew.timer.
  • VM-layout consolidation (commit 4bb5619). Every per-VM artefact lives under /rpool/vms/<slug>/; legacy installs migrate on first start.
  • Operator feature drop — Tasks 3-7 (2026-06-28, deployed to the fleet at 0.15.0+build.1243). Guest-OS matrix gaps (mismatch chip, gated guest run-command); VM lifecycle remainder (templates, per-VM resource controls, import-from-URL, OVA/OVF export); events/ tasks/audit gaps (task initiator, login/hardware events, audit source-IP + view, CSV/JSON export, notifications bell); the new synvirt-logging subsystem (§5.15); and the new synvirt-identity RBAC subsystem (§5.16). Two safety flags ship OFF by default: RBAC authz enforcement (report-only) and remote syslog egress. See MEMORY.md for the build/deploy narrative.

In flight on the 0.15.0 branch: incremental polish on the same 0.15.0 surfaces (CLI reconfigure-graphics SPICE strip, post-install PnP rescan, sanoid added to the live ISO package list).

Full release history: see CHANGELOG.md.

Open items (tracked in TODO.md + per-module ROADMAP.md):

  • RBAC enforcement is wired in report-only mode on a representative handler set — flipping to enforce (and broadening the guarded surface) is the operator-gated next step. Remote syslog egress + audit-forwarding ship built but disabled (no targets seeded).
  • synvirt-logging sub-daemon log-level fan-out over synvirt-ipc (the daemon’s own runtime-level reload is done; the split planes still take their level at start).
  • Per-module futures (libvirt-renderer hot-plug deltas, iso-detection persistent worker queue, …): see each module’s ROADMAP.md.

(Disk lifecycle attach/detach/resize, NIC attach/detach, and session-cookie auth — previously listed here — all shipped on 0.15.0.)


Full rules live in docs/BACKEND_CONVENTIONS.md and docs/FRONTEND_PRINCIPLES.md. Summary below. When these dedicated docs and this CLAUDE.md conflict, the dedicated docs win.

  • One Error enum per crate via thiserror, with machine-readable discriminant codes (e.g., code = "E_VM_NOT_FOUND").
  • Public APIs return Result<T, crate::Error>. anyhow::Result only in main and tests.
  • Public async fns annotated with #[tracing::instrument(skip_all)]. Use debug! / info! / warn! / error! — never println!.
  • Naming verbs carry meaning: get_* fails if absent, find_* returns Option, list_* returns Vec (empty OK), ensure_* idempotent create-if-missing, reconcile_* idempotent convergence, apply_* destructive, plan_* pure (no side effects).
  • No unwrap() / expect() in library code.
  • cargo fmt, cargo clippy -- -D warnings, cargo audit must pass.
  • Rustdoc on every pub item. First line = complete sentence with period.
  • Conventional Commits with crate scope: feat(synvirt-libvirt): ....
  • Subprocess calls: tokio::process::Command with timeout; capture stdout and stderr separately; never build shell strings.
  • Every #[utoipa::path] handler MUST set an explicit operation_id (otherwise utoipa derives one from the function name and openapi-typescript collapses duplicates).
  • Per-VM state DBs go to /var/lib/synvirt/<feature>.db/ (sled).

Shared across both surfaces:

  • Target operator = IT admin, not Linux expert.
  • Vocabulary translation (do not show raw terms in primary UI): zpool → Storage pool, domain → Virtual machine, vSwitch/bridge → Network, VLAN ID → Network tag, zvol → Virtual disk, uplink → Physical link. Raw terms allowed only in collapsed “Advanced” sections and tooltips.
  • Five-second rule: dashboard home must answer “Is everything OK?” at a glance.
  • Never show raw libvirt/virsh/systemctl errors. Map known errors to {user_message, suggested_action, severity} via the ApiError / ERROR_MAP surface.
  • Destructive actions use typed-name confirmation.
  • Empty states always include a primary CTA.
  • Smart defaults for create-VM (2 vCPU, 4 GB RAM, 40 GB disk, first pool, vSwitch0, first ISO available, start-now on).
  • Loading tiers: <100 ms no indicator; 100–500 ms skeleton; 500 ms-3 s spinner with context text; >3 s progress bar with cancel.
  • Never display: JSON payloads, UUIDs, Linux paths, ISO 8601 timestamps. Always resolve to names + relative times with absolute tooltip.
  • No purple-pink gradients (AI cliché). Accent gradient is cyan → blue (legacy) or #0566C1 brand (v2).
  • Icons: Lucide, inline SVG. No icon fonts.
  • Animation: 150–250 ms, ease cubic-bezier(.16, 1, .3, 1).
  • Radius: 12 px cards, 8 px inputs, 6 px buttons, 4 px badges.
  • All strings in crates/web-ux/assets/strings/en_us.json, called via Synvirt.t(key, vars).
  • Vendored Vue 3 + Pinia + Tailwind in crates/web-ux/vendor/, fonts (Inter + JetBrains Mono) in crates/web-ux/vendor/inter/ and vendor/jetbrains-mono/.
  • Hash-based routing.
  • Light theme is the only theme. Dark theme is web-ux-v2’s job.
  • <html lang="es" translate="no"> + <meta name="google" content="notranslate"> — Chrome Translate breaks Vue’s event delegation; never reintroduce automatic translation.
  • TypeScript 5.6 strict + noUncheckedIndexedAccess + verbatimModuleSyntax. No any. No unwrap-style escapes.
  • ESLint 9 flat config; npm run lint -- --max-warnings 0.
  • Vite 5 + Vue 3.5 + Pinia 2.2 + Vue Router 4.4 + vue-i18n 11 + Headless UI + lucide-vue-next + Tailwind 3.4.
  • API types generated from the daemon’s OpenAPI spec via openapi-typescriptsrc/api/generated/schemas.ts. Regenerate with npm run generate:api:from-file against the committed openapi.snapshot.json.
  • Typed openapi-fetch client; ApiError + ERROR_MAP curate every backend E_* code into operator-facing English copy.
  • SSE consumption via subscribeIsoDetection / per-feature composables; auto-cleanup in onUnmounted.
  • Vitest 2 + jsdom unit tests. Coverage target ≥ 75% on components/, stores/, composables/. Polyfills for ResizeObserver / IntersectionObserver / scrollIntoView in src/vitest.setup.ts for Headless UI under jsdom.
  • Brand color #0566C1 (token --brand). Dark theme + density variants live in src/styles/tokens.css.
  • Host-level abstractions go in their own crate when the surface outgrows a daemon submodule (synvirt-libvirt, synvirt-guest-*, migrator). Daemon-internal modules stay under crates/daemon/src/<name>/ with their own README in docs/modules/<name>/.
  • No hardcoded restrictions in the installer. Replace hard limits with soft warnings; the only blockers are what literally breaks install (zero disks selected, etc.).
  • Standard VGA / text video output for installers and TUIs, not serial-console hacks.
  • Binaries live at /opt/synvirt/bin/. systemd units at /etc/systemd/system/synvirt-*.service. Config at /etc/synvirt/. State at /var/lib/synvirt/.

This section is a quick map. Each module’s deep-dive lives in docs/modules/<name>/ (see §10 for the index).

Workspace shared lib: constants, the canonical Error / Result<T>, base traits (StorageProvider, NetworkProvider, ComputeProvider future surfaces), and shared types reused by the daemon, the CLI, and the node agent.

Wraps libvirt/QEMU-KVM via virsh subprocess for transparency and zero native build dependencies. Modules: client.rs, vm.rs (lifecycle including pause/resume), disk.rs, nic.rs, host.rs, boot_order.rs (CDROM media-swap patching), xml.rs (legacy XML emitter; the renderer at crates/daemon/src/libvirt_renderer/ is the new path), virsh.rs (subprocess helper), error.rs, strings.rs.

axum HTTP API on TLS :443 (or :8443 in dev). PAM Basic Auth middleware. Mounts:

  • /api/v1/* REST surface, all utoipa-annotated with unique operation_ids.
  • /api/v1/openapi.json + /docs/ Swagger UI.
  • /api/v1/events SSE for daemon events.
  • /api/v1/iso-detection/events SSE for detection lifecycle.
  • / static-served TS bundle (dist of web-ux-v2) — canonical.
  • /legacy/* legacy web-ux static bundle (off-ramp).
  • /v2/* 308-redirects to / for compatibility with old links.
  • /console/vnc/{uuid}?ticket=… WebSocket bridge for VNC consoles.

Submodules — see the crates/daemon/src/ table in §2.

Three modules ring the wizard’s data flow:

vm_profiles ──describes the recipe──▶ libvirt_renderer ──emits domain XML──▶ libvirt
▲ ▲
│ │
iso_detection ──suggests a recipe via OsHint─┘
  • vm_profiles — declarative catalog of every supported guest OS, with security + networking overlays composed at resolve time. Hierarchical browse endpoints (/families/{family}/sub-families/{sub_family}/versions) back the wizard’s three cascading dropdowns.
  • iso_detection — 11-detector pipeline that probes uploaded ISOs, caches results in a sled DB, and emits SSE lifecycle events.
  • libvirt_renderer — turns ResolvedProfile + VmContext into the libvirt <domain> XML. 14 element renderers. 6 insta snapshots + virsh-validate integration test gate the output.

TypeScript + Vue 3 SPA mounted at / (canonical surface). Built on Vite + Pinia + Vue Router + vue-i18n + Tailwind + Headless UI. Token-driven theme with light / dark / density variants. AppShell with command palette (⌘K) + 11 built-in commands.

Routes today: /login, / (Dashboard home), /iso-library, /templates (template library), /vms (list + bulk actions), /vms/:name (multi-tab detail), /vms/new (create wizard), /storage*, /network*, /cluster, /migrator*, /backup, /security (users/roles/sessions/hardening), /activity, /settings, /hardware, /monitor/{issues,tasks,events,audit,logs, logging} (observability + logs), /playground (component QA). Note: the Logging-admin view (/monitor/logging — syslog targets, runtime levels, retention) has no sidebar entry yet; it is reached from a link inside /monitor/logs.

API contract: types generated from the daemon’s OpenAPI spec via openapi-typescript. Typed openapi-fetch client + ApiError mapping for 25+ backend codes.

Tests: vitest under jsdom. Build size: ~210 kB JS / 75 kB gzip main + lazy chunks per route.

crates/console is the post-install 23-screen ratatui TUI on tty1. crates/boot-splash is the boot progress UI (12 steps + interactive recovery screen). Both share crates/tui-theme for palette. Debug HTTP server (default :8443 per crates/console/src/debug/config.rs; TLS on, token-auth on; persisted at /etc/synvirt/debug.yaml) exposes live logs + state + USB-bundle export.

crates/console-gateway is a WebSocket↔TCP-socket bridge that proxies VNC. crates/console-ticket issues HS256 JWTs (single-use, 30 s TTL, 24 h key rotation with 30 min grace). The bridge dials 127.0.0.1:5900+display directly via libvirt’s vncdisplay. The browser uses noVNC against /console/vnc/{uuid}?ticket=…. SPICE is not part of the stack — VNC is the only console path.

Five crates form the suite:

  • synvirt-guest-proto — portable protocol types (compiles for Linux/BSD/Win 64/Win 32 XP-2003).
  • synvirt-guest-tools — host-side supervisor inside the daemon (per-VM session state machine, sled-backed VmToolsState).
  • synvirt-guest-agent — modern Rust agent for Linux 3.10+ / BSD 12+ / Windows 7+. Packaged as .deb, .rpm, FreeBSD pkg, Windows .exe.
  • synvirt-guest-agent-legacy — C agent for Windows XP / Server 2003 i686.
  • synvirt-guest-installer — .NET 8 WinForms self-contained EXE (SynVirt-Setup.exe) with rollback stack + silent CLI mode. Its embedded payloads (virtio-win drivers, qemu-ga.msi, synvirt-agent.exe) are external artifacts, never committed: pinned by SHA-256 in iso-builder/vendor/guest-payloads/MANIFEST.toml, staged by iso-builder/scripts/prepare-guest-payloads.sh, verified and copied into dotnet/Embedded/ by build.rs at compile time. A missing source or hash mismatch is a hard build failure — no stubs. See docs/modules/guest-tools/INSTALLER-PAYLOADS.md.

Transport: libvirt <channel type='unix' mode='bind'> at /var/run/synvirt/agent/{uuid}.sock. Daemon dials in as a client. Protocol spec: docs/GUEST_AGENT_PROTOCOL.md.

Dashboard surface (Basic Auth): GET /api/v1/vms/:name/tools (cached state) + POST .../tools/{get-info,shutdown,reboot,set-hostname}.

crates/guest-tools-iso packs virtio-win drivers + synvirt-agent-* into a hybrid ISO with autorun scripts and a SHA-256 manifest. spice-guest-tools and spice-vdagent are no longer staged. Staged at /opt/synvirt/isos/guest-tools.iso.

Separate systemd service that owns the OVS surface: vSwitches with VLAN + bond support, firewall apply, the boot-time reapply loop with uptime guard, and a YAML source of truth at /etc/synvirt/network.yaml editable from a console session. Sled cache at /var/lib/synvirt/network.db/ is rebuilt from YAML on every start.

Ownership invariants (single owner). /etc/synvirt/network.yaml is the canonical persistent source of truth and synvirt-network is the only applicator/reconciler. Installer, console TUI, and web installer must not write netplan for SynVirt-managed networking; the legacy netplan path was migrated out and the boot binary disables any leftover conflicting netplan / systemd-networkd drop-ins on each start (see legacy_migration module). When editing L3 / management-IP / DNS code, the only mutation surfaces are: crates/daemon/src/api/install.rs (installer chroot write of network.yaml), crates/console/ (post-install TUI read/write of network.yaml via synvirt-network::yaml_store), and synvirt-network itself (apply / reconcile / verify).

Physical OVS uplinks are always L2-only — no IPv4, no IPv6, no DHCP, no RA, no SLAAC, no NDP. The boot binary regenerates /etc/sysctl.d/99-synvirt-uplinks.conf every start with disable_ipv6=1, accept_ra=0, autoconf=0 for every uplink kernel name and writes the same values straight into /proc for immediate effect (see uplink_l2 module). Host L3 lives on KernelPorts only: ipMGMT carries management, ipStorage storage, ipMigration migration. DHCP / static management IP must be applied only to ipMGMT; no new code may assign L3 to a physical OVS uplink. DNS comes from the default vSwitch’s ipmgmt.dns field and is materialised by the boot binary as a systemd-resolved drop-in at /etc/systemd/resolved.conf.d/99-synvirt.conf. Post-apply invariants are verified each start by the invariants module and every violation is logged at error! level (boot does not abort so the operator can recover via dashboard / TUI).

In-process surface: Commit 6 collapsed the earlier synvirt-network D-Bus indirection. The daemon holds an Arc<NetworkController> (AppState::network_controller, built by api::network::build_controller) and the /api/v1/vswitches/* + /api/v1/network/* handlers in api/network.rs dispatch to it directly.

Locked safety rules:

  1. Bridge in YAML, missing in OVS → create.
  2. Bridge in OVS, missing in YAML → log warning, surface drift, never auto-delete.
  3. Empty YAML + non-trivial OVS on initial start → refuse to start (E_NETWORK_RECONCILE_REFUSE); operator decides.
  4. Boot reapply only fires inside the 60 s uptime window of the service. To reapply on demand after that window (operator action), run synvirt-network reconcile — it bypasses the uptime guard and reconciles OVS directly. The post-install console’s “Regenerate network from defaults” and IPv4-apply screens use this path.

Boot order: network-pre.target → openvswitch-switch.service → synvirt-network.service → libvirtd.service → synvirt-daemon.service. systemd-networkd remains enabled to keep network-online.target firing for downstream services (chrony, etc.) but owns nothing SynVirt cares about — synvirt-network.service creates the OVS bridge, enforces L2 on every uplink, and applies L3 on ipMGMT via direct_apply. The boot binary runs in order: 1) load + validate network.yaml (hard gate), 2) legacy netplan / networkd remediation (back up + disable conflicting files under /var/lib/synvirt/network-migration/<ts>/), 3) vmnic reconcile, 4) OVS boot reapply (uptime-guarded), 5) vmkernel direct_apply, 6) uplink L2-only enforcement, 7) DNS drop-in, 8) post-apply invariant verification.

Dashboard creates/edits via the /api/v1/vswitches HTTP shape; the daemon’s handlers call the in-process NetworkController directly (Commit 6 — no D-Bus).

Commit-confirm + gateway probe is preserved daemon-side: applying a switch arms a 60 s watchdog and a parallel ICMP probe to the default gateway; on failure the daemon re-applies the previous spec (or deletes a first-time vSwitch) through the NetworkController to roll back.

Deep dive: docs/modules/synvirt-network/. Operations runbook: docs/operations/refactor-r1-network.md.

crates/daemon/src/activity/ — sled-backed persisted activity log with inverted-timestamp keys for newest-first iteration + base64 cursor pagination. GET /api/v1/activity returns paginated events; SSE merges new events into the v2 dashboard’s Activity view. VM lifecycle handlers (start/stop/pause/resume/force-off/ reboot) emit events.

VM import engine with two parallel ingress paths: live pull from external hypervisor control planes (six adapters), and offline import of portable archives or disk images (ten formats). Both paths land on the same DestinationSink (default ZfsZvolSink) and share the post-ingest pipeline (firmware mapping, virtio injection for Windows guests, libvirt domain definition, start). Public façade: JobManager with sled-backed StateStore, mounted at /api/v1/migrator.

Deep dive: docs/modules/migrator/.

TLS certificate lifecycle for the daemon’s :443 listener. Three sources: self-signed (installer-generated default), operator- uploaded PEM bundle, ACME (Let’s Encrypt) via HTTP-01 or DNS-01. DNS-01 providers: Cloudflare, Route53, Google Cloud DNS, DigitalOcean — credentials encrypted with the same AES-256-GCM EncryptionKey synvirt-bmc introduced in Phase 8a, decrypted only into /run/synvirt/cert-provider-creds/ for the duration of a certbot run, drop guard unlinks afterwards.

Hot reload: RustlsConfig::reload_from_pem_file swaps the running keypair without dropping connections. Auto-renew: synvirt-cert-renew.{timer,service} fires daily at 03:17 with a 30 min jitter; the script POSTs the renewal endpoint over loopback so audit + hot-reload still apply.

Routes under /api/v1/settings/certificate: GET / (state), POST /upload, POST /letsencrypt, POST /renew, POST /reset-self-signed, GET /download, plus an SSE stream at /api/v1/changes/:change_id/stream for certbot progress.

Domain ownership: synvirt-certs is the only crate that parses or generates certificates. The daemon consumes it for :443; no other service handles cert material.

Each Settings panel is backed by a dedicated synvirt-* crate that the daemon mounts under /api/v1/settings/<section>:

Section Crate Notes
Time synvirt-time NTP / chrony + canonical AuditStore reused by every section
Reservation synvirt-resources CPU + RAM headroom for the host plane
Services synvirt-services systemd panel; allowlist in synvirt-services::allowlist() is the single source of truth
Firewall synvirt-firewall nftables inet synvirt table CRUD
Swap synvirt-swap /var/lib/synvirt/swap.img + sysctl drop-in
Packages synvirt-packages APT list / refresh / upgrade with SSE
BMC synvirt-bmc Connection-only Phase 8a; AES-256-GCM credential store
Certificate synvirt-certs See §5.12

Cross-cutting helpers in synvirt-time:

  • synvirt_time::audit::AuditStore — sled-backed audit log shared across every settings section. Opened once in main.rs and passed to each service’s constructor; never sled::open() it twice (the post-R1.2 BMC bug — see MEMORY.md).
  • synvirt_time::exec::Exec — subprocess abstraction reused by synvirt-firewall, synvirt-swap, etc.

synvirt-bmc::encryption::EncryptionKey is also cross-crate: synvirt-certs reuses it for DNS-01 provider credentials so operators see one keyfile, not two. synvirt-bmc::encryption::ensure_machine_id is the canonical healer for missing /etc/machine-id on cloned VMs — runs idempotently inside EncryptionKey::load.

Multi-phase split of the monolithic daemon into separate systemd services so a control-plane restart never touches the data plane:

Phase Status Scope
R1synvirt-network ✅ Shipped on 0.15.0 (tag r1-deployed-20260505) OVS, vSwitches, firewall apply, boot reapply. (R1 made the daemon a D-Bus client; Commit 6 later collapsed it back in-process.)
R2synvirt-storaged ✅ Shipped on 0.15.0 (commit 12606977) ZFS pools, datastores, auto-import, the /api/v1/storage/* surface. Dedicated systemd service (synvirt-storaged), synvirt-ipc HTTP-over-UDS transport, daemon thin client. Startup is read-only w.r.t. existing pools (probe + import-if-absent), so a control-plane restart never touches the ZFS data plane.
R3synvirt-host 📐 Planned Time, swap, packages, services panel, BMC, certificates — every Settings section that today loads in-process inside the daemon migrates to its own service. Single org.synnet.synvirt.Host1 interface.
R4 — Daemon reduction + rename 📐 Planned Daemon shrinks to “API only”: HTTP, auth, OpenAPI, dashboard static, websocket bridges. Removes any remaining legacy-*-direct Cargo features (the legacy-network-direct one is already gone — Commit 6). Renames synvirt-daemon.servicesynvirt-api.service (the placeholder is already in synvirt-services::allowlist()).

Per-refactor invariants live in MEMORY.md: every new service crate inherits the “single sled handle / accept &Db from the caller” rule, the ensure_machine_id heal-on-load pattern when deriving keys, the “single path constant per cross-service file” rule, and the domain-ownership rule (no duplicates per domain).

5.15 synvirt-logging (host logging subsystem)

Section titled “5.15 synvirt-logging (host logging subsystem)”

In-process domain crate (loaded inside the daemon, like synvirt-firewall; not a separate service in v1) owning the logging surface end-to-end. Read path (Phase 0/1): a LogSource registry (journal units, /var/log/synvirt files, per-VM QEMU logs, audit), bounded pull with search/since/until/severity, SSE follow (the events_sse broadcast pattern), raw download, and a host support bundle — the generator was extracted out of crates/console so the TUI and the daemon call one implementation. Admin path (Phase 2/3/4): runtime per-subsystem log levels via a tracing_subscriber::reload handle on AppState; a remote syslog forwarder (RFC 5424 framing, UDP/TCP/TLS on rustls, per-target bounded queue + drop-oldest + backoff so a dead SIEM never stalls the daemon); audit→syslog forwarding through a mandatory redaction pass; and rotation/retention of SynVirt-owned files.

Safety: egress is OFF by default — zero targets seeded, targets created disabled, and no code path egresses a raw audit record. Config at /etc/synvirt/logging.yaml. REST under /api/v1/logs/* (+ /api/v1/logs/syslog/targets, /api/v1/logs/levels, /api/v1/logs/retention). Deep dive: docs/modules/synvirt-logging/.

5.16 synvirt-identity (RBAC / access control)

Section titled “5.16 synvirt-identity (RBAC / access control)”

In-process domain crate owning users + roles + sessions + authorization. sled at /var/lib/synvirt/identity.db/; reuses the shared synvirt-time::AuditStore. Authorization is modelled as data, not walls: a flat permission catalog (vm:*, storage:*, network:*, settings:*, security:*, host:*, backup:*), three editable builtin roles (admin / operator / read-only), and a pure decide(grants, needed). The guard (api/authz.rs::require) defaults to report-only (logs “would deny”, allows); enforce is operator-opt-in via PUT /api/v1/settings/security/enforcement.

Guaranteed break-glass: a pinned super-admin set (default {"root"}, configurable) that require() short-circuits to Allow before any record/role/enforcement/DB-error evaluation — it can never be demoted, disabled, deleted, or emptied (E_SUPER_ADMIN_PINNED / E_SUPER_ADMIN_EMPTY), and E_LAST_ADMIN guards the final admin. The OS-level root (SSH / PAM / Console TUI) stays outside RBAC as the deeper recovery path. PamBackend wraps the existing auth.rs PAM path (the single process-wide PAM lock is preserved); LDAP/Kanidm + MFA are seam-only. Hardening (SSH toggle, lockdown liftable from the Console TUI, brute-force soft-lockout, audit policy) composes the existing services. REST under /api/v1/{permissions,roles,users,sessions} + /api/v1/settings/security/* + /api/v1/audit/*. Deep dive: docs/modules/synvirt-identity/.


Full spec: docs/architecture/host-roles.md (see TODO above).

Key rule: one role per host (no dual-role hardware). A cluster mixes nodes of different roles freely.

Workload roles (coexist in the same cluster)

Section titled “Workload roles (coexist in the same cluster)”
  • HCI — Compute + Storage colocated (dense, simple, ideal 3-8 nodes)
  • Compute — VMs only, consumes remote storage
  • Storage — Storage only, exposes the pool to the cluster

Data-protection roles (dedicated, exclusive hardware)

Section titled “Data-protection roles (dedicated, exclusive hardware)”
  • Backup — Host dedicated to S3-immutable backups with object lock (compliance or governance mode). Isolation prevents I/O windows from degrading production VM performance.
  • Replica — Host dedicated to async DR replication to a remote cluster. Isolated network + storage.
  • A (All-HCI, SMB recommended, 3-8 nodes): all HCI + optional Backup + optional Replica.
  • B (Compute + Storage separated, enterprise classic): 3 Compute + 3 Storage + Backup + Replica.
  • C (Mixed, very common — incremental growth): 3 HCI legacy + 1 Compute with GPU + optional Backup.
  • D (Storage-heavy, I/O intensive): 1 Compute + 4 Storage + Backup.

Dashboard → right-click in the nodes view → Add new host. Dialog asks: management IP, root password or SSH key, role. Role cannot be changed in-place; to change you drain + remove + re-add with new role.


  • Local-only git repository (no remote). git remote -v is empty by design. Off-box durability is provided by Veeam image-level backups of the dev VM — there is no remote to push to. Never add a remote or git push.
  • Daily snapshot alias (commit only, no push): xnap='git add -A && git commit -m "snap: $(date +%Y%m%d-%H%M)"'

Test fleet (the dev appliance at 10.10.26.65 is RETIRED)

Section titled “Test fleet (the dev appliance at 10.10.26.65 is RETIRED)”

The nested dev appliance that used to answer at 10.10.26.65 no longer exists (fleet re-IP 2026-06-22; retirement confirmed by the operator 2026-07-13). Never target, probe, or propose .65 for anything. Mentions of .65 elsewhere in docs/ and in old commit messages are historical records — do not update them, and do not resurrect the host from them. (The 10.10.26.65/24 strings in synvirt-network unit-test fixtures are arbitrary example CIDRs, unrelated to the retired host.)

End-to-end testing happens on the test fleet:

  • 10.10.26.11 (synvirt-01-vxrail) / 10.10.26.12 (synvirt-02-ml30) / 10.10.26.13 (synvirt-03-vm). SSH root. Raft cluster synnet-cluster, VIP 10.10.26.100.
  • Fleet deploys are operator-gated: plan + explicit GO per payload, one host at a time, leader last, guest QEMU PIDs must survive untouched. Wrapper: scripts/night-deploy-fleet.sh.
  • Pre-deploy evidence runs on hermetic local rigs on this dev box (file-backed zpool + defined-never-started domain + daemon/backupd from target/release), not on the fleet.
  • Disambiguation: a baremetal SynVirt host also reports hostname synvirt-01 at 10.10.26.52 (see docs/modules/webrtc-console/PIPELINE.md). When a task targets it, the prompt names .52 explicitly.
  • Historical nested-KVM baseline (CPU-Z 2.19, Windows 2022 guest, commit 828c978): 492 single / 1837 multi — recorded on the retired appliance; still the reference for nested-guest perf deltas.

Browser UI on this same dev host that launches parallel Claude Code agents with YOLO mode + persistent sessions + xterm shell + file browser. Replaces ad-hoc tmux + claude -p loops. Not part of the SynVirt product — local dev tooling only.

  • App: Next.js + Socket.io + Claude CLI wrapper at /opt/synvirtdev-console. PM2 unit pm2-root.service. App listens on 127.0.0.1:3100. server.js bootstraps TS via require('tsx/cjs').
  • Reverse proxy: Caddy is a local binary at /home/synvirt/dev/caddy-proxy/, NOT systemd. Reload with caddy reload --config .../Caddyfile --adapter caddyfile (admin API on localhost:2019).
  • Persistence: MariaDB (mariadb-server) + Redis. Database synvirtdev_console, user svconsole.
  • DNS: dev.synvirt.mx resolves via /etc/hosts — primary 10.10.26.1 (LAN ens192), secondary 10.61.26.5 (ZeroTier ztpp6mxztz, for remote admin only). LAN clients use internal DNS at 10.10.10.1. Cloudflare zone synvirt.mx is the same account as synnet.mx, so Let’s Encrypt DNS-01 reuses the existing CF token at /etc/caddy/cf.env. Legacy hostname synvirtdev.synnet.mx is retired as of 2026-05-19.
  • Claude CLI auth: /root/.claude/.credentials.json (Max subscription). claude-agent.ts strips ANTHROPIC_API_KEY from the spawned env so the CLI uses OAuth, never an API key.
  • Login root; password in /root/.synvirtdev-console-secrets (mode 600).
  • Agents run as root, shell PTY runs as root, no cwd sandboxing — intentional (this is Tony’s dev box, agents need libvirt / systemd / the SynVirt tree).

Configured broadly: Rust (cargo build/test/clippy, rustc), Node.js (npm, pnpm, yarn), shell (bash, sh), PHP (php, composer, wp-cli), databases (mysql, psql), web servers (caddy, nginx), process managers (pm2), containers (docker), security tools (crowdsec, fail2ban), and filesystem utilities including rm on cache paths.

rm on cache paths like /var/cache/nginx/fastcgi/* or iso-builder/live-build/cache/ is routine — Claude Code should not flag or ask permission.

Tony works under an “autonomous-on-VirtSYN” rule: Claude Code executes full master prompts end-to-end without mid-phase confirmation. If a phase partially fails, commit what works with explicit “KNOWN: phase N incomplete because X” in the commit body, then proceed.

Do not invoke /superpowers:brainstorming for execute-this-plan prompts. Master prompts with numbered phases are treated as approved specifications.

For singletons whose lifetime matches the daemon/console process:

once_cell::sync::OnceCell<Arc<Mutex<Option<T>>>>

Exposed via set_running(t), get_running() -> Option<&T>, clear_running().

iso-builder/live-build/cache/ persists across builds. FORCE_REBUILD=1 purges it fully. ZFS is compiled via DKMS inside the chroot and cached there.

Typical build times:

  • First build / FORCE_REBUILD=1: 30-45 min
  • Incremental: 2-5 min
  • iso-builder/live-build/config/hooks/normal/9999-synvirt-enable.hook.chroot forces multi-user.target.wants/ symlinks for synvirt-live.service
    • synvirt-daemon.service during the live-build chroot stage.
  • Matching target-side fallback in the daemon’s install handler: after each systemctl enable <svc> inside the target chroot we drop an idempotent ln -sf for every custom synvirt-* unit.
  • The install pipeline rsyncs the live rootfs into the target instead of debootstrap + apt-get install, so the install is fully offline.
  • iso-builder/build.sh runs npm install + npm run build in crates/web-ux-v2/ and stages the dist/ under /opt/synvirt/web-ux-v2/ inside the squashfs.

synvirt-daemon --dump-openapi writes the OpenAPI 3.1 spec to stdout and exits — used by npm run generate:api:from-file.


  1. Investigate WITH Tony — ask focused questions when uncertain, don’t dump long explanations.
  2. Solutions ready-to-paste, complete, not half-done.
  3. No preamble or intros — go straight to the point.
  4. Standard VGA / text video output for installers (no serial-console hacks).
  5. English-native for all SynVirt product surfaces; Spanish only when the task explicitly names i18n.
  6. No third-party product names (VMware/ESXi/Proxmox/Hyper-V/Nutanix) in code, comments, or commits.
  7. Autonomous execution on VirtSYN — run full master prompts without mid-phase confirmation.
  8. rm on cache paths is routine.
  9. No hardcoded restrictions in installer — soft warnings only, blockers reserved for what literally breaks install.
  10. Atomic commits between phases. Ignore Windows CRLF warnings.
  11. once_cell::sync::OnceCell<Arc<Mutex<Option<T>>>> for global state of long-lived singletons.
  12. When rewriting external-product references from skill contexts into SynVirt code, use technical terms: “post-install Direct Console”, “text-mode admin TUI”, “canonical 20-screen admin console”, “KVM-based hypervisor console”.

Path Purpose
/opt/synvirt/bin/synvirt-daemon Control-plane HTTP API binary
/opt/synvirt/bin/synvirt-network Data-plane daemon (R1: OVS, vSwitches, firewall, boot reapply)
/opt/synvirt/bin/synvirt-console Post-install TUI on tty1
/opt/synvirt/bin/synvirt-boot-splash Boot progress UI on tty1
/opt/synvirt/bin/synvirt-cli Operator CLI binary
/opt/synvirt/bin/synvirt-* (daemons) Host daemon binaries are staged into the ISO automatically from each synvirt-*.service unit’s ExecStart (iso-builder/build.sh); a binary ships only when its unit exists, so a new sub-daemon self-stages the moment its unit lands. Today this covers the split data planes synvirt-storaged, synvirt-migratord, synvirt-updaterd, synvirt-livemotiond, synvirt-backupd in addition to the four above. The host node agent (synvirt-node-agent) is a placeholder with no unit and is not shipped to the host yet. The guest agent (synvirt-agent) runs inside guests — delivered via .deb/.rpm and the guest-tools ISO — and is not staged to the host bin dir.
/opt/synvirt/web-ux/ Legacy dashboard static assets
/opt/synvirt/web-ux-v2/ v2 dashboard static assets (TS bundle)
/opt/synvirt/isos/guest-tools.iso Pre-built guest-tools ISO
/etc/synvirt/release Running version string
/etc/synvirt/configured Flag: wizard finalized
/etc/synvirt/setup.completed Flag: first-run wizard done
/etc/synvirt/deployment Deployment identifier
/etc/synvirt/mode Cluster mode (standalone / cluster-member)
/etc/synvirt/storage-mode ZFS layout / pool config
/etc/synvirt/synvirt.toml Console policy config (TOML)
/etc/synvirt/network.yaml synvirt-network source of truth (vSwitches + firewall + management) — editable from console with vi
/etc/synvirt/logging.yaml synvirt-logging source of truth: runtime log levels + remote-syslog targets + rotation/retention (egress off until a target is enabled)
/etc/synvirt/tls/cert.pem + key.pem TLS material for :443 / :8443 (managed by synvirt-certs after first boot)
/etc/synvirt/secrets.key 32-byte AES key (auto-generated by synvirt-bmc::EncryptionKey::load); reused by synvirt-certs for DNS-01 provider creds
/etc/synvirt/virtswitch-seed.json Installer-written vSwitch JSON consumed by synvirt-network first-boot migration; unlinked after import
/var/lib/synvirt/ Per-feature state files
/var/lib/synvirt/iso-detection.db/ sled: ISO detection cache
/var/lib/synvirt/policies.db sled: per-VM USB redirection policies + the guest run-command gate
/var/lib/synvirt/identity.db/ sled: synvirt-identity users / roles / sessions / pinned super-admins
/var/lib/synvirt/templates.db/ sled: VM template library records (datasets live at <pool>/templates/<id>/)
/var/lib/synvirt/guest-tools.db sled: per-VM guest agent state
/var/lib/synvirt/activity.db/ sled: persisted activity log
/var/lib/synvirt/migrator/ sled + state for the migrator
/var/lib/synvirt/network.db/ sled: synvirt-network runtime cache (rebuilt from network.yaml on every start)
/var/lib/synvirt/settings-audit.db/ sled: shared audit log for every Settings section. Opened once in main.rs.
/var/lib/synvirt/maintenance.lock Maintenance-mode flag (JSON)
/var/run/synvirt/agent/{uuid}.sock Canonical per-VM guest agent Unix socket (tmpfs); mirrored as /rpool/vms/<slug>/agent.sock symlink
/var/lib/libvirt/swtpm/{uuid} symlink → /rpool/vms/<slug>/vtpm/ (consolidated layout)
/rpool/vms/<slug>/ Per-VM dataset — every persistent artefact lives here: the VM’s RAW disk volumes (ZFS zvols, e.g. disk0/disk1, exposed as block devices via /dev/zvol/…), nvram.fd (OVMF UEFI vars), vtpm/ (swtpm state), domain.xml (read-only mirror; libvirt’s canonical copy still lives at /etc/libvirt/qemu/<name>.xml), agent.sock (symlink to /run). The daemon’s vm_layout::migrate_all migrates legacy installs into this layout on first start.
/var/lib/libvirt/images/<pool>/ VM disk images (RAW; zvol-backed)
/var/lib/libvirt/images/isos/ ISO library at /api/v1/isos
/etc/systemd/system/synvirt-*.service systemd unit files (synvirt-daemon.service, synvirt-network.service, synvirt-cert-renew.timer, …)
/etc/pam.d/synvirt-daemon PAM service for Basic Auth
/etc/pam.d/synvirt-console PAM service for console TUI auth
/etc/netplan/01-synvirt.yaml Legacy — no longer written by SynVirt. On boot, synvirt-network detects any leftover copy that references SynVirt-managed names, backs it up under /var/lib/synvirt/network-migration/<ts>/, and renames the source with a .disabled.synvirt-migrated.<ts> suffix so neither netplan generate nor systemd-networkd picks it up again. See §5.9.
/etc/sysctl.d/99-synvirt-uplinks.conf SynVirt-owned sysctl drop-in. Regenerated by synvirt-network on every start from network.yaml; pins disable_ipv6=1, accept_ra=0, autoconf=0 on every uplink kernel name.
/etc/systemd/resolved.conf.d/99-synvirt.conf SynVirt-owned systemd-resolved drop-in. Regenerated by synvirt-network on every start; carries the default vSwitch’s ipmgmt.dns list.
/var/lib/synvirt/network-migration/<ts>/ Backup directory for the legacy netplan / systemd-networkd files synvirt-network disabled on first start after the migration. Includes a MANIFEST.txt and a snapshot/ of network.yaml + ip addr + ip route + ovs-vsctl show + resolvectl status at the time of remediation.
/etc/chrony/chrony.conf NTP config edited by console TUI

On the dev box (dev.synvirt.mx, Debian 13)

Section titled “On the dev box (dev.synvirt.mx, Debian 13)”
Path Purpose
/home/synvirt/dev/ Repo root
/home/synvirt/dev/CLAUDE.md This file
/home/synvirt/dev/CHANGELOG.md Canonical release history
/home/synvirt/dev/TODO.md Short-lived workspace TODOs
/home/synvirt/dev/docs/ Canonical engineering docs
/home/synvirt/dev/docs/modules/ Per-module deep-dive docs
/home/synvirt/dev/crates/ All workspace crates
/home/synvirt/dev/iso-builder/ live-build pipeline
/home/synvirt/dev/iso-builder/VERSION Shipping ISO version
/home/synvirt/dev/iso-builder/output/*.iso Built ISOs
/home/synvirt/dev/iso-builder/live-build/cache/ Bootstrap + chroot cache
/home/synvirt/dev/target/release/ Compiled Rust binaries
/home/synvirt/dev/crates/web-ux-v2/dist/ Built v2 dashboard bundle
/home/synvirt/dev/crates/web-ux-v2/openapi.snapshot.json Frozen OpenAPI for offline generate:api

When editing this file, keep sections 1-10 numbered and top-level. Add new subsections under the appropriate section instead of appending at the end. Bump the “Last updated” line below on every change.

Question File
“What is SynVirt? What conventions apply?” This file (CLAUDE.md)
“What shipped in 0.x.y?” CHANGELOG.md
“What’s the open work today?” TODO.md + per-module ROADMAP.md
“How does module X work in detail?” docs/modules/<X>/
“Architecture of subsystem X?” docs/architecture/X.md
“How do I deploy / migrate / recover X?” docs/operations/<X>.md
“Why was decision Y made? What invariants did it produce?” MEMORY.md (deploy postmortems + cross-phase rules)
“Backend / frontend conventions?” docs/BACKEND_CONVENTIONS.md / docs/FRONTEND_PRINCIPLES.md

Modules with full docs/modules/<X>/ trees today:

  • iso-detection — pipeline, SSE, detectors
  • libvirt-renderer — element rendering, snapshots, virsh validation
  • migrator — adapters, importers, sinks, REST surface
  • vm-profiles — catalog, overlays, hierarchical browse, validation
  • vm-hardware-editor — hardware-editor backend follow-up notes
  • web-ux-v2 — architecture, API client, i18n, components, modules, roadmap
  • webrtc-console — server-side WebRTC pipeline, codec, E2E budget on synvirt-01
  • guest-tools — host supervisor, wire protocol v1, modern + legacy agents, .NET WinForms installer, modern + legacy ISO bundles
  • performance — VM XML defaults, universal renderer knobs (SMT clamp, ioapic kvm, pmu off, tsc native, vmcoreinfo), host kernel cmdline + tuned, baseline benchmark on synvirt-01
  • synvirt-network — R1 data-plane service, YAML source of truth, OVS reconcile rules, D-Bus interface, boot reapply guard
  • db-consolidation — sled handle ownership rule + observability recon
  • backup — SynBaaS: repositories (S3/NFS/peer-pending), jobs + wizard, run engine (manifest-last, anchors, guardrail), GFS chain-aware retention, restore-as-a-new-VM (D5)
  • synvirt-logging — LogSource registry, viewer/follow/bundle, runtime levels, RFC 5424 forwarder, redaction, egress-off-by-default
  • synvirt-identity — permission catalog, report-only authz guard, pinned super-admin break-glass, users/roles/sessions/hardening

Operations runbooks (deploy + recovery procedures):

  • refactor-r1-network — 6-step migration of an existing host onto synvirt-network.service

Modules without dedicated docs (rustdoc-only today):

  • synvirt-core, synvirt-cli, synvirt-node-agent, console-ticket, console-gateway, daemon api/ handlers, console TUI, boot-splash, web-installer, web-ux (legacy), synvirt-libvirt, daemon::activity, synvirt-time, synvirt-resources, synvirt-services, synvirt-firewall, synvirt-swap, synvirt-packages, synvirt-bmc, synvirt-certs, synvirt-update, synvirt-iso.

When a session targets one of these and the rustdoc isn’t enough, write a docs/modules/<name>/README.md first and commit it before the feature work.

When a new minor ships, the previous-but-one minor’s “Current state” detail (§3) moves out of CLAUDE.md and into CHANGELOG.md. CLAUDE.md §3 only carries the currently shipping version + the in-flight one. Anything older lives in CHANGELOG.md.

This is what keeps the file lean. Do not append release notes to this file.

Last updated: 2026-07-13 — dev appliance 10.10.26.65 RETIRED (host no longer exists; operator-confirmed). §7 “Dev appliance” section replaced by “Test fleet”: e2e targets are 10.10.26.11/.12/.13 (synnet-cluster, VIP .100), fleet deploys operator-gated via scripts/night-deploy-fleet.sh, pre-deploy evidence on hermetic local rigs. Historical .65 mentions in docs/ stay as history; unit-test fixture CIDRs 10.10.26.65/24 in synvirt-network are unrelated example addresses.

Last updated: 2026-07-01 — storage-plane ownership doctrine amended (backup-P3 D1 signature): §2 synvirt-storaged row now scopes it to the storage-object lifecycle control plane; streaming zfs send/recv executes in the owning daemon. Canonical wording in docs/BACKEND_CONVENTIONS.md §External command execution; spec at docs/superpowers/specs/backup-p3-engine.md.

Last updated: 2026-06-28 — Tasks 3-7 operator feature drop + fleet deploy (0.15.0+build.1243). §2 crate count 48→50 (new synvirt-identity + synvirt-logging under a new “Identity, access control & logging” group); the daemon submodule table gains templates/vm_resources/authz/security/audit/csv_export/ logs/logs_forward/vm_tags plus the run-command gate on policy.rs. §3 records the drop and retires the now-shipped open items (disk + NIC lifecycle, session-cookie auth). New §5.15 (synvirt-logging) + §5.16 (synvirt-identity, incl. the pinned super-admin break-glass). §5.5 route list refreshed. §9 adds identity.db, templates.db, logging.yaml. §10 index adds both new module docs. Older verbose edit notes condensed below.

2026-05-27 (network.yaml as canonical source) — landed the installer + console TUI migration off netplan. §2 install-component bullet rewritten: daemon/src/api/install.rs writes only /etc/synvirt/network.yaml in the target chroot now; no more netplan write, no netplan generate chroot call. §5.9 “Ownership reality” note replaced by an “Ownership invariants” block — network.yaml is the single source of truth, synvirt-network the single applicator, physical uplinks are always L2-only, host L3 only on KernelPorts, DHCP/static management IP only on ipMGMT. §5.9 boot order updated to drop systemd-networkd.service from the SynVirt critical path (the unit stays enabled to keep network-online.target firing, but owns nothing SynVirt cares about). §5.9 boot binary flow expanded to 8 steps: load+validate YAML → legacy netplan/networkd remediation → vmnic reconcile → OVS boot reapply → vmkernel direct_apply → uplink L2-only enforcement → DNS drop-in → post-apply invariant verification. §9 path-table entry for /etc/netplan/01-synvirt.yaml rewritten as “legacy — auto-remediated on boot”; new entries added for the SynVirt-owned sysctl drop-in (/etc/sysctl.d/99-synvirt-uplinks.conf), the systemd-resolved drop-in (/etc/systemd/resolved.conf.d/99-synvirt.conf), and the legacy-migration backup directory (/var/lib/synvirt/network-migration/<ts>/). New synvirt-network modules: uplink_l2, dns, invariants, legacy_migration. Console TUI now reads/writes /etc/synvirt/network.yaml via synvirt-network::yaml_store and calls systemctl reload-or-restart synvirt-network instead of netplan apply. Diagnostics bundle expanded with the new artifacts (network.yaml + last-good + backup listing, synvirt-network status + journal, ovs-vsctl show, ip -d addr, ip route, resolvectl status, IPv6 uplink sysctls, the legacy netplan slot for stale-config detection). Legacy crates/web-installer/wizard.html lost its netplanRaw override textarea.

Last updated: 2026-05-27 — conservative rewrite pass against the post-P1+P2 codebase. §1 console screen count corrected to 22 (was 20; new screens since: lock, full debug-server triad). §2 crate count bumped 39 → 40 (web-installer-v2 now a registered workspace member). §2 daemon submodule table adds host_tunings.rs, perf_reapply.rs, storage_bootstrap.rs, telemetry/, update_v2/, vm_layout.rs (verified by ls crates/daemon/src/); the previously listed network_client.rs and boot_reapply.rs no longer exist on disk — flagged as TODO. §2 versioning paragraph rewritten: every crate now inherits version.workspace = true from the workspace placeholder "0.0.0-dev", and runtime version display reads env!("SYNVIRT_RELEASE") populated by per-crate build.rs from iso-builder/VERSION (P1 unification, June 2026). §2 ARCHITECTURE.md reference corrected to the lowercase architecture.md file actually on disk. §5.6 console debug server port corrected 9443 → 8443 (the real default in crates/console/src/debug/config.rs). §6 cites docs/architecture/host-roles.md which is missing — TODO inserted, content preserved as intent. §7 dev appliance IP corrected 10.10.26.51 → 10.10.26.65 (auto-memory reference_synvirt_01); also flags the second host that reports the same hostname at .52 (baremetal, referenced by docs/modules/webrtc-console/ and db-consolidation). §10 module deep-dive index adds entries for vm-hardware-editor, webrtc-console, and db-consolidation which have dedicated docs/modules/<X>/ trees today but were not listed.

2026-05-27 (network-narrative fix) — corrected the “single owner = synvirt-network” narrative that R1 left behind. The runtime path is netplan-free (R1), but the install path (daemon/src/api/install.rs writing /etc/netplan/01-synvirt.yaml + netplan generate in the target chroot) and the post-install console TUI (crates/console/src/system/network.rs::NETPLAN_PATH = /etc/netplan/01-synvirt.yaml) still own netplan directly. §2 “crates/installer excluded” block now names the three live install components (crates/live, crates/web-installer-v2, daemon/src/api/install.rs) and flags crates/installer/ as dead legacy. §5.9 gains an “Ownership reality (not yet single-owner)” note. §5.9 boot order adds systemd-networkd.service between network-pre.target and openvswitch-switch.service and explains why. §9 path-table entry for /etc/netplan/01-synvirt.yaml now lists both writers and cross-references §5.9. Caught during a network-bug audit (reboot hang + ICMPv6 NA storm + L3 on the physical NIC instead of the OVS bridge internal port).

Older edit history (pre-2026-05-27): condensed for leanness — see git log -- CLAUDE.md for the full per-edit narrative (crate-catalog growth 20 → 31 → 39 → 40, the R1/R2 service extractions, the SYNVirtDEV → dev.synvirt.mx dev-box rename, and the network.yaml-as-single-source-of-truth migration).