← All courses ← Course home

Lesson 02 of 05

Put middleware in front of the session

One PromptGate type checks and redacts text before respond and before a tool returns. Apple's Guardrails.default stays on the session.

Agent brief (llms.md)

Middleware is a Swift type your app calls instead of talking to the session or a tool directly. The view sends user text here. Tools send their arguments here. The model never sees a string this type has not passed.

UI or toolRaw string
MiddlewareCheck and redact
Sessionrespond
import FoundationModels

enum GuardError: Error {
    case blocked(String)
}

struct PromptGate: Sendable {
    var injection: InjectionChecker
    var redactor: PIIRedactor

    func prepareForModel(_ text: String, source: TextSource) throws -> String {
        if let reason = injection.blockReason(text, source: source) {
            throw GuardError.blocked(reason)
        }
        return redactor.redact(text, source: source)
    }

    func respond(_ session: LanguageModelSession, to raw: String, source: TextSource) async throws -> String {
        let prompt = try prepareForModel(raw, source: source)
        let reply = try await session.respond(to: prompt)
        return redactor.redact(reply.content, source: .modelOutput)
    }
}

enum TextSource: String {
    case userTyped
    case pasted
    case toolResult
    case modelOutput
}

Call the same prepareForModel from Tool.call before you run work, and again on tool output before you return it to the session. A tool result is untrusted text. It can carry an injection back into the next turn.

func call(arguments: Arguments) async throws -> String {
    let cleaned = try gate.prepareForModel(arguments.query, source: .pasted)
    let raw = try await store.search(cleaned)
    return try gate.prepareForModel(raw, source: .toolResult)
}
User or pasteprepareForModel
Tool outputprepareForModel
Model replyRedact before UI

Keep Apple's guardrails: .default on the session. Your type does not replace it. Your type covers injection and PII, which that default does not document as its job.

Read Run tools in a safe box, Talk to tools with MCP, and Make several agents agree first so a passed prompt still cannot open every file, and so a tool list from MCP is still just data.

Rule. If any code path calls session.respond with a raw paste, the middleware is not in production.

Next, write the injection checks.

Key concepts

  • One gate owns the injection checker and the PII redactor.
  • Prepare text for the model, then wrap session.respond.
  • Use the same prepare path on tool arguments and tool results.
  • Keep guardrails: .default on the session.

Takeaways

  • One door into the model: UI and tools go through middleware.
  • Any raw paste straight to session.respond means middleware is not in production.
  • Your gate covers injection and PII. Apple's default stays as a second layer.