Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
b6c7c99
feat(fleet): add fleet control client package
KafuChino123 Jul 2, 2026
1153a1c
feat(fleet): add desktop fleet control facade
KafuChino123 Jul 3, 2026
d09c297
feat(fleet): inject fleet control client
KafuChino123 Jul 3, 2026
060b43d
feat(fleet): add fleet view model
KafuChino123 Jul 3, 2026
a305f2b
chore(fleet): record control proto source
KafuChino123 Jul 14, 2026
ec8c6df
feat(fleet): align client with current control API
KafuChino123 Jul 14, 2026
bc6b9c3
feat(fleet): add platform enrollment client
KafuChino123 Jul 14, 2026
74158fd
feat(runner): add runner host model, view model, and sample data (RUN-9)
AprilNEA Jun 10, 2026
2e9f318
feat(runner): add CI Runners section with host status bar and empty s…
AprilNEA Jun 10, 2026
6697b65
feat(runner): render live fleet agent state
KafuChino123 Jul 14, 2026
f6c7822
fix(runner): restore app test build
KafuChino123 Jul 14, 2026
8d9cc3d
feat(fleet): complete platform client contract
KafuChino123 Jul 14, 2026
e98bfca
feat(fleet): orchestrate desktop enrollment
KafuChino123 Jul 15, 2026
9138757
fix(auth): restore sessions off the main actor
KafuChino123 Jul 15, 2026
f3f8256
feat(runner): reconcile live enrollment UI
KafuChino123 Jul 15, 2026
b942ec2
feat(fleet): add macOS VM control models
KafuChino123 Jul 15, 2026
876385a
feat(fleet): add macOS runner VM settings
KafuChino123 Jul 15, 2026
f3bce61
fix(runner): distinguish enrollment recovery states
KafuChino123 Jul 15, 2026
b1307fa
feat(fleet): handle agent update state
KafuChino123 Jul 15, 2026
3b7a5cd
fix(runner): stop window blowup when enrollment error appears
KafuChino123 Jul 15, 2026
ecf1cd3
feat(fleet): surface macOS runner readiness
KafuChino123 Jul 15, 2026
62af227
fix(runner): clarify fleet recovery states
KafuChino123 Jul 15, 2026
0eb0652
feat(auth): switch sign-in to the device-authorization grant
KafuChino123 Jul 16, 2026
3ef7c47
feat(fleet): add manual enrollment fallback
KafuChino123 Jul 17, 2026
97e5cde
refactor(fleet): remove manual enrollment UI
KafuChino123 Jul 19, 2026
9b98855
feat(runner): add platform machine and job data store
KafuChino123 Jul 24, 2026
11c6d2f
feat(runner): add host and job selection
KafuChino123 Jul 24, 2026
91ecd41
feat(runner): add host detail tabs
KafuChino123 Jul 24, 2026
ed7d362
feat(runner): add job detail tabs
KafuChino123 Jul 24, 2026
a14d188
feat(runner): report live pool activity
KafuChino123 Jul 27, 2026
454805e
fix(auth): isolate device flow from HTTP cookies
KafuChino123 Aug 7, 2026
cbe1d79
fix(auth): address device flow follow-ups
KafuChino123 Aug 9, 2026
b070046
fix(runner): trust terminal fleet snapshots
KafuChino123 Aug 9, 2026
170c749
fix(runner): fall back to connectivity state for stale terminal snaps…
KafuChino123 Aug 9, 2026
ce1bbd5
Merge remote-tracking branch 'origin/master' into feat/abxd-99-device…
KafuChino123 Aug 9, 2026
d94423f
fix(ui): remove duplicate runner sidebar item
KafuChino123 Aug 9, 2026
4dfd4b2
fix(build): remove unstable test package references
KafuChino123 Aug 9, 2026
dff2b2a
Merge remote-tracking branch 'origin/feat/abxd-99-device-auth' into f…
KafuChino123 Aug 9, 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
1 change: 1 addition & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ jobs:
POSTHOG_API_KEY =
OIDC_ISSUER_URL =
OIDC_CLIENT_ID =
FLEET_PLATFORM_BASE_URL =
XCEOF

