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.
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)
}
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.
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: .defaulton the session.
Takeaways
- One door into the model: UI and tools go through middleware.
- Any raw paste straight to
session.respondmeans middleware is not in production. - Your gate covers injection and PII. Apple's default stays as a second layer.