398 lines
17 KiB
Swift
398 lines
17 KiB
Swift
import Foundation
|
|
|
|
// Linux substitutes for the Apple-only surface NucleicCore leans on (docs/CARBON_RUNNER.md
|
|
// §9, CLOUD_RUNTIME §3.1). Everything here is `#if`-gated to compile ONLY where the Apple
|
|
// original is absent, so Darwin builds are untouched.
|
|
|
|
// MARK: - os.Logger shim
|
|
|
|
#if !canImport(os)
|
|
/// The narrow slice of `os.Logger` NucleicCore uses (module-level loggers + privacy-annotated
|
|
/// interpolation), emitted to stderr with the subsystem/category label. Structured logging can
|
|
/// replace this with swift-log later; the call sites won't change.
|
|
public struct Logger: Sendable {
|
|
public struct Message: ExpressibleByStringInterpolation, Sendable {
|
|
public struct Privacy: Sendable {
|
|
public static let `public` = Privacy()
|
|
public static let `private` = Privacy()
|
|
public static let auto = Privacy()
|
|
public static let sensitive = Privacy()
|
|
}
|
|
|
|
public struct StringInterpolation: StringInterpolationProtocol, Sendable {
|
|
var rendered = ""
|
|
public init(literalCapacity: Int, interpolationCount: Int) {
|
|
rendered.reserveCapacity(literalCapacity + interpolationCount * 8)
|
|
}
|
|
public mutating func appendLiteral(_ literal: String) { rendered += literal }
|
|
public mutating func appendInterpolation<T>(_ value: @autoclosure () -> T) {
|
|
rendered += String(describing: value())
|
|
}
|
|
public mutating func appendInterpolation<T>(
|
|
_ value: @autoclosure () -> T, privacy: Privacy
|
|
) {
|
|
rendered += String(describing: value())
|
|
}
|
|
}
|
|
|
|
let rendered: String
|
|
public init(stringLiteral value: String) { rendered = value }
|
|
public init(stringInterpolation: StringInterpolation) {
|
|
rendered = stringInterpolation.rendered
|
|
}
|
|
}
|
|
|
|
let label: String
|
|
public init(subsystem: String, category: String) { label = "\(subsystem).\(category)" }
|
|
|
|
public func trace(_ message: Message) { emit("trace", message) }
|
|
public func debug(_ message: Message) { emit("debug", message) }
|
|
public func info(_ message: Message) { emit("info", message) }
|
|
public func notice(_ message: Message) { emit("notice", message) }
|
|
public func warning(_ message: Message) { emit("warning", message) }
|
|
public func error(_ message: Message) { emit("error", message) }
|
|
public func fault(_ message: Message) { emit("fault", message) }
|
|
|
|
private func emit(_ level: String, _ message: Message) {
|
|
FileHandle.standardError.write(Data("[\(label)] \(level): \(message.rendered)\n".utf8))
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// MARK: - File-based secret store (the Keychain's Linux stand-in)
|
|
|
|
#if !canImport(Security)
|
|
/// Where the Keychain-backed stores keep their secrets on Linux: 0600 files under a 0700 dir,
|
|
/// one per account string — the CLOUD_RUNTIME §3.1 "0600-file secret store" seam. Inside a
|
|
/// runner container this directory lives on the container's own disk (checkpointed per
|
|
/// CLOUD_RUNTIME §3.5), never in the repo or a mount the agent works in.
|
|
enum LinuxSecretStore {
|
|
static var baseDirectory: URL {
|
|
if let override = ProcessInfo.processInfo.environment["NUCLEIC_SECRETS_DIR"],
|
|
!override.isEmpty
|
|
{
|
|
return URL(fileURLWithPath: override, isDirectory: true)
|
|
}
|
|
let home = FileManager.default.homeDirectoryForCurrentUser
|
|
return home.appendingPathComponent(".config/nucleic/secrets", isDirectory: true)
|
|
}
|
|
|
|
private static func url(for account: String) -> URL {
|
|
// Account strings are reverse-DNS-ish identifiers; keep the mapping readable while
|
|
// guarding against separators.
|
|
let safe = account.replacingOccurrences(of: "/", with: "_")
|
|
return baseDirectory.appendingPathComponent(safe)
|
|
}
|
|
|
|
static func read(account: String) -> Data? {
|
|
try? Data(contentsOf: url(for: account))
|
|
}
|
|
|
|
@discardableResult
|
|
static func write(account: String, data: Data) -> Bool {
|
|
let dir = baseDirectory
|
|
do {
|
|
try FileManager.default.createDirectory(
|
|
at: dir, withIntermediateDirectories: true,
|
|
attributes: [.posixPermissions: 0o700])
|
|
let target = url(for: account)
|
|
try data.write(to: target, options: .atomic)
|
|
try FileManager.default.setAttributes(
|
|
[.posixPermissions: 0o600], ofItemAtPath: target.path)
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
static func remove(account: String) {
|
|
try? FileManager.default.removeItem(at: url(for: account))
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// MARK: - Sandbox-service stubs
|
|
|
|
#if !canImport(Containerization)
|
|
/// The two container-probe value types the engine defines on Darwin
|
|
/// (`Container/ContainerEngine.swift`); mirrored here so the shared surface compiles.
|
|
public struct ContainerResourceSample: Sendable, Equatable {
|
|
public let cpuPercent: Double
|
|
public let memoryUsedBytes: UInt64
|
|
public let memoryTotalBytes: UInt64
|
|
|
|
public init(cpuPercent: Double, memoryUsedBytes: UInt64, memoryTotalBytes: UInt64) {
|
|
self.cpuPercent = cpuPercent
|
|
self.memoryUsedBytes = memoryUsedBytes
|
|
self.memoryTotalBytes = memoryTotalBytes
|
|
}
|
|
|
|
/// Memory utilization 0…100. Zero when the total is unknown (avoids a divide-by-zero).
|
|
public var memoryPercent: Double {
|
|
memoryTotalBytes == 0 ? 0 : min(100, Double(memoryUsedBytes) / Double(memoryTotalBytes) * 100)
|
|
}
|
|
}
|
|
|
|
public struct ContainerKillDiagnosis: Sendable, Equatable {
|
|
public let containerRunning: Bool
|
|
public let oomKills: Int?
|
|
public let sample: ContainerResourceSample?
|
|
|
|
public init(containerRunning: Bool, oomKills: Int?, sample: ContainerResourceSample?) {
|
|
self.containerRunning = containerRunning
|
|
self.oomKills = oomKills
|
|
self.sample = sample
|
|
}
|
|
}
|
|
|
|
/// Linux stand-in for the Apple-containerization sandbox orchestrator. On a runner the
|
|
/// container IS the sandbox (`RunSpec.container` stays nil — CLOUD_RUNTIME §0), so the
|
|
/// service is never enabled and the throwing members are never reached; they exist so the
|
|
/// shared object graph (AppStore, backends, SessionController signatures) compiles unchanged.
|
|
/// Tier-1 session sandboxes on Linux arrive later via the RunnerPool acquire seam
|
|
/// (docs/CARBON_RUNNER.md §4), not through this type. The pure naming statics mirror the
|
|
/// Darwin originals exactly (same channel defines), so names stay identical cross-platform.
|
|
public actor ContainerManager {
|
|
public struct LinuxUnsupported: Error, CustomStringConvertible {
|
|
public let description = "the Apple-containerization sandbox service does not exist on Linux"
|
|
}
|
|
|
|
// Naming statics — byte-identical to Container/ContainerManager.swift; keep in lockstep.
|
|
public nonisolated static let channelSuffix: String = {
|
|
#if NUCLEIC_STABLE
|
|
return ""
|
|
#elseif NUCLEIC_RC
|
|
return "-rc"
|
|
#elseif NUCLEIC_BETA
|
|
return "-beta"
|
|
#elseif NUCLEIC_CANARY
|
|
return "-canary"
|
|
#else
|
|
return "-local"
|
|
#endif
|
|
}()
|
|
public nonisolated static let nonReleaseSuffixes = ["-local", "-canary", "-beta", "-rc"]
|
|
|
|
public nonisolated static func ownsContainer(named name: String) -> Bool {
|
|
channelSuffix.isEmpty
|
|
? !nonReleaseSuffixes.contains(where: name.hasSuffix)
|
|
: name.hasSuffix(channelSuffix)
|
|
}
|
|
|
|
public nonisolated static func containerName(for session: SessionID) -> String {
|
|
"nucleic-\(session.short.lowercased())\(channelSuffix)"
|
|
}
|
|
|
|
public static let sharedControlContainerName = "nucleic-control" + channelSuffix
|
|
public static let claudeControlContainerName = "nucleic-control-claude" + channelSuffix
|
|
public static let codexControlContainerName = "nucleic-control-codex" + channelSuffix
|
|
public static let xaiControlContainerName = "nucleic-control-xai" + channelSuffix
|
|
public static let opencodeControlContainerName = "nucleic-control-opencode" + channelSuffix
|
|
public static let openclawControlContainerName = "nucleic-control-openclaw" + channelSuffix
|
|
public static let hermesControlContainerName = "nucleic-control-hermes" + channelSuffix
|
|
public static let cursorControlContainerName = "nucleic-control-cursor" + channelSuffix
|
|
public static let allSharedControlContainerNames = [
|
|
sharedControlContainerName, claudeControlContainerName, codexControlContainerName,
|
|
xaiControlContainerName, opencodeControlContainerName, openclawControlContainerName,
|
|
hermesControlContainerName, cursorControlContainerName,
|
|
]
|
|
|
|
public nonisolated static func sharedContainerName(for backend: BackendID, split: Bool) -> String {
|
|
guard split else { return sharedControlContainerName }
|
|
switch backend {
|
|
case .claudeCode: return claudeControlContainerName
|
|
case .codex, .codexExec: return codexControlContainerName
|
|
case .grok: return xaiControlContainerName
|
|
case .opencode: return opencodeControlContainerName
|
|
case .openclaw: return openclawControlContainerName
|
|
case .hermes: return hermesControlContainerName
|
|
case .cursorAgent: return cursorControlContainerName
|
|
case .acp: return sharedControlContainerName
|
|
}
|
|
}
|
|
|
|
let onSharedContainerRemoved: @Sendable (String) async -> Void
|
|
|
|
public init(onSharedContainerRemoved: @Sendable @escaping (String) async -> Void = { _ in }) {
|
|
self.onSharedContainerRemoved = onSharedContainerRemoved
|
|
}
|
|
|
|
public func ensureRunning(_ spec: ContainerSpec) async throws
|
|
-> (name: String, hostGateway: String)
|
|
{
|
|
throw LinuxUnsupported()
|
|
}
|
|
|
|
public func exec(
|
|
name: String, workdir: String, env: [String: String], argv: [String],
|
|
uid: Int? = nil, gid: Int? = nil
|
|
) async throws -> any ProcessHandle {
|
|
throw LinuxUnsupported()
|
|
}
|
|
|
|
public func finished(name: String) {}
|
|
public func isRunning(name: String) async -> Bool { false }
|
|
public func stopAgentContainer(name: String) async {}
|
|
public func removeAgentContainer(name: String) async -> Bool { false }
|
|
public func teardown(_ session: SessionID, waitForActive: Bool = false) async -> String? { nil }
|
|
public func teardownShared() async {}
|
|
public func controlContainerStatus() async -> (exists: Bool, running: Bool) { (false, false) }
|
|
public func controlContainers() async -> [ControlContainerEntry] { [] }
|
|
public func controlContainerUsage() async -> ContainerResourceSample? { nil }
|
|
public func controlDownloadProgress() async -> ContainerDownloadProgress? { nil }
|
|
public func diagnoseContainerKill(name: String) async -> ContainerKillDiagnosis {
|
|
ContainerKillDiagnosis(containerRunning: false, oomKills: nil, sample: nil)
|
|
}
|
|
public func reconcile(activeSessions: [SessionID]) async {}
|
|
public func recreateShared() async {}
|
|
public func restartShared() async {}
|
|
public func updateAgentCLIs() async -> [AgentCLIUpdateResult] { [] }
|
|
public func checkImageUpdate() async -> ImageUpdateStatus { .idle }
|
|
public func reclaimMemoryNow() async {}
|
|
}
|
|
#endif
|
|
|
|
#if !canImport(Virtualization)
|
|
/// `UncheckedSendableBox` lives in `MacVM/MacVMEngine+Base.swift` on Darwin (excluded here);
|
|
/// `MacVMSurfaceHost` (portable, kept) names it in a signature, so mirror the box.
|
|
public struct UncheckedSendableBox<T>: @unchecked Sendable {
|
|
public let value: T
|
|
public init(_ value: T) { self.value = value }
|
|
}
|
|
|
|
/// Mirrors of the two Control-panel value types `MacVM/MacVMManager.swift` defines on Darwin.
|
|
public struct MacVMEntry: Sendable, Equatable {
|
|
public let name: String
|
|
public let ipAddress: String?
|
|
public let os: GuestOS
|
|
public let paused: Bool
|
|
public let resuming: Bool
|
|
public let suspendedToDisk: Bool
|
|
public init(
|
|
name: String, ipAddress: String?, os: GuestOS = .macOS,
|
|
paused: Bool = false, resuming: Bool = false, suspendedToDisk: Bool = false
|
|
) {
|
|
self.name = name
|
|
self.ipAddress = ipAddress
|
|
self.os = os
|
|
self.paused = paused
|
|
self.resuming = resuming
|
|
self.suspendedToDisk = suspendedToDisk
|
|
}
|
|
}
|
|
|
|
public struct MacVMMaintenanceInfo: Sendable, Equatable {
|
|
public let name: String
|
|
public let os: GuestOS
|
|
public let activity: String
|
|
public let booting: Bool
|
|
public init(name: String, os: GuestOS, activity: String, booting: Bool) {
|
|
self.name = name
|
|
self.os = os
|
|
self.activity = activity
|
|
self.booting = booting
|
|
}
|
|
}
|
|
|
|
/// Linux stand-in for the macOS/Linux-VM orchestrator (`mac_vm_*` / `linux_vm_*` tools). The VM
|
|
/// service is Darwin-only (Apple Virtualization); on a runner these tools are simply never
|
|
/// exposed (`isSupported` is false, `MacVMSettings.serviceEnabled` defaults off, and no
|
|
/// `RunSpec.allow*VM*` is ever set), so the throwing members are never reached. Exists so the
|
|
/// shared object graph compiles unchanged. Signatures mirror `MacVM/MacVMManager.swift`.
|
|
public actor MacVMManager {
|
|
public struct LinuxUnsupported: Error, CustomStringConvertible {
|
|
public let description = "the macOS/Linux VM service does not exist on a Linux runner"
|
|
}
|
|
|
|
public nonisolated static var isSupported: Bool { false }
|
|
|
|
public nonisolated static func vmName(for session: SessionID) -> String {
|
|
"nucleic-mac-\(session.short.lowercased())\(ContainerManager.channelSuffix)"
|
|
}
|
|
|
|
public nonisolated static func linuxVMName(for session: SessionID) -> String {
|
|
"nucleic-lin-\(session.short.lowercased())\(ContainerManager.channelSuffix)"
|
|
}
|
|
|
|
public init() {}
|
|
|
|
public func ensureRunning(_ spec: MacVMSpec) async throws -> (name: String, ipAddress: String) {
|
|
throw LinuxUnsupported()
|
|
}
|
|
|
|
public func exec(
|
|
name: String, workdir: String?, env: [String: String], argv: [String]
|
|
) async throws -> any ProcessHandle {
|
|
throw LinuxUnsupported()
|
|
}
|
|
|
|
public func run(
|
|
name: String, command: String, workdir: String?, env: [String: String] = [:]
|
|
) async throws -> (exitCode: Int32, stdout: String, stderr: String) {
|
|
throw LinuxUnsupported()
|
|
}
|
|
|
|
public func performComputerAction(
|
|
name: String, action: String, x: Int?, y: Int?, text: String?,
|
|
ref: String? = nil, value: String? = nil,
|
|
scrollDirection: String?, scrollAmount: Int?, durationMs: Int?
|
|
) async throws -> (imageBase64: String?, summary: String) {
|
|
throw LinuxUnsupported()
|
|
}
|
|
|
|
public func installApps(name: String, appPaths: [String]) async throws -> [String] {
|
|
throw LinuxUnsupported()
|
|
}
|
|
public func installAppsIntoBase(appPaths: [String]) async throws -> [String] {
|
|
throw LinuxUnsupported()
|
|
}
|
|
public func installPackages(name: String, packageIDs: [String]) async throws -> [String] {
|
|
throw LinuxUnsupported()
|
|
}
|
|
public func installPackagesIntoBase(packageIDs: [String]) async throws -> [String] {
|
|
throw LinuxUnsupported()
|
|
}
|
|
|
|
public func finished(name: String) async {}
|
|
public func suspendIfIdle(_ session: SessionID) async {}
|
|
public func teardown(_ session: SessionID, waitForActive: Bool = false) async -> String? { nil }
|
|
public func restart(_ session: SessionID) async {}
|
|
public func stop(name: String) async {}
|
|
public func suspend(name: String) async -> Bool { false }
|
|
public func resume(name: String) async -> Bool { false }
|
|
public func restart(name: String) async {}
|
|
public func remove(name: String) async -> Bool { false }
|
|
public func exists(name: String) async -> Bool { false }
|
|
public func isPaused(name: String) async -> Bool { false }
|
|
public func isRunning(name: String) async -> Bool { false }
|
|
public func isWorkingInBackground(name: String) -> Bool { false }
|
|
public func backgroundLog(name: String) async -> [MacVMLogLine] { [] }
|
|
public func runningVMs() async -> [MacVMEntry] { [] }
|
|
public func baseMaintenanceVM() async -> MacVMMaintenanceInfo? { nil }
|
|
public func captureScreen(name: String) async -> String? { nil }
|
|
public func captureScreenData(name: String) async -> Data? { nil }
|
|
public func sampleResourceUsage(name: String) async -> MacVMResourceSample? { nil }
|
|
public func baseProgress() async -> MacVMBaseProgress? { nil }
|
|
public func baseImageOSVersion() async -> String? { nil }
|
|
public func baseStatus() async -> MacVMBaseStatus { MacVMBaseStatus() }
|
|
public func linuxBaseStatus() async -> MacVMBaseStatus { MacVMBaseStatus() }
|
|
public func baseBundleRootPath() async -> String? { nil }
|
|
public func beginBaseRecovery() async throws -> String { throw LinuxUnsupported() }
|
|
public func endBaseRecovery() async {}
|
|
public func setSurfaceHost(_ host: any MacVMSurfaceHost) async {}
|
|
public func setEvictionPolicy(_ ranker: @escaping @Sendable ([String]) async -> [String]) async {}
|
|
public func buildBaseImage(localRestoreImagePath: String? = nil) async throws {
|
|
throw LinuxUnsupported()
|
|
}
|
|
public func deleteBaseImage(includingRestoreImages: Bool) async throws {
|
|
throw LinuxUnsupported()
|
|
}
|
|
public func buildLinuxBaseImage() async throws { throw LinuxUnsupported() }
|
|
public func deleteLinuxBaseImage() async throws { throw LinuxUnsupported() }
|
|
public func setBaseProvisionObserver(visible: Bool) async {}
|
|
public func presentOperatorAssist(name: String, visible: Bool) async {}
|
|
public func reconcile(activeSessions: [SessionID]) async {}
|
|
public func reprovisionStaleBaseImagesIfNeeded() async {}
|
|
}
|
|
#endif
|