CLAUDE.md
Synced read-only from
/home/synnet/mirrors/synvirt-product/CLAUDE.md. Edit at the source, not here.
CLAUDE.md — SynVirt project context
Section titled “CLAUDE.md — SynVirt project context”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.
The Mission
Section titled “The Mission”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 matters —
axumfor 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.
1. Product overview
Section titled “1. Product overview”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; noapt-get installduring install). - Post-install UX:
- Browser dashboard — two surfaces coexist; v2 is canonical:
web-ux-v2modern (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-uxlegacy (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, formerlydcui) — 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)
- Browser dashboard — two surfaces coexist; v2 is canonical:
- Cluster / datacenter manager (future 1.0+): VirtDCM control plane
Owner: Tony Castrejón — SYNNET (Culiacán, MX).
Native language
Section titled “Native language”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.
Third-party products — do not name
Section titled “Third-party products — do not name”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”).
2. Repository layout
Section titled “2. Repository layout”/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/*.isobrand/ 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/.
Active workspace crates (50)
Section titled “Active workspace crates (50)”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 PoolId ↔ PoolLabel 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.completedis 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.yamlin the target chroot (the canonical source of truth), rsync the live rootfs to disk, and finalise.synvirt-network.servicethen applies that YAML on first boot (OVS bridge + ipMGMT L3 viadirect_apply); no netplan write, nonetplan generatechroot call. This is where the post-install network config actually lands today (not incrates/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.
Daemon (crates/daemon) submodules
Section titled “Daemon (crates/daemon) submodules”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).
3. Current state
Section titled “3. Current state”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-networkextraction (commits38dd6bathroughdfbcc30, pluse9c711dfollow-up). OVS, vSwitches, firewall apply, and the boot reapply loop now live in their own systemd service.systemctl restart synvirt-daemonno longer touches OVS state — that’s the structural fix from the 2026-05-05 incident. R1 landed the daemon as a D-Bus client ofsynvirt-network; Commit 6 later collapsed that indirection back in-process (the daemon now calls thesynvirt-networkNetworkControllerdirectly) and removed thelegacy-network-directfeature. 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 throughRustlsConfigreload trait. Auto-renew viasynvirt-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 newsynvirt-loggingsubsystem (§5.15); and the newsynvirt-identityRBAC subsystem (§5.16). Two safety flags ship OFF by default: RBAC authz enforcement (report-only) and remote syslog egress. SeeMEMORY.mdfor 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-loggingsub-daemon log-level fan-out oversynvirt-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.)
4. Engineering conventions (canonical)
Section titled “4. Engineering conventions (canonical)”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.
4.1 Backend (Rust)
Section titled “4.1 Backend (Rust)”- One
Errorenum per crate viathiserror, with machine-readable discriminant codes (e.g.,code = "E_VM_NOT_FOUND"). - Public APIs return
Result<T, crate::Error>.anyhow::Resultonly inmainand tests. - Public async fns annotated with
#[tracing::instrument(skip_all)]. Usedebug!/info!/warn!/error!— neverprintln!. - Naming verbs carry meaning:
get_*fails if absent,find_*returnsOption,list_*returnsVec(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 auditmust pass.- Rustdoc on every
pubitem. First line = complete sentence with period. - Conventional Commits with crate scope:
feat(synvirt-libvirt): .... - Subprocess calls:
tokio::process::Commandwith timeout; capture stdout and stderr separately; never build shell strings. - Every
#[utoipa::path]handler MUST set an explicitoperation_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).
4.2 Frontend — both legacy and v2
Section titled “4.2 Frontend — both legacy and v2”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 theApiError/ERROR_MAPsurface. - 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
#0566C1brand (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.
4.3 Frontend — web-ux legacy specifics
Section titled “4.3 Frontend — web-ux legacy specifics”- All strings in
crates/web-ux/assets/strings/en_us.json, called viaSynvirt.t(key, vars). - Vendored Vue 3 + Pinia + Tailwind in
crates/web-ux/vendor/, fonts (Inter + JetBrains Mono) incrates/web-ux/vendor/inter/andvendor/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.
4.4 Frontend — web-ux-v2 specifics
Section titled “4.4 Frontend — web-ux-v2 specifics”- TypeScript 5.6 strict +
noUncheckedIndexedAccess+verbatimModuleSyntax. Noany. Nounwrap-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-typescript→src/api/generated/schemas.ts. Regenerate withnpm run generate:api:from-fileagainst the committedopenapi.snapshot.json. - Typed openapi-fetch client;
ApiError+ERROR_MAPcurate every backendE_*code into operator-facing English copy. - SSE consumption via
subscribeIsoDetection/ per-feature composables; auto-cleanup inonUnmounted. - Vitest 2 + jsdom unit tests. Coverage target ≥ 75% on
components/,stores/,composables/. Polyfills for ResizeObserver / IntersectionObserver / scrollIntoView insrc/vitest.setup.tsfor Headless UI under jsdom. - Brand color
#0566C1(token--brand). Dark theme + density variants live insrc/styles/tokens.css.
4.5 SynVirt-specific rules
Section titled “4.5 SynVirt-specific rules”- Host-level abstractions go in their own crate when the surface
outgrows a daemon submodule (
synvirt-libvirt,synvirt-guest-*,migrator). Daemon-internal modules stay undercrates/daemon/src/<name>/with their own README indocs/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/.
5. Architecture highlights
Section titled “5. Architecture highlights”This section is a quick map. Each module’s deep-dive lives in
docs/modules/<name>/ (see §10 for the index).
5.1 synvirt-core
Section titled “5.1 synvirt-core”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.
5.2 synvirt-libvirt
Section titled “5.2 synvirt-libvirt”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.
5.3 synvirt-daemon (control plane)
Section titled “5.3 synvirt-daemon (control plane)”axum HTTP API on TLS :443 (or :8443 in dev). PAM Basic Auth middleware. Mounts:
/api/v1/*REST surface, all utoipa-annotated with uniqueoperation_ids./api/v1/openapi.json+/docs/Swagger UI./api/v1/eventsSSE for daemon events./api/v1/iso-detection/eventsSSE for detection lifecycle./static-served TS bundle (dist ofweb-ux-v2) — canonical./legacy/*legacyweb-uxstatic 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.
5.4 Module ring around the wizard
Section titled “5.4 Module ring around the wizard”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— turnsResolvedProfile + VmContextinto the libvirt<domain>XML. 14 element renderers. 6 insta snapshots + virsh-validate integration test gate the output.
5.5 web-ux-v2
Section titled “5.5 web-ux-v2”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.
5.6 Console TUI + boot splash
Section titled “5.6 Console TUI + boot splash”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.
5.7 Console module (browser VNC)
Section titled “5.7 Console module (browser VNC)”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.
5.8 Guest Tools Suite
Section titled “5.8 Guest Tools Suite”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-backedVmToolsState).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 iniso-builder/vendor/guest-payloads/MANIFEST.toml, staged byiso-builder/scripts/prepare-guest-payloads.sh, verified and copied intodotnet/Embedded/bybuild.rsat compile time. A missing source or hash mismatch is a hard build failure — no stubs. Seedocs/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.
5.9 synvirt-network (data plane, R1)
Section titled “5.9 synvirt-network (data plane, R1)”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:
- Bridge in YAML, missing in OVS → create.
- Bridge in OVS, missing in YAML → log warning, surface drift, never auto-delete.
- Empty YAML + non-trivial OVS on initial start → refuse to
start (
E_NETWORK_RECONCILE_REFUSE); operator decides. - 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.
5.10 Activity log
Section titled “5.10 Activity log”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.
5.11 migrator
Section titled “5.11 migrator”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/.
5.12 synvirt-certs (Phase 9)
Section titled “5.12 synvirt-certs (Phase 9)”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.
5.13 Settings module — service crates
Section titled “5.13 Settings module — service crates”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 inmain.rsand passed to each service’s constructor; neversled::open()it twice (the post-R1.2 BMC bug — seeMEMORY.md).synvirt_time::exec::Exec— subprocess abstraction reused bysynvirt-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.
5.14 Refactor roadmap (R1 → R4)
Section titled “5.14 Refactor roadmap (R1 → R4)”Multi-phase split of the monolithic daemon into separate systemd services so a control-plane restart never touches the data plane:
| Phase | Status | Scope |
|---|---|---|
R1 — synvirt-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.) |
R2 — synvirt-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. |
R3 — synvirt-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.service → synvirt-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/.
6. Host roles (cluster architecture)
Section titled “6. Host roles (cluster architecture)”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.
Typical cluster topologies
Section titled “Typical cluster topologies”- 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.
Adding hosts
Section titled “Adding hosts”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.
7. Infrastructure and tooling
Section titled “7. Infrastructure and tooling”Source control
Section titled “Source control”- Local-only git repository (no remote).
git remote -vis 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 orgit 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). SSHroot. Raft clustersynnet-cluster, VIP10.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-01at10.10.26.52(seedocs/modules/webrtc-console/PIPELINE.md). When a task targets it, the prompt names.52explicitly. - 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.
SynVirtDev Console (dev.synvirt.mx)
Section titled “SynVirtDev Console (dev.synvirt.mx)”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 unitpm2-root.service. App listens on127.0.0.1:3100.server.jsbootstraps TS viarequire('tsx/cjs'). - Reverse proxy: Caddy is a local binary at
/home/synvirt/dev/caddy-proxy/, NOT systemd. Reload withcaddy reload --config .../Caddyfile --adapter caddyfile(admin API onlocalhost:2019). - Persistence: MariaDB (
mariadb-server) + Redis. Databasesynvirtdev_console, usersvconsole. - DNS:
dev.synvirt.mxresolves via/etc/hosts— primary10.10.26.1(LANens192), secondary10.61.26.5(ZeroTierztpp6mxztz, for remote admin only). LAN clients use internal DNS at10.10.10.1. Cloudflare zonesynvirt.mxis the same account assynnet.mx, so Let’s Encrypt DNS-01 reuses the existing CF token at/etc/caddy/cf.env. Legacy hostnamesynvirtdev.synnet.mxis retired as of 2026-05-19. - Claude CLI auth:
/root/.claude/.credentials.json(Max subscription).claude-agent.tsstripsANTHROPIC_API_KEYfrom 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).
Claude Code allowedTools
Section titled “Claude Code allowedTools”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.
Autonomous execution
Section titled “Autonomous execution”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.
Subprocess global state
Section titled “Subprocess global state”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().
Cache and rebuild discipline
Section titled “Cache and rebuild discipline”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
Build pipeline hooks
Section titled “Build pipeline hooks”iso-builder/live-build/config/hooks/normal/9999-synvirt-enable.hook.chrootforcesmulti-user.target.wants/symlinks forsynvirt-live.servicesynvirt-daemon.serviceduring 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 idempotentln -sffor every customsynvirt-*unit. - The install pipeline
rsyncs the live rootfs into the target instead ofdebootstrap+apt-get install, so the install is fully offline. iso-builder/build.shrunsnpm install+npm run buildincrates/web-ux-v2/and stages thedist/under/opt/synvirt/web-ux-v2/inside the squashfs.
Daemon CLI
Section titled “Daemon CLI”synvirt-daemon --dump-openapi writes the OpenAPI 3.1 spec to
stdout and exits — used by npm run generate:api:from-file.
8. Working style rules for Claude
Section titled “8. Working style rules for Claude”- Investigate WITH Tony — ask focused questions when uncertain, don’t dump long explanations.
- Solutions ready-to-paste, complete, not half-done.
- No preamble or intros — go straight to the point.
- Standard VGA / text video output for installers (no serial-console hacks).
- English-native for all SynVirt product surfaces; Spanish only when the task explicitly names i18n.
- No third-party product names (VMware/ESXi/Proxmox/Hyper-V/Nutanix) in code, comments, or commits.
- Autonomous execution on VirtSYN — run full master prompts without mid-phase confirmation.
rmon cache paths is routine.- No hardcoded restrictions in installer — soft warnings only, blockers reserved for what literally breaks install.
- Atomic commits between phases. Ignore Windows CRLF warnings.
once_cell::sync::OnceCell<Arc<Mutex<Option<T>>>>for global state of long-lived singletons.- 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”.
9. File paths cheat sheet
Section titled “9. File paths cheat sheet”On the installed appliance
Section titled “On the installed appliance”| 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 |
10. End-of-doc signal
Section titled “10. End-of-doc signal”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.
Where each kind of context lives
Section titled “Where each kind of context lives”| 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 |
Module deep-dive index
Section titled “Module deep-dive index”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 ontosynvirt-network.service
Modules without dedicated docs (rustdoc-only today):
synvirt-core,synvirt-cli,synvirt-node-agent,console-ticket,console-gateway,daemonapi/handlers,consoleTUI,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.
Canonical-facts pruning rule
Section titled “Canonical-facts pruning rule”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).