Reference · Architecture

Architecture

Every module, every actor boundary, every singleton, every notification — the map a contributor needs in their head before changing anything.

Layered overview

┌────────────────────────────────────────────────────────────────┐
│  UI layer (AppKit, NSWindowController-based, @MainActor)        │
│   - VMLibraryWindowController       (main window, ~2600 LOC)    │
│   - PacketAnalysisWindowController  (filter bar, table, hex)    │
│   - VM display windows              (one per running guest)     │
└────────────────────────┬───────────────────────────────────────┘
                         │  Notifications + Combine subscriptions
                         ▼
┌────────────────────────────────────────────────────────────────┐
│  Domain layer (singletons, mostly @MainActor)                   │
│   - VMManager.shared                                            │
│   - VirtualNetworkSwitch.shared                                 │
│   - PacketCaptureManager.shared                                 │
│   - ISOCacheManager.shared                                      │
│   - MacOSVMInstaller.shared                                     │
│   - VMSecurityMonitor.shared                                    │
└────────────────────────┬───────────────────────────────────────┘
                         │  Reads/writes on background queues
                         ▼
┌────────────────────────────────────────────────────────────────┐
│  Persistence + system layer                                     │
│   - ~/.avf/ bundle filesystem (FileManager)                     │
│   - Apple Virtualization framework (VZ*)                        │
│   - URLSession (ISO/IPSW download)                              │
│   - tshark subprocess (Process + Pipe + FIFO)                   │
│   - Endpoint Security Framework (Network Extension target)      │
└────────────────────────────────────────────────────────────────┘

Three layers, well-defined responsibilities. The UI never talks to URLSession directly; the domain layer never owns NSView. Notifications and Combine are the glue.

Module map

Core

  • AppDelegate.swift — application lifecycle, menu bar, NotificationCenter handlers, multi-window VM management. Owns the menu wiring and routes user actions to the domain layer.
  • VMManager.swift — VM CRUD, bundle scanning, async initialization on background threads, transitions to VZVirtualMachine. The factory and the lifecycle authority.
  • VMConfiguration.swift — Codable model. The source of truth for metadata.json. Version-bump-safe.

Network stack

  • VirtualNetworkSwitch.swift — L2/L3 switch in Swift. Holds the MAC table, runs the forwarding loop, publishes frames.
  • PacketCaptureManager.swift — owns the tshark subprocess, the FIFO, the JSON-EK parser. Publishes DecodedFrame via Combine.
  • PacketAnalysisWindowController.swift — the Wireshark-style UI. Subscribes to PacketCaptureManager, drives the table, hex view, decoded-layers tree.

UI

  • VMLibraryWindowController.swift — main window. Table view, packet log panel, Active VMs sidebar. By far the biggest file (~2600 LOC) because it's the central hub.

AI sandbox

  • AISandboxMacVMConfiguration.swift — builds the VZVirtualMachineConfiguration for the ephemeral macOS guest, including the vsock device on port 2222 and the VirtioFS workspace share.
  • AISandboxVMSession.swift — host-side session API: cloneBase(), boot(), run(), destroy().
  • AISandboxMacVMInstaller.swift — provisions the base bundle (IPSW download → restore → setup).

Security

  • VMSecurityMonitor.swift — FSEvents-based observer. Filesystem deltas, resource pressure, severity-graded events.
  • SecVFError.swift — typed error enum (11 categories) with LocalizedError conformance and recovery suggestions. Mirrors to error-audit.log.

Supporting

  • ISOCacheManager.swift — ~1000 LOC. ISO download, SHA-256 verification, cache scanning. The provenance enforcer.
  • DistroVersionFetcher.swift — discovers live distro versions from mirrors. Distro metadata in Resources/distros.json.
  • MacOSVMInstaller.swift — IPSW download from Apple CDN with security validation.
  • ScriptsUSBManager.swift — builds and attaches the Scripts USB image for guest scripts (router setup, FakeNet, etc.).

Protocols (DI seams)

  • VMManagerProtocol.swift
  • NetworkSwitchProtocol.swift
  • PacketCaptureProtocol.swift

Each abstracts a singleton so tests can substitute a mock. Production code holds any VMManagerProtocol at the boundary.

Threading & actor model

Three execution domains:

DomainWhere it runsWhat's on it
Main actor@MainActor Swift ConcurrencyEvery UI class, every window controller, the singletons (their public API is @MainActor).
Capture queueDispatchQueue(label: "secvf.capture", qos: .userInitiated)Frame forwarding, JSON-EK parsing. Never blocks the main thread.
Async tasksImplicit Task contextsVZ boot, IPSW restore, ISO download. await-resumed on the calling actor.

The rules:

  1. All UI work is on the main actor. There is no exception.
  2. All publicly mutating domain methods are on the main actor. Internal mutation (e.g. the MAC table update inside the switch) happens on the switch's serial queue and is re-published on main.
  3. Long-running work — VZ boot, IPSW download — is an async function that suspends. The UI shows a progress indicator; the framework's callback completes on the main actor.
  4. The capture pipeline is the one true "background" path. tshark output is parsed off-main and Combine-published to UI subscribers, which run on main by default.
Why singletons + @MainActor? SecVF is a desktop app with a single UI process. Domain state is intrinsically singleton — there is one VM library, one switch, one capture pipeline. @MainActor guarantees serial access to mutable shared state without locks; the queue is the framework's main run loop.

NotificationCenter contract

The cross-cutting glue. Most of the UI subscribes to a small set of named notifications:

