← All courses ← Course home

Lesson 05 of 05

Put the box behind every tool

Inject the executor into each Tool. The session can ask. The executor can still refuse.

Agent brief (llms.md)

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

  1. Assume in-process tools share the app sandbox.
  2. On macOS, move risky work to an XPC service with fewer rights when you need a second process.
  3. On iOS, do not promise a general helper binary. Use allow-lists and extensions Apple already defines.
  4. Give every tool one executor. Ban direct FileManager and URLSession in tool types.
  5. Timeout, cap bytes, resolve files in the app, deny network by default.
  6. 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 FileManager and URLSession in 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.call must go through the executor with no bypass.
  • Keep the honest OS story in the shipping notes.