← All courses ← Course home

Lesson 03 of 05

Write one isolated executor

Every tool calls one executor. That type owns file, network, and long work. Tools do not call FileManager themselves.

Agent brief (llms.md)

An isolated executor is the only type allowed to touch files, network, or long work. Tools call the executor. They do not call FileManager or URLSession directly. That gives you one place to enforce the box.

Tool.callArguments
ExecutorPolicy check
WorkFile, net, or compute
struct ToolRequest: Sendable {
    var name: String
    var fileIDs: [String]
    var url: URL?
    var work: @Sendable () async throws -> String
}

enum ExecutorError: Error {
    case unknownTool
    case fileNotAllowed
    case networkDenied
    case timeout
}

actor ToolExecutor {
    var allowedFiles: Set<String>
    var networkAllowed: Set<String>
    var timeout: Duration = .seconds(8)

    func run(_ request: ToolRequest) async throws -> String {
        guard allowedFiles.isSuperset(of: request.fileIDs) else {
            throw ExecutorError.fileNotAllowed
        }
        if let url = request.url, !networkAllowed.contains(request.name) {
            throw ExecutorError.networkDenied
        }
        return try await withThrowingTaskGroup(of: String.self) { group in
            group.addTask { try await request.work() }
            group.addTask {
                try await Task.sleep(for: timeout)
                throw ExecutorError.timeout
            }
            let value = try await group.next()!
            group.cancelAll()
            return value
        }
    }
}

On macOS, work can be an XPC call instead of local file code. The executor still checks the allow-list first. The helper should refuse paths the UI process did not send as security-scoped bookmarks.

#if os(macOS)
func readInHelper(_ bookmark: Data) async throws -> String {
    let connection = NSXPCConnection(serviceName: "dev.edgefde.ToolHelper")
    connection.remoteObjectInterface = NSXPCInterface(with: ToolHelperProtocol.self)
    connection.resume()
    // Call the helper through the typed protocol. The helper opens the bookmark.
    return try await HelperClient(connection: connection).read(bookmark)
}
#endif
ExecutorAllow-list
XPCTyped call
HelperFewer rights

Confirm XPC API names in the installed SDK. The sketch shows the shape: a named service, a protocol, and a bookmark. It is not a full helper project.

Read Talk to tools with MCP, Stop prompt attacks and leaks, and Make several agents agree first if the request arguments themselves are the attack.

Rule. If a tool can open a file without the executor, the box is optional and will be skipped.

Next, set limits the executor always applies.

Key concepts

  • The executor actor is the only type that touches files, network, or long work.
  • A request carries name, file ids, optional url, and a work closure.
  • Errors include unknown tool, file not allowed, network denied, and timeout.
  • Timeout by racing sleep in a task group, then cancel.

Takeaways

  • Tools call the executor, never FileManager or URLSession directly.
  • One executor gives one enforcement point.
  • If a tool can open a file without the executor, the box is optional.