- name: Generate Xcode project
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ jobs:
#include "Version.xcconfig"
OIDC_ISSUER_URL = https:/$()/auth.arcbox.dev/api/auth
OIDC_CLIENT_ID = arcbox-desktop
FLEET_PLATFORM_BASE_URL = https:/$()/api.arcbox.dev
XCEOF

- name: Trust SwiftPM plugins
Expand Down
2 changes: 1 addition & 1 deletion .swiftlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ included:
- Packages

excluded:
- Packages/ArcBoxClient/Sources/ArcBoxClient/Generated
- Packages/*/Sources/*/Generated
- Packages/*/.build
- .build
- .claude
Expand Down
5 changes: 5 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ A bare `Bool` like `hasCompletedInitialLoad` cannot distinguish "never started"
### Default tab vs lazy tabs
The default tab's view renders during startup. Other tabs render lazily when the user switches to them. This means timing bugs in `.task(id:)` only manifest on the default tab — other tabs work by accident because dependencies are already available when they appear. Always test startup behavior on the default tab specifically.

### `fixedSize(horizontal: false, vertical: true)` window blowup (macOS 26)
Any state change inside a `fixedSize(vertical: true)` subtree in a main-window view triggers a window-sizing pass that resizes the window — or, if the window can't grow, the `NavigationSplitView` content inside it — to the screen's *visible-frame height* (content slides under the title bar, bottom-pinned views disappear). Verified on macOS 26.5 with a minimal repro: inserting, removing, or even changing the text of such a label fires it; the same label without `fixedSize` does not, and still wraps correctly inside width-constrained containers.

**Rule**: don't use `fixedSize(vertical: true)` on labels whose content appears/changes dynamically in the main window (error banners, status text). Text wraps without it in width-bounded layouts; use it only for genuinely static text, ideally in sheets.

## Code Style
- Swift 6 strict concurrency (`SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor`, `SWIFT_APPROACHABLE_CONCURRENCY = YES`)
- ViewModels use `@Observable`; environment injection via custom `EnvironmentKey`
Expand Down
288 changes: 252 additions & 36 deletions ArcBox.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

78 changes: 67 additions & 11 deletions ArcBox/App/ApplicationCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import AppKit
import ArcBoxAuth
import ArcBoxClient
import DockerClient
import FleetPlatformClient
import Foundation
import OSLog
import Observation
Expand All @@ -18,15 +19,19 @@ final class ApplicationCoordinator: NSObject {
let networksVM = NetworksViewModel()
let volumesVM = VolumesViewModel()
let systemVmBackendVM = SystemVmBackendModel()
// App-scoped so the Fleet Watch survives closing the main window.
let runnersVM = RunnersViewModel()

private let eventMonitor = DockerEventMonitor()
private let sandboxEventMonitor = SandboxEventMonitor()
private let machineEventMonitor = MachineEventMonitor()
private let sleepWakeManager = SleepWakeManager()
private let deepLinkRouter = DeepLinkRouter()
private let fleetAgentConnection = FleetAgentConnection()
private let updaterDelegate = UpdaterDelegate()
private let updaterController: SPUStandardUpdaterController
private let updaterSettings: UpdaterSettingsModel
private var fleetPlatformClient: FleetPlatformClient?

private(set) var arcboxClient: ArcBoxClient?
private(set) var dockerClient: DockerClient?
Expand Down Expand Up @@ -99,8 +104,30 @@ final class ApplicationCoordinator: NSObject {
}
}

fleetAgentConnection.start()
Task { [weak self] in
await self?.authSession.loadUserInfo()
guard let self else { return }
await authSession.restoreSession()
initFleetPlatformClientIfNeeded()
runnersVM.start(
controlClient: fleetAgentConnection.controlClient,
platformClient: fleetPlatformClient,
authentication: authSession,
agentReadiness: fleetAgentConnection
)
await authSession.refreshSession()
}
Task { [weak self] in
guard let self else { return }
do {
_ = try await fleetAgentConnection.ensureReady()
} catch is CancellationError {
Log.fleet.info("Fleet Agent readiness probe cancelled")
} catch {
Log.fleet.info(
"Fleet Agent is not ready: \(error.localizedDescription, privacy: .private)"
)
}
}

if !isOnboarding {
Expand Down Expand Up @@ -197,7 +224,7 @@ final class ApplicationCoordinator: NSObject {
guard !isTerminating else { return false }
isTerminating = true

WebAuthenticationController.shared.cancelForTermination()
authSession.cancelSignIn()
statusItemController?.closePopover()
statusItemController?.setVisible(false)
let screen = NSApp.keyWindow?.screen ?? NSApp.mainWindow?.screen ?? NSScreen.main
Expand All @@ -224,6 +251,19 @@ final class ApplicationCoordinator: NSObject {
}

func shutdown() async {
let enrollmentSettled = await runnersVM.prepareForTermination()
if !enrollmentSettled {
Log.fleet.warning(
"Fleet enrollment did not settle before the application termination deadline"
)
}
let connectionClosedGracefully = await fleetAgentConnection.shutdown()
if !connectionClosedGracefully {
Log.fleet.warning(
"Fleet client transport required forced shutdown during application termination"
)
}

startupTask?.cancel()
await startupOrchestrator?.cancelForTermination()
await startupTask?.value
Expand Down Expand Up @@ -263,11 +303,7 @@ final class ApplicationCoordinator: NSObject {
imagesVM: imagesVM,
networksVM: networksVM,
openMainWindow: { [weak self] in self?.showMainWindow() },
openSettingsWindow: { [weak self] in self?.showSettings() },
oauthCallbackScheme: OIDCClientConfiguration.redirectURI.scheme,
onOAuthCallback: { [weak self] url in
Task { await self?.authSession.handleAuthorizationCallback(url) }
}
openSettingsWindow: { [weak self] in self?.showSettings() }
))
}

Expand Down Expand Up @@ -449,8 +485,11 @@ final class ApplicationCoordinator: NSObject {
.environment(volumesVM)
.environment(sandboxEventMonitor)
.environment(authSession)
.environment(runnersVM)
.environment(\.arcboxClient, arcboxClient)
.environment(\.dockerClient, dockerClient)
.environment(\.fleetControlClient, fleetAgentConnection.controlClient)
.environment(\.fleetPlatformClient, fleetPlatformClient)
.environment(\.startupOrchestrator, startupOrchestrator)
.environment(\.accessTokenProvider, authSession)
.frame(minWidth: 900, minHeight: 600)
Expand All @@ -467,8 +506,11 @@ final class ApplicationCoordinator: NSObject {
.environment(authSession)
.environment(systemVmBackendVM)
.environment(updaterSettings)
.environment(runnersVM.fleet)
.environment(\.arcboxClient, arcboxClient)
.environment(\.dockerClient, dockerClient)
.environment(\.fleetControlClient, fleetAgentConnection.controlClient)
.environment(\.fleetPlatformClient, fleetPlatformClient)
.environment(\.accessTokenProvider, authSession)
)
}
Expand Down Expand Up @@ -496,14 +538,28 @@ final class ApplicationCoordinator: NSObject {
showSettings(tab: .account)
return
}
guard authSession.status != .signingIn, !authSession.configuration.isPlaceholder else {
return
}
guard authSession.status != .restoring, authSession.status != .signingIn,
!authSession.configuration.isPlaceholder
else { return }
Task {
await authSession.signIn(using: WebAuthenticationController.shared.authenticate)
await authSession.signIn()
}
}

/// Create the authenticated Platform REST client without starting network work.
private func initFleetPlatformClientIfNeeded() {
guard fleetPlatformClient == nil else { return }

let configuration = FleetPlatformConfiguration.current
Log.fleet.info(
"Creating FleetPlatformClient for \(configuration.baseURL.absoluteString, privacy: .public)"
)
fleetPlatformClient = FleetPlatformClient(
configuration: configuration,
accessTokenProvider: authSession
)
}

private func activate() {
NSApp.unhide(nil)
NSApp.activate(ignoringOtherApps: true)
Expand Down
15 changes: 1 addition & 14 deletions ArcBox/App/DeepLinkRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,6 @@ final class DeepLinkRouter {
let networksVM: NetworksViewModel
let openMainWindow: () -> Void
let openSettingsWindow: () -> Void
/// URL scheme of the OAuth redirect (e.g. `com.arcboxlabs.desktop`).
/// Callbacks with this scheme are forwarded to `onOAuthCallback` rather
/// than parsed as `arcbox://` deep links.
let oauthCallbackScheme: String?
let onOAuthCallback: (URL) -> Void
}

private var target: Target?
Expand All @@ -40,14 +35,6 @@ final class DeepLinkRouter {
}

private func dispatch(_ url: URL) {
guard let target else { return }
if let scheme = target.oauthCallbackScheme,
url.scheme?.caseInsensitiveCompare(scheme) == .orderedSame
{
Log.deepLink.info("Handling OAuth redirect callback")
target.onOAuthCallback(url)
return
}
guard let link = DeepLink(url) else {
Log.deepLink.warning("Ignoring unrecognized deep link: \(url.absoluteString, privacy: .private)")
return
Expand Down Expand Up @@ -81,7 +68,7 @@ final class DeepLinkRouter {
case .volumes: target.volumesVM.selectedID = id
case .images: target.imagesVM.selectedID = id
case .networks: target.networksVM.selectedID = id
case .activity, .pods, .services, .machines, .sandboxes:
case .activity, .pods, .services, .machines, .sandboxes, .runner:
Log.deepLink.info("Item selection unsupported for \(item.rawValue, privacy: .public)")
}
}
Expand Down
20 changes: 20 additions & 0 deletions ArcBox/App/EnvironmentValues+Clients.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import ArcBoxAuth
import ArcBoxClient
import DockerClient
import FleetControlClient
import FleetPlatformClient
import SwiftUI

private struct ArcBoxClientKey: EnvironmentKey {
Expand All @@ -15,6 +17,14 @@ private struct DockerClientKey: EnvironmentKey {
static let defaultValue: DockerClient? = nil
}

private struct FleetControlClientKey: EnvironmentKey {
static let defaultValue: FleetControlClient? = nil
}

private struct FleetPlatformClientKey: EnvironmentKey {
static let defaultValue: FleetPlatformClient? = nil
}

private struct StartupOrchestratorKey: EnvironmentKey {
static let defaultValue: StartupOrchestrator? = nil
}
Expand All @@ -30,6 +40,16 @@ extension EnvironmentValues {
set { self[DockerClientKey.self] = newValue }
}

var fleetControlClient: FleetControlClient? {
get { self[FleetControlClientKey.self] }
set { self[FleetControlClientKey.self] = newValue }
}

var fleetPlatformClient: FleetPlatformClient? {
get { self[FleetPlatformClientKey.self] }
set { self[FleetPlatformClientKey.self] = newValue }
}

var startupOrchestrator: StartupOrchestrator? {
get { self[StartupOrchestratorKey.self] }
set { self[StartupOrchestratorKey.self] = newValue }
Expand Down
3 changes: 2 additions & 1 deletion ArcBox/Components/EmptyStateView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ struct EmptyStateView<Content: View>: View {
Image(systemName: icon)
.font(.system(size: 26))
.foregroundStyle(AppColors.textMuted)
.accessibilityHidden(true)
}

Text(title)
.font(.system(size: 13))
.font(.headline)
.foregroundStyle(AppColors.textSecondary)

content
Expand Down
2 changes: 1 addition & 1 deletion ArcBox/Components/StatusBadge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ struct StatusBadge: View {
.fill(color)
.frame(width: AppMetrics.statusDot, height: AppMetrics.statusDot)
Text(label)
.font(.system(size: 13))
.font(.callout)
.foregroundStyle(color)
}
.accessibilityElement(children: .combine)
Expand Down
2 changes: 2 additions & 0 deletions ArcBox/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
<string>$(OIDC_CLIENT_ID)</string>
<key>OIDCIssuerURL</key>
<string>$(OIDC_ISSUER_URL)</string>
<key>FleetPlatformBaseURL</key>
<string>$(FLEET_PLATFORM_BASE_URL)</string>
<key>ArcBoxProfile</key>
<string>$(ARCBOX_PROFILE)</string>
<key>PostHogAPIKey</key>
Expand Down
1 change: 1 addition & 0 deletions ArcBox/Logging.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ nonisolated enum Log {
static let network = Logger(subsystem: subsystem, category: "network")
static let sandbox = Logger(subsystem: subsystem, category: "sandbox")
static let machine = Logger(subsystem: subsystem, category: "machine")
static let fleet = Logger(subsystem: subsystem, category: "fleet")
static let pods = Logger(subsystem: subsystem, category: "pods")
static let services = Logger(subsystem: subsystem, category: "services")
static let context = Logger(subsystem: subsystem, category: "context")
Expand Down
9 changes: 9 additions & 0 deletions ArcBox/Models/FleetRunnerImageReadiness.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/// macOS runner image readiness presented on the runner page.
enum FleetRunnerImageReadiness: Equatable {
case hidden
case pending(reference: String)
case preparing(FleetImagePreparationProgress)
case restartRequired
case completed(reference: String)
case failed(String)
}
5 changes: 5 additions & 0 deletions ArcBox/Models/NavItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ enum NavItem: String, CaseIterable, Identifiable {
case services
case machines
case sandboxes
case runner

var id: String { rawValue }

Expand All @@ -23,6 +24,7 @@ enum NavItem: String, CaseIterable, Identifiable {
case .services: "Services"
case .machines: "Machines"
case .sandboxes: "Sandboxes"
case .runner: "This Mac"
}
}

Expand All @@ -37,6 +39,7 @@ enum NavItem: String, CaseIterable, Identifiable {
case .services: "gearshape.2"
case .machines: "desktopcomputer"
case .sandboxes: "server.rack"
case .runner: "hammer"
}
}

Expand All @@ -47,6 +50,7 @@ enum NavItem: String, CaseIterable, Identifiable {
case kubernetes = "KUBERNETES"
case linux = "LINUX"
case sandbox = "SANDBOX"
case runners = "RUNNERS"

var id: String { rawValue }

Expand All @@ -57,6 +61,7 @@ enum NavItem: String, CaseIterable, Identifiable {
case .kubernetes: [.pods, .services]
case .linux: [.machines]
case .sandbox: [.sandboxes]
case .runners: [.runner]
}
}
}
Expand Down
18 changes: 18 additions & 0 deletions ArcBox/Models/RunnerHostCapability.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import Darwin
import Foundation

/// Local hardware facts available before this Mac enrolls in a Fleet.
enum RunnerHostCapability {
static let chipName: String = {
var size = 0
guard sysctlbyname("machdep.cpu.brand_string", nil, &size, nil, 0) == 0, size > 0 else {
return "Apple Silicon"
}
var brand = [CChar](repeating: 0, count: size)
guard sysctlbyname("machdep.cpu.brand_string", &brand, &size, nil, 0) == 0 else {
return "Apple Silicon"
}
let bytes = brand.prefix(while: { $0 != 0 }).map { UInt8(bitPattern: $0) }
return String(bytes: bytes, encoding: .utf8) ?? "Apple Silicon"
}()
}
8 changes: 8 additions & 0 deletions ArcBox/Models/RunnerHostDetailTab.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
enum RunnerHostDetailTab: String, CaseIterable, Identifiable {
case overview = "Overview"
case capacity = "Capacity"
case settings = "Settings"
case identity = "Identity"

var id: Self { self }
}
Loading