Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ Then add the product to your target:
- `JavaScriptExecutionCall.result`
- `JavaScriptExecutionCall.cancel()`
- `CodeModeToolError`
- `CodeModeConfiguration`
- `CodeModeFileSystem`
- `LocalCodeModeFileSystem`
- `CodeModeAgentToolDescriptions`

## Quick Start
Expand Down Expand Up @@ -103,6 +106,20 @@ let result = try await call.result
print(result.output ?? .null)
```

## Filesystem Integration

CodeMode keeps its JavaScript filesystem API stable while allowing hosts to provide the underlying operations:

```swift
let tools = CodeModeAgentTools(
config: CodeModeConfiguration(
fileSystem: MyCodeModeFileSystem()
)
)
```

`CodeModeFileSystem` receives paths after `PathPolicy` resolution, so sandbox root enforcement stays in CodeMode while the host can route reads, writes, listings, moves, copies, deletes, and stats through another backing implementation. `LocalCodeModeFileSystem` preserves the default `FileManager` behavior.

## Search

`searchJavaScriptAPI` accepts `JavaScriptAPISearchRequest`:
Expand Down
3 changes: 3 additions & 0 deletions Sources/CodeMode/API/BridgeModels.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,20 @@ import Foundation

public struct CodeModeConfiguration: Sendable {
public var pathPolicy: any PathPolicy
public var fileSystem: any CodeModeFileSystem
public var artifactStore: any ArtifactStore
public var permissionBroker: any PermissionBroker
public var auditLogger: any AuditLogger

public init(
pathPolicy: any PathPolicy = DefaultPathPolicy(),
fileSystem: any CodeModeFileSystem = LocalCodeModeFileSystem(),
artifactStore: any ArtifactStore = InMemoryArtifactStore(),
permissionBroker: any PermissionBroker = SystemPermissionBroker(),
auditLogger: any AuditLogger = SyncAuditLogger()
) {
self.pathPolicy = pathPolicy
self.fileSystem = fileSystem
self.artifactStore = artifactStore
self.permissionBroker = permissionBroker
self.auditLogger = auditLogger
Expand Down
124 changes: 124 additions & 0 deletions Sources/CodeMode/Bridges/CodeModeFileSystem.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import Foundation

public struct CodeModeFileSystemEntry: Sendable, Equatable {
public var name: String
public var path: String
public var isDirectory: Bool
public var size: Int

public init(name: String, path: String, isDirectory: Bool, size: Int) {
self.name = name
self.path = path
self.isDirectory = isDirectory
self.size = size
}
}

public struct CodeModeFileSystemAttributes: Sendable, Equatable {
public var isDirectory: Bool
public var size: Int
public var creationDate: Date?
public var modificationDate: Date?

public init(isDirectory: Bool, size: Int, creationDate: Date?, modificationDate: Date?) {
self.isDirectory = isDirectory
self.size = size
self.creationDate = creationDate
self.modificationDate = modificationDate
}
}

public struct CodeModeFileSystemAccess: Sendable, Equatable {
public var readable: Bool
public var writable: Bool

public init(readable: Bool, writable: Bool) {
self.readable = readable
self.writable = writable
}
}

public protocol CodeModeFileSystem: Sendable {
func listDirectory(at url: URL) throws -> [CodeModeFileSystemEntry]
func readData(at url: URL) throws -> Data
func writeData(_ data: Data, to url: URL) throws
func moveItem(at sourceURL: URL, to destinationURL: URL) throws
func copyItem(at sourceURL: URL, to destinationURL: URL) throws
func removeItem(at url: URL) throws
func attributesOfItem(at url: URL) throws -> CodeModeFileSystemAttributes
func createDirectory(at url: URL, recursive: Bool) throws
func itemExists(at url: URL) -> Bool
func access(at url: URL) -> CodeModeFileSystemAccess
}

public final class LocalCodeModeFileSystem: CodeModeFileSystem, @unchecked Sendable {
private let fileManager: FileManager

public init(fileManager: FileManager = .default) {
self.fileManager = fileManager
}

public func listDirectory(at url: URL) throws -> [CodeModeFileSystemEntry] {
let values = try fileManager.contentsOfDirectory(
at: url,
includingPropertiesForKeys: [.isDirectoryKey, .fileSizeKey],
options: [.skipsHiddenFiles]
)

return try values.map { item in
let resourceValues = try item.resourceValues(forKeys: [.isDirectoryKey, .fileSizeKey])
return CodeModeFileSystemEntry(
name: item.lastPathComponent,
path: item.path,
isDirectory: resourceValues.isDirectory ?? false,
size: resourceValues.fileSize ?? 0
)
}
}

public func readData(at url: URL) throws -> Data {
try Data(contentsOf: url)
}

public func writeData(_ data: Data, to url: URL) throws {
try data.write(to: url, options: .atomic)
}

public func moveItem(at sourceURL: URL, to destinationURL: URL) throws {
try fileManager.moveItem(at: sourceURL, to: destinationURL)
}

public func copyItem(at sourceURL: URL, to destinationURL: URL) throws {
try fileManager.copyItem(at: sourceURL, to: destinationURL)
}

public func removeItem(at url: URL) throws {
try fileManager.removeItem(at: url)
}

public func attributesOfItem(at url: URL) throws -> CodeModeFileSystemAttributes {
let attrs = try fileManager.attributesOfItem(atPath: url.path)
let type = attrs[.type] as? FileAttributeType
return CodeModeFileSystemAttributes(
isDirectory: type == .typeDirectory,
size: (attrs[.size] as? NSNumber)?.intValue ?? 0,
creationDate: attrs[.creationDate] as? Date,
modificationDate: attrs[.modificationDate] as? Date
)
}

public func createDirectory(at url: URL, recursive: Bool) throws {
try fileManager.createDirectory(at: url, withIntermediateDirectories: recursive)
}

public func itemExists(at url: URL) -> Bool {
fileManager.fileExists(atPath: url.path)
}

public func access(at url: URL) -> CodeModeFileSystemAccess {
CodeModeFileSystemAccess(
readable: fileManager.isReadableFile(atPath: url.path),
writable: fileManager.isWritableFile(atPath: url.path)
)
}
}
6 changes: 4 additions & 2 deletions Sources/CodeMode/Bridges/DefaultCapabilityLoader.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import Foundation

public enum DefaultCapabilityLoader {
public static func loadAllRegistrations() -> [CapabilityRegistration] {
let fs = FileSystemBridge()
public static func loadAllRegistrations(
fileSystem: any CodeModeFileSystem = LocalCodeModeFileSystem()
) -> [CapabilityRegistration] {
let fs = FileSystemBridge(fileSystem: fileSystem)
let network = NetworkBridge()
let keychain = KeychainBridge()
let location = LocationBridge()
Expand Down
69 changes: 34 additions & 35 deletions Sources/CodeMode/Bridges/FileSystemBridge.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import Foundation

public final class FileSystemBridge: @unchecked Sendable {
private let fileManager: FileManager
private let fileSystem: any CodeModeFileSystem

public init(fileManager: FileManager = .default) {
self.fileManager = fileManager
public init(fileSystem: any CodeModeFileSystem = LocalCodeModeFileSystem()) {
self.fileSystem = fileSystem
}

public convenience init(fileManager: FileManager) {
self.init(fileSystem: LocalCodeModeFileSystem(fileManager: fileManager))
}

public func list(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue {
Expand All @@ -13,14 +17,12 @@ public final class FileSystemBridge: @unchecked Sendable {
}

let url = try context.pathPolicy.resolve(path: path)
let values = try fileManager.contentsOfDirectory(at: url, includingPropertiesForKeys: [.isDirectoryKey, .fileSizeKey], options: [.skipsHiddenFiles])
let entries: [JSONValue] = try values.map { item in
let resourceValues = try item.resourceValues(forKeys: [.isDirectoryKey, .fileSizeKey])
let entries: [JSONValue] = try fileSystem.listDirectory(at: url).map { item in
return .object([
"name": .string(item.lastPathComponent),
"name": .string(item.name),
"path": .string(item.path),
"isDirectory": .bool(resourceValues.isDirectory ?? false),
"size": .number(Double(resourceValues.fileSize ?? 0)),
"isDirectory": .bool(item.isDirectory),
"size": .number(Double(item.size)),
])
}

Expand All @@ -34,7 +36,7 @@ public final class FileSystemBridge: @unchecked Sendable {

let encoding = arguments.string("encoding") ?? "utf8"
let url = try context.pathPolicy.resolve(path: path)
let data = try Data(contentsOf: url)
let data = try fileSystem.readData(at: url)

switch encoding.lowercased() {
case "utf8", "utf-8":
Expand Down Expand Up @@ -62,7 +64,7 @@ public final class FileSystemBridge: @unchecked Sendable {
let url = try context.pathPolicy.resolve(path: path)

let parent = url.deletingLastPathComponent()
try fileManager.createDirectory(at: parent, withIntermediateDirectories: true)
try fileSystem.createDirectory(at: parent, recursive: true)

let data: Data
switch encoding.lowercased() {
Expand All @@ -78,7 +80,7 @@ public final class FileSystemBridge: @unchecked Sendable {
throw BridgeError.invalidArguments("Unsupported encoding: \(encoding)")
}

try data.write(to: url, options: .atomic)
try fileSystem.writeData(data, to: url)
return .object([
"path": .string(url.path),
"bytesWritten": .number(Double(data.count)),
Expand All @@ -93,11 +95,11 @@ public final class FileSystemBridge: @unchecked Sendable {
let fromURL = try context.pathPolicy.resolve(path: from)
let toURL = try context.pathPolicy.resolve(path: to)

if fileManager.fileExists(atPath: toURL.path) {
try fileManager.removeItem(at: toURL)
if fileSystem.itemExists(at: toURL) {
try fileSystem.removeItem(at: toURL)
}

try fileManager.moveItem(at: fromURL, to: toURL)
try fileSystem.moveItem(at: fromURL, to: toURL)
return .object([
"from": .string(fromURL.path),
"to": .string(toURL.path),
Expand All @@ -112,11 +114,11 @@ public final class FileSystemBridge: @unchecked Sendable {
let fromURL = try context.pathPolicy.resolve(path: from)
let toURL = try context.pathPolicy.resolve(path: to)

if fileManager.fileExists(atPath: toURL.path) {
try fileManager.removeItem(at: toURL)
if fileSystem.itemExists(at: toURL) {
try fileSystem.removeItem(at: toURL)
}

try fileManager.copyItem(at: fromURL, to: toURL)
try fileSystem.copyItem(at: fromURL, to: toURL)
return .object([
"from": .string(fromURL.path),
"to": .string(toURL.path),
Expand All @@ -130,17 +132,17 @@ public final class FileSystemBridge: @unchecked Sendable {

let recursive = arguments.bool("recursive") ?? false
let url = try context.pathPolicy.resolve(path: path)
var isDirectory: ObjCBool = false

guard fileManager.fileExists(atPath: url.path, isDirectory: &isDirectory) else {
guard fileSystem.itemExists(at: url) else {
return .object(["deleted": .bool(false), "path": .string(url.path)])
}

if isDirectory.boolValue, recursive == false {
let attrs = try fileSystem.attributesOfItem(at: url)
if attrs.isDirectory, recursive == false {
throw BridgeError.invalidArguments("fs.delete requires recursive=true for directories")
}

try fileManager.removeItem(at: url)
try fileSystem.removeItem(at: url)
return .object(["deleted": .bool(true), "path": .string(url.path)])
}

Expand All @@ -150,16 +152,14 @@ public final class FileSystemBridge: @unchecked Sendable {
}

let url = try context.pathPolicy.resolve(path: path)
let attrs = try fileManager.attributesOfItem(atPath: url.path)
let type = attrs[.type] as? FileAttributeType
let isDirectory = type == .typeDirectory
let attrs = try fileSystem.attributesOfItem(at: url)

return .object([
"path": .string(url.path),
"isDirectory": .bool(isDirectory),
"size": .number(Double((attrs[.size] as? NSNumber)?.intValue ?? 0)),
"createdAt": .string((attrs[.creationDate] as? Date)?.ISO8601Format() ?? ""),
"modifiedAt": .string((attrs[.modificationDate] as? Date)?.ISO8601Format() ?? ""),
"isDirectory": .bool(attrs.isDirectory),
"size": .number(Double(attrs.size)),
"createdAt": .string(attrs.creationDate?.ISO8601Format() ?? ""),
"modifiedAt": .string(attrs.modificationDate?.ISO8601Format() ?? ""),
])
}

Expand All @@ -170,7 +170,7 @@ public final class FileSystemBridge: @unchecked Sendable {

let recursive = arguments.bool("recursive") ?? true
let url = try context.pathPolicy.resolve(path: path)
try fileManager.createDirectory(at: url, withIntermediateDirectories: recursive)
try fileSystem.createDirectory(at: url, recursive: recursive)

return .object(["created": .bool(true), "path": .string(url.path)])
}
Expand All @@ -181,7 +181,7 @@ public final class FileSystemBridge: @unchecked Sendable {
}

let url = try context.pathPolicy.resolve(path: path)
return .bool(fileManager.fileExists(atPath: url.path))
return .bool(fileSystem.itemExists(at: url))
}

public func access(arguments: [String: JSONValue], context: BridgeInvocationContext) throws -> JSONValue {
Expand All @@ -190,11 +190,10 @@ public final class FileSystemBridge: @unchecked Sendable {
}

let url = try context.pathPolicy.resolve(path: path)
let readable = fileManager.isReadableFile(atPath: url.path)
let writable = fileManager.isWritableFile(atPath: url.path)
let access = fileSystem.access(at: url)
return .object([
"readable": .bool(readable),
"writable": .bool(writable),
"readable": .bool(access.readable),
"writable": .bool(access.writable),
"path": .string(url.path),
])
}
Expand Down
2 changes: 1 addition & 1 deletion Sources/CodeMode/Host/CodeModeAgentTools.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public final class CodeModeAgentTools: @unchecked Sendable {

public init(config: CodeModeConfiguration = .init()) {
let registrations = CapabilityPlatformSupport.filter(
DefaultCapabilityLoader.loadAllRegistrations(),
DefaultCapabilityLoader.loadAllRegistrations(fileSystem: config.fileSystem),
for: .current
)
let registry = CapabilityRegistry(registrations: registrations)
Expand Down
Loading
Loading