Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
6461619
feat(hv): dory-hv VMM boots the Apple kernel to console
Augani Jul 4, 2026
976806f
feat(hv): virtio-mmio transport, virtio-blk, virtio-rng; dockerd up i…
Augani Jul 4, 2026
9ac1a8c
feat(hv): elastic memory works: free page reporting + stage-2 unmap r…
Augani Jul 4, 2026
085e38d
feat(hv): networking, engine mode, and app integration behind DORY_HV…
Augani Jul 4, 2026
5a18ad7
feat(engine): default the VZ helper to a 2GiB ceiling with tunable he…
Augani Jul 4, 2026
ddd3d7a
feat(hv): SMP via PSCI CPU_ON, one thread per vCPU
Augani Jul 4, 2026
fd874bd
fix(hv): make free-page reclaim SMP-safe; close RX queue race
Augani Jul 4, 2026
7879d9a
feat(hv): crash-safe disk model + graceful shutdown + trim v2
Augani Jul 4, 2026
8d6b8a2
fix(hv): harden engine lifecycle (5 review findings)
Augani Jul 4, 2026
0f510e8
docs(hv): record the OrbStack win + SMP/crash-safety in the architecture
Augani Jul 4, 2026
90b25a0
fix(hv): stop reclaim thrash from guest trim loop
Augani Jul 4, 2026
f702705
docs(hv): honest signed-app numbers + reclaim tuning notes
Augani Jul 4, 2026
589d698
test(hv): unit coverage for FDT, kernel image, virtqueue, MMIO, recla…
Augani Jul 4, 2026
f35a77b
build(hv): bundle dory-hv + gvproxy into the self-contained app
Augani Jul 4, 2026
a6b8968
perf(hv): cap idle page cache at ~320MiB; document reclaim floor
Augani Jul 4, 2026
fb14c45
feat(hv): recognize dory-hv as a first-class engine in host support
Augani Jul 4, 2026
823955a
refactor: dory-hv is the sole engine; remove Apple container entirely
Augani Jul 4, 2026
373200e
feat(hv): auto-forward published container ports through gvproxy
Augani Jul 4, 2026
62244bd
docs(hv): dory-hv is the sole engine; add release notes + milestone s…
Augani Jul 4, 2026
38b99e3
fix(hv): honest engine-availability reporting + no leaked port forwards
Augani Jul 4, 2026
1fd4a68
feat: bundle kubectl + docker CLIs so nothing extra is needed
Augani Jul 4, 2026
dacead6
feat(hv): offline engine variant loads a bundled rootfs (no first-run…
Augani Jul 4, 2026
88a9459
feat(hv): virtio-vsock, virtio-fs+DAX, usbip, agent channel, direct-I…
Augani Jul 4, 2026
b2b6370
feat: machine recipes+SSH config, USB UI, credential/expose/tun-route…
Augani Jul 4, 2026
48c6ce9
docs+scripts: readiness gates (--dax, --rosetta, usb), recipes, plans…
Augani Jul 4, 2026
d3c1210
fix: wire SSHConfigWriter into machine refresh; drop stray committed …
Augani Jul 4, 2026
76a424b
bench(t1): end-to-end virtio-fs benchmark vs OrbStack; fix kernel con…
Augani Jul 4, 2026
4d7b4b6
docs: mark Track 1.6 benchmarks done in roadmap
Augani Jul 4, 2026
2d33292
perf(virtiofs): fix guest page-cache retention (17x) + FUSE processin…
Augani Jul 4, 2026
8c8f80f
perf(virtiofs): zero-copy READ via preadv into guest descriptor segments
Augani Jul 4, 2026
99cd674
perf(virtiofs): make DAX actually work end-to-end (54x on raw reads, …
Augani Jul 4, 2026
55db661
docs: record DAX file-sharing benchmark (1,074k raw IOPS, beats OrbSt…
Augani Jul 4, 2026
41680c0
fix(virtiofs): honor FATTR_SIZE in SETATTR (truncate/O_TRUNC no longe…
Augani Jul 5, 2026
42df0c3
feat(virtiofs): auto-share $HOME at its real path so -v just works wi…
Augani Jul 5, 2026
d756dda
docs: file-sharing auto-wiring + write-correctness notes
Augani Jul 5, 2026
2f02d19
plan(usb): add Track 3.6 — complete usbip-over-vsock last mile (relea…
Augani Jul 5, 2026
bada921
fix(virtiofs): advertise FUSE_DO_READDIRPLUS so shared dirs enumerate
Augani Jul 5, 2026
a66260f
fix(security): hide credential dirs from the $HOME auto-share (:safe)
Augani Jul 5, 2026
63f5b0b
docs: correct USB passthrough over-claim (empty controller, not devic…
Augani Jul 5, 2026
546db73
fix(migration): let the user pick the import source (fixes 'can't imp…
Augani Jul 5, 2026
a64c758
feat(usb): host UsbipBridge serve loop + vsock EOF signal (Track 3.6b…
Augani Jul 5, 2026
d7e9814
feat(usb): guest agent dials the host + runs the usbip import handsha…
Augani Jul 5, 2026
3139277
feat(usb): UsbipManager (claimed-device registry) + engine listener (…
Augani Jul 5, 2026
4eb8791
plan(usb): mark Track 3.6 data-path subtasks done (3.6b-g,m,n); contr…
Augani Jul 5, 2026
2d146cd
feat(usb): dory-hv usb probe <busid> [mode] — host-claim + control-tr…
Augani Jul 5, 2026
3149776
feat(usb): engine control plane — dory usb attach/detach claim+regist…
Augani Jul 5, 2026
c55ce07
docs: this release is Apple-silicon-only for Dory's own engine; Intel…
Augani Jul 5, 2026
d628c2d
plan(usb): control plane done (3.6h-j); only hardware smoke + entitle…
Augani Jul 5, 2026
86365ad
feat(net): configurable service ports + disable *.dory.local (GitHub #2)
Augani Jul 5, 2026
10fa36f
docs: fix false k8s image claim (#4) + document amd64/qemu segfault l…
Augani Jul 5, 2026
51aa712
feat(vz): make dory-vm memory ceiling env-tunable (DORY_VM_MEM_MB)
Augani Jul 5, 2026
34d613d
feat(engine): opt-in 'Run x86 with Rosetta' engine mode (GH#3 SQL Ser…
Augani Jul 5, 2026
c889b62
harden(virtiofs): hidden names also reject create/mkdir/unlink/rmdir/…
Augani Jul 5, 2026
5dce3c6
ci: retry the whole suite on any non-clean attempt (partial shared-ru…
Augani Jul 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ ExportOptions.plist
# Release pipeline output
release-build/

# Guest build artifacts (kernel Image, compiled agent) — rebuilt via guest/kernel/build.sh + guest/agent/build.sh
guest/out/

# Dory runtime data
.dory/

Expand All @@ -65,3 +68,6 @@ cloud/

# Website build output (source lives in website/; built into docs/ via `npm run build`)
docs-build/

# Go build artifact (build.sh writes the real output to guest/out/)
guest/agent/agent
18 changes: 10 additions & 8 deletions COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ CA trust install remain consent-gated, the same one-time admin grant OrbStack ne
| **Bind-mount file sharing** | ✅ | Home dir shared into the VM (virtiofs); verified `docker run -v ~/proj:/app` reads/writes host files live |
| One-click Kubernetes | ✅ | `KubernetesProvisioner` runs k3s in the shared VM; verified host `kubectl` + pod deploy; GUI "Enable" button |
| Linux machines (Ubuntu/Debian/Fedora/Alpine) | ✅ | `MachineProvider` via `container machine`; verified real machine create/list/start/stop/delete; GUI picker |
| x86/amd64 emulation | ✅ (qemu) | Auto-installs qemu binfmt; verified `--platform linux/amd64 → x86_64`. Rosetta fast-path is a documented gap |
| x86/amd64 emulation | ✅ (qemu, with limits) | Auto-installs qemu binfmt; verified `--platform linux/amd64 → x86_64`. **Heavy amd64 workloads can segfault** under qemu-user on the default `dory-hv` engine — SQL Server (`mcr.microsoft.com/mssql/server`), Oracle, and some AVX/threading-heavy images hit `qemu: uncaught target signal 11`. This is a qemu-user emulation limit, not a Dory bug, and Rosetta cannot run on `dory-hv` (raw Hypervisor.framework). The fast x86 path is the Virtualization.framework helper: `dory vm --arch amd64 --rosetta` / `DORY_ENGINE_ROSETTA=1`. (GitHub #3) |
| Volume file browser | ✅ | `VolumeBrowser`; verified list + read files inside volumes; GUI sheet |
| Terminal / SSH into containers + machines | ✅ | `TerminalLauncher` opens Terminal.app against Dory's socket/engine |
| Docker Desktop / OrbStack migration | ✅ | `MigrationAssistant` imports images + containers into Dory's shared VM; Docker-compatible sources stream image archives directly when possible, so local/private images do not require a registry pull or a full tarball buffered in app memory |
Expand All @@ -90,11 +90,11 @@ CA trust install remain consent-gated, the same one-time admin grant OrbStack ne

### Apple containerization helper: low-level VM controls delivered

Every feature achievable through Apple's `container` CLI + the dind architecture is done. The four
items below were each investigated and shown to need low-level VM control the CLI does not expose:
device passthrough, memory ballooning, Rosetta device, custom mounts. Dory now delivers those
controls through the bundled `dory-vm` helper, which links the `apple/containerization` Swift
package and drives the VM in-process.
Several features need low-level VM control the `container` CLI does not expose: audio, memory
ballooning, the Rosetta device, custom mounts — all delivered through the bundled `dory-vm` helper,
which links the `apple/containerization` Swift package and drives the VM in-process. USB *device*
passthrough is the exception: the helper attaches a USB controller but does not yet pass a host
device through — real per-device passthrough is the usbip-over-vsock path (roadmap Track 3.6).

**Foundation built + PROVEN END-TO-END.** `Packages/ContainerizationEngine/` is an additive Swift
package (separate from the shipping app) that links `apple/containerization` and drives the Linux VM
Expand All @@ -116,10 +116,12 @@ features without linking the framework's large dependency tree.
|---|---|---|
| Rosetta-speed x86 | ✅ **delivered** | `dory vm --arch amd64 --rosetta -- <cmd>` → `uname -m == x86_64`. Verified through the CLI |
| Reverse / bidirectional file mount | ✅ **delivered** | `dory vm --mount host:guest -- <cmd>` reads/writes host files in the container. Verified |
| USB / audio passthrough | ✅ **delivered** | `dory vm --devices`: a `VZInstanceExtension` injects an XHCI USB controller + `VZVirtioSoundDevice`. Verified `USB controllers attached: 1` |
| Audio passthrough | ✅ **delivered** | `dory vm --devices`: a `VZInstanceExtension` injects `VZVirtioSoundDevice`. Verified audio device configured |
| USB device passthrough | 🚧 **in progress** | `dory vm --devices` attaches a `VZXHCIController`, but **no host USB device is passed through** — it is an empty controller (`USB controllers attached: 1` confirms the controller, not a device). Real per-device passthrough is the usbip-over-vsock path (roadmap Track 3.6), pending the `--usb` hardware gate |
| Dynamic memory balloon → macOS | ✅ **delivered** | `dory vm --devices` attaches a balloon and reclaims RAM at runtime via the public `vzVirtualMachine`, verified `1024MiB → 512MiB reclaimed to macOS` |

**All four are delivered** through the bundled, entitlement-signed `dory-vm` helper, surfaced by the
**Rosetta, file mount, audio, and the memory balloon are delivered** through the bundled,
entitlement-signed `dory-vm` helper, surfaced by the
`dory` CLI (`dory vm`). The default shared-VM engine is untouched. (A GUI entry point for the
in-process engine is not yet wired up.)

Expand Down
12 changes: 12 additions & 0 deletions Dory/App/AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import AppKit

final class DoryAppDelegate: NSObject, NSApplicationDelegate {
private var wakeObserver: NSObjectProtocol?

static var isTestHost: Bool {
ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil
|| ProcessInfo.processInfo.environment["XCTestSessionIdentifier"] != nil
Expand All @@ -9,13 +11,23 @@ final class DoryAppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
guard !Self.isTestHost else { return }
NSApp.setActivationPolicy(.accessory)
wakeObserver = NSWorkspace.shared.notificationCenter.addObserver(
forName: NSWorkspace.didWakeNotification,
object: nil,
queue: .main
) { _ in
SharedVMProvisioner.resyncClockAfterWake()
}
}

func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
false
}

func applicationWillTerminate(_ notification: Notification) {
if let wakeObserver {
NSWorkspace.shared.notificationCenter.removeObserver(wakeObserver)
}
DockerContext.deactivateSync()
SharedVMProvisioner.stopEngineDetached()
}
Expand Down
2 changes: 1 addition & 1 deletion Dory/Compose/ComposeModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,7 @@ enum ComposeParser {
return .mapping(merged)
}

private static func stripTags(_ value: YAMLValue) -> YAMLValue {
private nonisolated static func stripTags(_ value: YAMLValue) -> YAMLValue {
switch value {
case let .tagged(_, inner): return stripTags(inner)
case let .mapping(map): return .mapping(map.mapValues(stripTags))
Expand Down
67 changes: 43 additions & 24 deletions Dory/Compose/YAMLParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ struct YAMLParser {
private let lines: [Line]
private var index = 0

static func parse(_ text: String) throws -> YAMLValue {
nonisolated static func parse(_ text: String) throws -> YAMLValue {
var parser = YAMLParser(text: text)
return try parser.parseDocument()
}

private init(text: String) {
private nonisolated init(text: String) {
var collected: [Line] = []
for raw in text.split(separator: "\n", omittingEmptySubsequences: false) {
let stripped = YAMLParser.stripComment(String(raw))
Expand All @@ -28,19 +28,19 @@ struct YAMLParser {
lines = collected
}

private mutating func parseDocument() throws -> YAMLValue {
private nonisolated mutating func parseDocument() throws -> YAMLValue {
guard index < lines.count else { return .null }
return try parseNode(indent: lines[index].indent)
}

private mutating func parseNode(indent: Int) throws -> YAMLValue {
private nonisolated mutating func parseNode(indent: Int) throws -> YAMLValue {
if lines[index].content.hasPrefix("- ") || lines[index].content == "-" {
return try parseSequence(indent: indent)
}
return try parseMapping(indent: indent)
}

private mutating func parseMapping(indent: Int) throws -> YAMLValue {
private nonisolated mutating func parseMapping(indent: Int) throws -> YAMLValue {
var map: [String: YAMLValue] = [:]
while index < lines.count, lines[index].indent == indent,
!lines[index].content.hasPrefix("- ") {
Expand All @@ -58,7 +58,7 @@ struct YAMLParser {
return .mapping(map)
}

private mutating func parseSequence(indent: Int) throws -> YAMLValue {
private nonisolated mutating func parseSequence(indent: Int) throws -> YAMLValue {
var seq: [YAMLValue] = []
while index < lines.count, lines[index].indent == indent,
lines[index].content.hasPrefix("- ") || lines[index].content == "-" {
Expand Down Expand Up @@ -92,7 +92,7 @@ struct YAMLParser {
return .sequence(seq)
}

private mutating func parseChildBlock(parentIndent: Int) throws -> YAMLValue {
private nonisolated mutating func parseChildBlock(parentIndent: Int) throws -> YAMLValue {
guard index < lines.count else { return .null }
let next = lines[index]
if next.indent > parentIndent {
Expand All @@ -106,7 +106,7 @@ struct YAMLParser {

// MARK: Scalars and flow collections

static func scalarOrFlow(_ raw: String) throws -> YAMLValue {
nonisolated static func scalarOrFlow(_ raw: String) throws -> YAMLValue {
let trimmed = raw.trimmingCharacters(in: .whitespaces)
if let (tag, remainder) = mergeTag(trimmed) {
return .tagged(tag, try scalarOrFlow(remainder))
Expand All @@ -118,7 +118,7 @@ struct YAMLParser {
return scalar(trimmed)
}

private static func mergeTag(_ trimmed: String) -> (MergeTag, String)? {
private nonisolated static func mergeTag(_ trimmed: String) -> (MergeTag, String)? {
for (token, tag) in [("!override", MergeTag.override), ("!reset", MergeTag.reset)] {
if trimmed == token { return (tag, "") }
if trimmed.hasPrefix(token + " ") {
Expand All @@ -128,7 +128,7 @@ struct YAMLParser {
return nil
}

static func scalar(_ raw: String) -> YAMLValue {
nonisolated static func scalar(_ raw: String) -> YAMLValue {
let trimmed = raw.trimmingCharacters(in: .whitespaces)
if trimmed.hasPrefix("\"") && trimmed.hasSuffix("\"") && trimmed.count >= 2 {
return .string(unescape(String(trimmed.dropFirst().dropLast())))
Expand All @@ -150,13 +150,13 @@ struct YAMLParser {
return .string(trimmed)
}

private static func unescape(_ s: String) -> String {
private nonisolated static func unescape(_ s: String) -> String {
s.replacingOccurrences(of: "\\n", with: "\n")
.replacingOccurrences(of: "\\t", with: "\t")
.replacingOccurrences(of: "\\\"", with: "\"")
}

static func splitKeyValue(_ content: String) -> (key: String, value: String)? {
nonisolated static func splitKeyValue(_ content: String) -> (key: String, value: String)? {
var inSingle = false, inDouble = false
let chars = Array(content)
var i = 0
Expand All @@ -177,13 +177,13 @@ struct YAMLParser {
return nil
}

private static func unquoteKey(_ key: String) -> String {
private nonisolated static func unquoteKey(_ key: String) -> String {
if key.hasPrefix("\"") && key.hasSuffix("\"") && key.count >= 2 { return String(key.dropFirst().dropLast()) }
if key.hasPrefix("'") && key.hasSuffix("'") && key.count >= 2 { return String(key.dropFirst().dropLast()) }
return key
}

private static func stripComment(_ line: String) -> String {
private nonisolated static func stripComment(_ line: String) -> String {
var inSingle = false, inDouble = false
let chars = Array(line)
var i = 0
Expand All @@ -205,9 +205,9 @@ struct YAMLParser {
private struct FlowScanner {
private let chars: [Character]
private var i = 0
init(_ string: String) { chars = Array(string) }
nonisolated init(_ string: String) { chars = Array(string) }

mutating func parseValue() throws -> YAMLValue {
nonisolated mutating func parseValue() throws -> YAMLValue {
skipSpaces()
guard i < chars.count else { return .null }
switch chars[i] {
Expand All @@ -218,7 +218,7 @@ private struct FlowScanner {
}
}

private mutating func parseSequence() throws -> YAMLValue {
private nonisolated mutating func parseSequence() throws -> YAMLValue {
i += 1
var items: [YAMLValue] = []
skipSpaces()
Expand All @@ -233,7 +233,7 @@ private struct FlowScanner {
return .sequence(items)
}

private mutating func parseMapping() throws -> YAMLValue {
private nonisolated mutating func parseMapping() throws -> YAMLValue {
i += 1
var map: [String: YAMLValue] = [:]
skipSpaces()
Expand All @@ -253,26 +253,45 @@ private struct FlowScanner {
return .mapping(map)
}

private mutating func parseQuoted() -> String {
private nonisolated mutating func parseQuoted() -> String {
let quote = chars[i]; i += 1
var result = ""
while i < chars.count, chars[i] != quote { result.append(chars[i]); i += 1 }
if i < chars.count { i += 1 }
return result
}

private mutating func parseScalar() -> String {
private nonisolated mutating func parseScalar() -> String {
var result = ""
while i < chars.count, !",]}".contains(chars[i]) { result.append(chars[i]); i += 1 }
var braceDepth = 0
while i < chars.count {
let c = chars[i]
if c == "{" {
braceDepth += 1
result.append(c)
i += 1
continue
}
if c == "}" {
if braceDepth == 0 { break }
braceDepth -= 1
result.append(c)
i += 1
continue
}
if c == "," || c == "]" { break }
result.append(c)
i += 1
}
return result.trimmingCharacters(in: .whitespaces)
}

private mutating func parseKey() -> String {
private nonisolated mutating func parseKey() -> String {
var result = ""
while i < chars.count, !":,]}".contains(chars[i]) { result.append(chars[i]); i += 1 }
return result.trimmingCharacters(in: .whitespaces)
}

private func peek() -> Character? { i < chars.count ? chars[i] : nil }
private mutating func skipSpaces() { while i < chars.count, chars[i] == " " { i += 1 } }
private nonisolated func peek() -> Character? { i < chars.count ? chars[i] : nil }
private nonisolated mutating func skipSpaces() { while i < chars.count, chars[i] == " " { i += 1 } }
}
20 changes: 5 additions & 15 deletions Dory/Features/Containers/ContainersView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,11 @@ struct ContainersView: View {
SkeletonRows()
Spacer(minLength: 0)
} else if store.loadState == .engineOff {
if store.needsContainerToolchain || store.toolchainInstallPhase != .idle {
VStack {
Spacer(minLength: 0)
EngineSetupCard()
Spacer(minLength: 0)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.padding(40)
} else {
TableEmptyState(glyph: .containers, title: "Engine not running",
message: store.sharedVMStatus.isEmpty
? "Dory's container engine isn't running yet. It starts automatically when Dory connects."
: store.sharedVMStatus,
actionLabel: "Try again", action: { Task { await store.retryEngine() } })
}
TableEmptyState(glyph: .containers, title: "Engine not running",
message: store.sharedVMStatus.isEmpty
? "Dory's engine isn't running yet. It starts automatically when Dory connects."
: store.sharedVMStatus,
actionLabel: "Try again", action: { Task { await store.retryEngine() } })
} else if store.containers.isEmpty {
TableEmptyState(glyph: .containers, title: "No containers yet",
message: "Run a container from an image, or start one with `docker run`.",
Expand Down
Loading
Loading