Files
nucleic/Sources/NucleicApp/NucleicApp.swift
T
abkslmandClaude Opus 4.8 ff10d440a9 Add per-project sandboxing via Apple's container
Opt-in, per-project execution sandbox: when enabled, a project's sessions run
`claude` inside an isolated Linux VM (Apple `container`) with the worktree
bind-mounted, instead of directly on the host. Off by default — existing
behavior is unchanged.

- Domain: ProjectSandbox (enabled/image/idleTimeout) on Project; ContainerSpec
  on RunSpec/ResumeSpec.
- Persistence: migration v7 adds project.sandbox_config (JSON).
- ContainerRuntime: thin `container` CLI wrapper (preflight, default-image build,
  run/exec/stop/delete/list, host-gateway discovery).
- ContainerManager: app-level per-session lifecycle — ensureRunning, idle
  auto-stop, teardown, orphan reconcile.
- ClaudeCodeBackend: wraps the claude invocation in `container exec` when a
  ContainerSpec is present; binds the approval MCP server on 0.0.0.0 and rewrites
  its URL to the VM gateway so the containerized child can reach it.
- Repo root + worktree base mounted at identical paths (git links + cwd-hash
  resolve); host ~/.claude mounted read-only and seeded into a writable
  claude-home so credentials are never mutated but native resume still works.
- UI: ProjectSettingsSheet (toggle/image/idle) + "Sandboxed" badge; AppStore
  gains updateProject.
- Tests: 9 new (arg construction, mount formatting, name parsing/derivation,
  sandbox JSON round-trip, MCP host rewrite). 112 pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-12 18:26:37 -07:00

90 lines
3.8 KiB
Swift

import SwiftUI
import NucleicCore
/// The macOS cockpit (PLAN "macOS UI"). A thin SwiftUI shell over `AppStore` — it
/// holds no canonical state, just observes the store and sends intents (RUNTIME §3).
///
/// Runs as a SwiftPM executable; launch with
/// `swift run --build-system native NucleicApp`. The store persists under
/// `~/Library/Application Support/Nucleic`.
@main
struct NucleicApp: App {
@State private var store: AppStore
init() {
// Show a Dock icon + foreground window when launched from the terminal.
NSApplication.shared.setActivationPolicy(.regular)
let support = Self.supportDirectory()
do {
let database = try GRDBMetadataStore(
path: support.appendingPathComponent("nucleic.sqlite").path)
let worktrees = GitWorktreeManager()
let transcriptsDir = support.appendingPathComponent("sessions", isDirectory: true)
// Shared sandbox orchestrator: opt-in per project, dormant unless a session's
// project enables it. The same instance is handed to every backend so container
// lifecycle (idle stop, teardown, reconcile) stays consistent.
let containerManager = ContainerManager()
let store = AppStore(
database: database, worktrees: worktrees, transcriptsDir: transcriptsDir,
containerManager: containerManager
) { _ in
// Each chat turn is a single-shot run (stdin closed), with follow-ups
// resumed via --resume. This is the only reliable way to use the
// approval server: Claude's streaming-input mode hangs alongside
// --permission-prompt-tool. Approval server is authoritative
// (--permission-mode default); child is hermetic (--strict-mcp-config).
ClaudeCodeBackend(
configuration: .init(closeStdinAfterPrompt: true),
containerManager: containerManager)
}
store.intelligence = AppleIntelligenceProvider()
store.defaultModel = ModelCatalog.storedDefaultModel
store.defaultEffort = ModelCatalog.storedDefaultEffort
store.defaultAuto = ModelCatalog.storedDefaultAuto
_store = State(initialValue: store)
} catch {
fatalError("Could not open the Nucleic store at \(support.path): \(error)")
}
}
var body: some Scene {
WindowGroup {
RootView()
.environment(store)
.frame(minWidth: 900, minHeight: 560)
.task {
await store.loadProjects()
await store.loadSessions()
await store.loadTodos()
}
}
.windowToolbarStyle(.unified)
.commands {
CommandGroup(replacing: .newItem) {
// Cmd-N goes to the home dashboard — the surface for starting a new
// chat (its chat bar picks the project, model, and effort).
Button("New Chat") {
store.goHome()
}
.keyboardShortcut("n", modifiers: .command)
// Cmd-T captures a quick idea into the home to-do inbox from anywhere.
Button("New To-Do") {
store.presentQuickTodo()
}
.keyboardShortcut("t", modifiers: .command)
}
}
Settings {
SettingsView().environment(store)
}
}
private static func supportDirectory() -> URL {
let base = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
let dir = base.appendingPathComponent("Nucleic", isDirectory: true)
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
return dir
}
}