Workspace is a shell-agnostic Swift package for building agent and tool runtimes around a controlled filesystem model.
It gives you:
- virtual filesystem abstractions
- rooted and jailed disk access
- in-memory filesystems
- copy-on-write overlays
- mounted multi-root workspaces
- explicit permission checks for file operations
- a typed
Workspaceactor for reading, writing, walking trees, and applying batched edits
Workspace is beta software and should be used at your own risk. It is useful for app and agent workflows, but it is not a hardened sandbox or a security boundary by itself.
Many agent and tooling flows need more than plain disk I/O:
- one isolated workspace per task
- a shared scratch or memory area
- the ability to read a real project without writing back to it
- explicit approvals before reads or writes
- tree summaries, JSON helpers, and batched edits without shell parsing
Workspace provides one model for those cases. You can back it with memory, a rooted directory on disk, an overlay snapshot, or a mounted combination of several filesystems.
Workspace: high-level actor API for common file operations and batch editsWorkspaceFilesystem: low-level protocol for custom filesystem backendsReadWriteFilesystem: real disk access rooted to a configured directoryInMemoryFilesystem: fully in-memory filesystem for isolated sessions and testsOverlayFilesystem: snapshot a disk root and keep writes in memoryMountableFilesystem: compose multiple filesystems under one virtual treePermissionedWorkspaceFilesystem: wrap any filesystem with operation-level approvalsSandboxFilesystem: convenience wrapper for app sandbox rootsSecurityScopedFilesystem: security-scoped URL and bookmark-backed accessWorkspacePath: path normalization and joining helpers
Until this package is published to a remote, use it as a local SwiftPM dependency:
.dependencies: [
.package(path: "../Workspace")
],
.targets: [
.target(
name: "YourTarget",
dependencies: ["Workspace"]
)
]import Workspace
let filesystem = InMemoryFilesystem()
let workspace = Workspace(filesystem: filesystem)
try await workspace.writeFile("/notes/todo.txt", content: "ship it")
let text = try await workspace.readFile("/notes/todo.txt")
print(text) // ship itimport Workspace
struct Config: Codable {
var name: String
var enabled: Bool
}
let filesystem = InMemoryFilesystem()
let workspace = Workspace(filesystem: filesystem)
try await workspace.writeJson("/config.json", value: Config(name: "demo", enabled: true))
let config = try await workspace.readJson("/config.json", as: Config.self)
print(config.enabled) // trueUse ReadWriteFilesystem when you want real file access under one root:
import Foundation
import Workspace
let root = URL(fileURLWithPath: "/tmp/demo-workspace", isDirectory: true)
let filesystem = try ReadWriteFilesystem(rootDirectory: root)
let workspace = Workspace(filesystem: filesystem)
try await workspace.mkdir("/src")
try await workspace.writeFile("/src/main.swift", content: "print(\"hello\")\n")Use OverlayFilesystem when you want to read a real project but keep writes isolated in memory:
import Foundation
import Workspace
let projectRoot = URL(fileURLWithPath: "/path/to/project", isDirectory: true)
let filesystem = try OverlayFilesystem(rootDirectory: projectRoot)
let workspace = Workspace(filesystem: filesystem)
let preview = try await workspace.summarizeTree("/Sources", maxDepth: 2)
try await workspace.writeFile("/SCRATCH.md", content: "overlay-only change\n")Use MountableFilesystem to combine isolated roots and shared state in one virtual tree:
import Workspace
let workspaceA = InMemoryFilesystem()
let workspaceB = InMemoryFilesystem()
let sharedMemory = InMemoryFilesystem()
let mounted = MountableFilesystem(
base: InMemoryFilesystem(),
mounts: [
.init(mountPoint: "/workspace-a", filesystem: workspaceA),
.init(mountPoint: "/workspace-b", filesystem: workspaceB),
.init(mountPoint: "/memory", filesystem: sharedMemory),
]
)
let workspace = Workspace(filesystem: mounted)
try await workspace.writeFile("/memory/plan.txt", content: "shared notes")
try await workspace.cp("/memory/plan.txt", "/workspace-a/plan.txt")Use PermissionedWorkspaceFilesystem when the host should decide which operations are allowed:
import Workspace
let base = InMemoryFilesystem()
let filesystem = PermissionedWorkspaceFilesystem(
base: base,
authorizer: WorkspacePermissionAuthorizer { request in
switch request.operation {
case .readFile, .listDirectory, .stat:
return .allowForSession
default:
return .deny(message: "write access denied")
}
}
)
let workspace = Workspace(filesystem: filesystem)Workspace includes a typed edit API for tool-driven mutations:
let result = try await workspace.applyEdits([
.createDirectory(path: "/src"),
.writeFile(path: "/src/a.txt", content: "one"),
.appendFile(path: "/src/a.txt", content: " two"),
.copy(from: "/src/a.txt", to: "/src/b.txt"),
])You can also preview text replacements without mutating files:
let preview = try await workspace.replaceInFiles(
"/src/*.txt",
search: "foo",
replacement: "bar",
dryRun: true
)InMemoryFilesystemis ready to use immediately after initialization. Callreset()when you explicitly want to clear it.OverlayFilesystemsnapshots a real root into memory. Callreload()when you explicitly want to discard overlay edits and rebuild from disk.ReadWriteFilesystemandOverlayFilesystemnormalize paths and enforce a rooted/jail model.PermissionedWorkspaceFilesystemsees normalized virtual paths, not raw user input paths.walkTreeandsummarizeTreereturn stable path ordering, which is useful for deterministic tool output.
Workspaceis not a hardened sandbox.applyEditsandreplaceInFilesuse best-effort logical rollback, not atomic transactions.- Rollback is not crash-safe and does not coordinate with external processes.
OverlayFilesystemdoes not persist writes back to the original root.- Hard links across mounts are not supported.
- Some mutable filesystem implementations are
@unchecked Sendable; sharing one mutable filesystem instance across many independent actors or tasks should be done carefully.
- Jail and root enforcement belong to the underlying filesystem implementation.
- Permission checks are additive. They do not replace path normalization or jail enforcement.
- If you expose
Workspaceto model-driven or remote callers, the host still needs to define what roots, mounts, and permissions are acceptable.
swift test