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]>
This commit is contained in:
@@ -108,6 +108,9 @@ public final class GRDBMetadataStore: SessionMetadataStore {
|
||||
migrator.registerMigration("v6-todo-summary") { db in
|
||||
try db.execute(sql: "ALTER TABLE todo ADD COLUMN summary TEXT;")
|
||||
}
|
||||
migrator.registerMigration("v7-sandbox") { db in
|
||||
try db.execute(sql: "ALTER TABLE project ADD COLUMN sandbox_config TEXT;")
|
||||
}
|
||||
return migrator
|
||||
}
|
||||
|
||||
@@ -235,6 +238,7 @@ private struct ProjectRow: Codable, FetchableRecord, PersistableRecord {
|
||||
var worktree_base: String?
|
||||
var setup_script: String?
|
||||
var setup_policy: String
|
||||
var sandbox_config: String?
|
||||
var created_at: Date
|
||||
|
||||
init(_ p: Project) {
|
||||
@@ -246,6 +250,9 @@ private struct ProjectRow: Codable, FetchableRecord, PersistableRecord {
|
||||
worktree_base = p.worktreeBase
|
||||
setup_script = p.setupScript
|
||||
setup_policy = p.setupPolicy.rawValue
|
||||
sandbox_config = p.sandbox.flatMap { sandbox in
|
||||
(try? JSONEncoder().encode(sandbox)).map { String(decoding: $0, as: UTF8.self) }
|
||||
}
|
||||
created_at = p.createdAt
|
||||
}
|
||||
|
||||
@@ -259,6 +266,9 @@ private struct ProjectRow: Codable, FetchableRecord, PersistableRecord {
|
||||
worktreeBase: worktree_base,
|
||||
setupScript: setup_script,
|
||||
setupPolicy: SetupPolicy(rawValue: setup_policy) ?? .block,
|
||||
sandbox: sandbox_config.flatMap { json in
|
||||
try? JSONDecoder().decode(ProjectSandbox.self, from: Data(json.utf8))
|
||||
},
|
||||
createdAt: created_at)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user