NotificationPosted byuserInfo
Notification.Name.startVMUI (menu, button) or AppDelegate["name": String]
Notification.Name.stopVMUI["name": String, "force": Bool]
Notification.Name.pauseVMUI["name": String]
Notification.Name.vmStateChangedVMManager["name": String, "state": VMState]
Notification.Name.packetCapturedPacketCaptureManager["frame": DecodedFrame]
Notification.Name.securityEventVMSecurityMonitor["event": SecurityEvent]

Why NotificationCenter instead of Combine for these? Three reasons:

  1. The CLI (via DistributedNotificationCenter) posts the same notifications. NSDistributedNotification is the only cross-process channel that doesn't require XPC plumbing — and we want minimal IPC surface for security.
  2. Multiple subscribers, no backpressure semantics needed (it's UI events, not high-rate data).
  3. Easy to bridge to AppKit's existing notification idioms (NSWindow events, etc.).

Combine pipelines

Combine is used where backpressure-aware streaming matters:

Packet pipeline

VirtualNetworkSwitch.frames
    : PassthroughSubject<EthernetFrame, Never>
        │
        ├──▶ PacketCaptureManager      (writes to FIFO)
        │       │
        │       ▼
        │   tshark subprocess
        │       │
        │       ▼
        │   PacketCaptureManager.decodedFrames
        │       : PassthroughSubject<DecodedFrame, Never>
        │
        └──▶ VMSecurityMonitor         (looks for suspicious patterns)

Switch statistics

VirtualNetworkSwitch.stats
    : CurrentValueSubject<SwitchStats, Never>
        │
        ├──▶ Switch Statistics window  (subscribes, renders)
        ├──▶ Status bar item            (live counter in menu bar)
        └──▶ VMSecurityMonitor          (drop-rate alerts)

Why CurrentValueSubject for stats but PassthroughSubject for frames?

Stats need a "current" — a new subscriber should see the latest counters immediately. Frames are events; if you subscribe mid-flight, you missed earlier frames and that's correct.

Singletons & their boundaries

Five domain singletons. Their boundaries are deliberate:

SingletonOwnsTalks to
VMManager.sharedVM CRUD, bundle filesystem, VZVirtualMachine instancesThe framework, persistence, posts notifications
VirtualNetworkSwitch.sharedPort assignments, MAC table, forwarding loopVZNetworkDevice attachments, frame publisher
PacketCaptureManager.sharedtshark subprocess, FIFO, decoded framesVirtualNetworkSwitch (subscribes to frames)
ISOCacheManager.sharedISO downloads, SHA-256, cache directoryURLSession, FileManager, audit log
VMSecurityMonitor.sharedFSEvents, resource pressure, severity classificationAudit log writer, optional notification UI

Cross-singleton calls are deliberate and few — chiefly PacketCaptureManager subscribing to VirtualNetworkSwitch.frames, and VMSecurityMonitor watching both. Anything else goes through notifications.

Protocols & dependency injection

Each singleton has a matching protocol. Production code holds any VMManagerProtocol at the boundary, not VMManager directly:

// production
final class VMLibraryWindowController: NSWindowController {
    private let vmManager: any VMManagerProtocol

    init(vmManager: any VMManagerProtocol = VMManager.shared) {
        self.vmManager = vmManager
        super.init(window: nil)
    }
}

// tests
let mockVMManager = MockVMManager()
let controller = VMLibraryWindowController(vmManager: mockVMManager)
// drive the controller against the mock

This makes the entire UI testable in isolation. TestHelpers.swift provides factory functions for mocks.

Data flow: a capture session

Walk one frame from wire to UI to demonstrate the whole stack:

  1. Guest sends a packet. The guest's virtio-net driver writes a frame to its TX ring.
  2. VZ delivers it to VirtualNetworkSwitch's port for that VM, off-main, on the switch's serial queue.
  3. Switch forwards. MAC lookup → destination port → write to that port's RX queue. Stats counters bump.
  4. Switch publishes. The frames PassthroughSubject emits an EthernetFrame with timestamp and port metadata.
  5. PacketCaptureManager subscribes. It packs the frame into a libpcap-format record and writes to the FIFO. tshark reads it.
  6. tshark emits JSON-EK. PacketCaptureManager's stdout reader splits on newlines, parses each line into a DecodedFrame.
  7. Decoded frame publishes. The decodedFrames subject emits.
  8. UI subscribers receive. The packet table view appends a row (on main); the protocol stats widget bumps its counter; the hex panel (if selected) renders.
  9. VMSecurityMonitor inspects. If the frame matches a suspicious pattern (e.g. SYN scan, DNS exfil), it raises a WARNING event.

Total path from wire to UI: ~1–3 ms in the common case. The throttle is tshark's parse speed, not Swift's.

Test architecture

Tests live in SecVF/Tests/, organized by component:

FileCovers
TestHelpers.swiftMock VM bundle creation, async assertion helpers, XCTest extensions.
VMConfigurationTests.swiftCodable encode/decode, schema versioning.
VMManagerTests.swiftCRUD lifecycle, transitions, bundle scanning. Uses the VMManagerProtocol seam.
VirtualNetworkSwitchTests.swiftMAC learning, broadcast, drop policy, stats counters.
ISOCacheManagerTests.swiftProvenance enforcement, SHA-256 mismatch handling, hostname allowlist.
VMLibraryUITests.swiftUI smoke tests through the protocol mocks.
IntegrationTests.swiftEnd-to-end paths (skipped in CI; require physical Apple Silicon).

Convention: Given-When-Then. Each test name reads as a sentence. test_givenStoppedVM_whenStarted_thenStateBecomesRunning() — that style.

Want to contribute? The fastest way to learn the code is to add a test. Pick an uncovered method, write a GWT test, send a PR. See Contributing.