Core AI model zoo

Agentic security: a pre-ship checklist for local-LLM agent apps

Hard-won notes for shipping an on-device LLM agent (FM framework / App Intents) safely. Distilled from WWDC26 347 “Secure your app: mitigate risks to agentic features” (verbatim: ondevice/_wwdc347_transcript.txt) and the confirmation/ownership APIs in 343 “Explore advanced App Intents features” (ondevice/_wwdc343_transcript.txt). Written for the Vault app (Wave 2) but applies to any zoo model put behind LanguageModelSession / Siri.

⚠️ API names below are as spoken in the talks — captions don’t capture on-screen code, so confirm exact spelling/signatures (.onToolCall, .historyTransform, authenticationPolicy, OwnershipProvidingEntity, IntentDonationManager) against the developer docs before relying on them. The concepts are verbatim-confirmed; the Swift symbols are best-effort transcriptions.


1. The one sentence that matters

Indirect prompt injection is an unsolved research problem. Apple’s own framing (347): “our best approach at the moment is to understand how much your app is at risk, and aim to mitigate that risk.”

So this is not a “turn on a flag and you’re safe” page. It is a threat-modeling exercise plus a catalog of deterministic mitigations you bolt onto specific points in the agent loop.

2. The risk model (347)

Indirect prompt injection = instructions embedded in extra context (not the user’s prompt) that redirect the agent’s control flow. The context can arrive in the initial prompt or in a tool result.

Two distinct effects once an injection lands:

The Lethal Trifecta (Simon Willison, cited verbatim in 347). Maximum danger when an agent simultaneously has all three:

  1. access to private data,
  2. exposure to untrusted content,
  3. the ability to externally communicate — generalize this to any action with a side effect.

Vault is born inside the Lethal Trifecta. Its whole pitch is (1) your private notes/files/photos, and it will have (2) untrusted content (a note you saved from a web clip, a shared file, an email) and (3) side-effecting tools (send, post, delete, order). Treat the trifecta as Vault’s default state, not an edge case. The design lever is to break one leg per risky flow (e.g. redact private data before it reaches a tool that can communicate; or gate the communicating tool behind confirmation).

Out of scope for 347 (and for this page): model safety (is the output itself harmful) and guardrail circumvention. This is about an external attacker subverting your app.

3. The threat-modeling exercise (do this once per feature)

Step A — Data-flow analysis of the prompt

List every source that feeds prompt construction. For Vault that’s: system instructions, the user prompt, and extra context — retrieved notes, file contents, calendar events, a “friend feed”/shared items, and every tool result.

Step B — Mark what is untrusted

Rule of thumb (347): anything from an external entity is attack surface. A calendar invite a stranger sent you, a post on a feed, a shared document, the body of an inbound email, OCR text from an image — all untrusted. Your own first-party UI input is the only thing that starts trusted.

Step C — Enumerate actions and their side-effect risk

For each tool/intent, name the worst case:

Side-effect class Example (347) Vault analogue
Financial OrderTeaTool (lose money) any purchase/booking tool
Data exfiltration PostAndFetchPublicFeedTool (leak via public post) share / send / export / “post”
Data loss DeletePhoto (no undo) delete note / file / event
Stored / second-order BrewingTimerIntent label — injection writes instructions that a later “list timers” pulls back into context any tool that persists a model-controlled string that is later re-read (tags, titles, notes, reminders)

The stored-injection row is the sneaky one. 347’s createTimer example: a tool that looks harmless (no side effect) but takes an optional String label the model fills in. An injection sets the label to attacker text; a later “list timers” query reads it back → context poisoning across turns. Vault must audit every place the model writes a string that is later read back into a prompt.

4. Mitigations — deterministic first

347 is explicit: prefer deterministic mitigations as the baseline (“their security guarantees are easier to audit and reason about”); use probabilistic ones as defense-in-depth, not as the only line.

Prompt-level (cut the injection’s fuel and flag it)

Action-level (gate the dangerous verbs)

5. The concrete APIs

5a. Foundation Models framework — lifecycle event modifiers (347)

Deterministic callbacks fired at fixed points in the session loop. Two that matter for security:

The framework ships more modifiers and lets you package custom ones as reusable profile modifiers.

5b. App Intents — when the model is Siri, not your own loop (347 + 343)

When an App Intent adopts a schema it becomes a tool in Siri’s toolbox, and the model picks which intent to call — so injection can misuse it. The system gives you guardrails:

6. Pre-ship checklist (tick every box before TestFlight)

Threat model

Prompt-level (deterministic baseline)

Action-level

Verification (close the loop with Evaluations — see evaluations-framework.md)

7. Vault design implications (concrete)

  1. Default-deny side effects. Vault’s read/ask path (Spotlight RAG, embeddings, VL) is low-risk and can run freely. Every write/send/delete/order goes through a single .onToolCall gate with confirmation. Build it as one reusable profile modifier so all current and future tools inherit it.
  2. A redaction .historyTransform on the retrieval path. Vault reads the user’s private corpus into context constantly — strip secrets (tokens, full card numbers, addresses) before the model sees them, so a “summarize and share” can’t leak what was never present. Re-apply per iteration; cache with @SessionProperty if it’s expensive.
  3. Spotlight untrusted corpus items. Notes clipped from the web, shared files, inbound mail = untrusted. Tag them in .historyTransform; keep first-party typed input untagged.
  4. Lock-screen posture. If Vault exposes Siri intents (“ask Vault…”), mark anything that exports/deletes requiresAuthentication. The “ask my notes a question” intent can stay lock-screen-friendly; “share this note” cannot.
  5. The PCC/cloud profile is a privacy boundary, made visible. Sending the user’s private corpus to PCC (or any server model) is external communication = trifecta leg #3. Keep it an explicit opt-in profile (DynamicProfile), show it in the UI, and prefer redaction before any off-device hop. (Aligns with the track’s Value 4: “privacy by architecture.”)
  6. Ship the injection regression eval with the app. Per Value 1 (“measure before you claim”), Vault’s Evaluations suite should include a poisoned-context trajectory test as a permanent gate, not a one-off.

8. Pointers