The box only works if Tool.call cannot go around it. Inject the executor. Keep file and network code out of the tool type.
SessionMay call the tool
Tool.callBuild a request
ExecutorRun or refuse
import FoundationModels
struct ReadOrderNote: Tool {
let name = "read_order_note"
let description = "Read a note the app already stored for an order id."
let executor: ToolExecutor
let store: OrderStore
@Generable
struct Arguments {
var orderId: String
}
func call(arguments: Arguments) async throws -> String {
let fileID = try store.noteFileID(for: arguments.orderId)
let request = ToolRequest(
name: name,
fileIDs: [fileID],
url: nil,
work: { try await self.store.readNote(fileID: fileID) }
)
return try await executor.run(request)
}
}
let session = LanguageModelSession(
model: SystemLanguageModel.default,
tools: [ReadOrderNote(executor: executor, store: store)],
instructions: "Use read_order_note only with an order id the user gave."
)
Checklist
- Assume in-process tools share the app sandbox.
- On macOS, move risky work to an XPC service with fewer rights when you need a second process.
- On iOS, do not promise a general helper binary. Use allow-lists and extensions Apple already defines.
- Give every tool one executor. Ban direct
FileManagerandURLSessionin tool types. - Timeout, cap bytes, resolve files in the app, deny network by default.
- Run argument checks from the guardrail course before the executor starts work.
Primary sources: Foundation Models, XPC, and App Sandbox.
Related courses: Talk to tools with MCP, Stop prompt attacks and leaks, and Make several agents agree first.
Short form. The model calls your tool. Your tool calls the executor. The executor is the box.
Key concepts
- Inject the executor into each
Tool. The session may call. The executor may refuse. - Ban direct
FileManagerandURLSessionin tool types. - Run guardrail argument checks before the executor starts work.
- Use macOS XPC when you need a second process. Use iOS allow-lists when you do not.
Takeaways
- The model calls your tool. Your tool calls the executor. The executor is the box.
- Every
Tool.callmust go through the executor with no bypass. - Keep the honest OS story in the shipping notes.