Skip to content

Commit 4c4f607

Browse files
committed
docs(learnings): tool security, between-assistant data flow, variable-bag trust tiers
Captures the data-flow / threat-model framing that came out of Mudflap progressive caller-ID auth work (PRISM-528): - tools.md: new "Tool Security and Data Visibility" section — every tool result is in conversation history; static `parameters` is the only LLM-invisibility primitive (and the security-boundary semantics behind the existing override behavior); naming footgun between `tool.function.parameters` and the top-level `tool.parameters`; tool types that support static parameters; Mudflap progressive auth pattern as a worked example. - squads.md: extends the contextEngineeringPlan table with the previously missing `previousAssistantMessages` row (the only mode that scrubs current-assistant tool-call data at handoff — the PCI primitive). New "Passing data between assistants" section with a three-approach trust matrix (handoff arguments, variableExtractionPlan.schema, Liquid in destination prompt) and a sanitization subsection. - assistants.md: new "Liquid Variable Bag and Trust Tiers" section classifying every Liquid variable into Server-trusted (Tier 1, safe as a security boundary), Conversation-derived (Tier 2), and LLM/extraction-derived (Tier 3). Includes the {{ "now" }} timezone conversion gotcha (use the literal "now" string, not the variable). Cross-references between the three files keep readers on the right trust-tier table when reading any of them. All three sections also link out to the canonical docs.vapi.ai pages (some pending merge in the public docs repo; the URLs are forward-compatible). Refs: PRISM-528
1 parent abb294d commit 4c4f607

3 files changed

Lines changed: 155 additions & 0 deletions

File tree

docs/learnings/assistants.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,54 @@ They are merged, not mutually exclusive. But be aware of potential duplicates.
535535

536536
---
537537

538+
## Liquid Variable Bag and Trust Tiers
539+
540+
Cross-reference: [docs.vapi.ai/assistants/dynamic-variables](https://docs.vapi.ai/assistants/dynamic-variables). The trust-tier framing came out of Mudflap progressive-auth work (PRISM-528).
541+
542+
Vapi exposes a Liquid templating layer in prompts, tool config, and overrides — `{{ customer.number }}`, `{{ now }}`, etc. The variables in scope at runtime fall into three trust tiers based on where they originate. This matters because anything you place in a security-sensitive field (tool static `parameters`, message templates that go to a backend) is only as trustworthy as the source of the variable.
543+
544+
### Tier 1 — Server-trusted (safe for static `parameters` as a security boundary)
545+
546+
Populated from signaling, validated config, validated API call payloads, or the server clock. The LLM has no write path to these mid-conversation.
547+
548+
| Variable | Source |
549+
|---|---|
550+
| `{{ customer.number }}`, `{{ customer.sipUri }}` | SIP / Twilio signaling (inbound) or validated outbound API payload |
551+
| `{{ customer.name }}`, `{{ customer.email }}`, `{{ customer.extension }}` | Validated outbound API payload (only if you set them server-side) |
552+
| `{{ phoneNumber.number }}` | The Vapi number that placed/received the call |
553+
| `{{ call.id }}`, `{{ call.type }}`, `{{ call.startedAt }}` | Server-set call state |
554+
| `{{ now }}`, `{{ date }}`, `{{ time }}`, `{{ year }}`, `{{ month }}`, `{{ day }}` | Server clock at fulfill time |
555+
| Custom keys set in `assistantOverrides.variableValues` at call start | Validated API call payload |
556+
557+
### Tier 2 — Conversation-derived (NOT a security boundary)
558+
559+
| Variable | Why unsafe |
560+
|---|---|
561+
| `{{ messages }}`, `{{ transcript }}` | Includes raw user transcripts |
562+
| `{{ prompt }}` | Trusted at call-start, but pollutes if you template user input into it |
563+
564+
### Tier 3 — LLM- or extraction-derived (NEVER a security boundary)
565+
566+
| Variable | Why |
567+
|---|---|
568+
| `variableExtractionPlan` aliases | Only as trusted as the tool that produced them. An alias keyed on `{{ customer.number }}` is safe; one extracted from a tool whose response was shaped by user-spoken input is not. |
569+
| Handoff-tool-extracted variables (`variableExtractionPlan.schema` on a handoff destination) | LLM extraction pass against the transcript |
570+
| Handoff arguments (`function.parameters` on a handoff tool) | LLM-filled |
571+
572+
For the security-boundary use of Tier 1 variables in tool config, see [tools.md → Static `parameters` is the LLM-invisibility primitive](tools.md#static-parameters-is-the-llm-invisibility-primitive-security-boundary). For how the variable bag persists across squad handoffs, see [squads.md → Passing data between assistants](squads.md#passing-data-between-assistants).
573+
574+
### `{{ now }}` is UTC, hardcoded — use the `"now"` literal for timezone conversion
575+
576+
The `{{ now }}` variable is a pre-formatted string with " UTC" appended (e.g. `"Jan 1, 2024, 12:00 PM UTC"`). To render in another timezone, use the LiquidJS `date` filter with the literal string `"now"` — NOT the variable:
577+
578+
```liquid
579+
{{ "now" | date: "%I:%M %p", "America/Los_Angeles" }}
580+
```
581+
582+
**Common antipattern:** `{{ now | date: "...", "TZ" }}`. This pipes the pre-formatted UTC string through the filter, which fails because `date` cannot reparse Vapi's "Jan 1, 2024, 12:00 PM UTC" format reliably. The quoted `"now"` literal is the only form that works.
583+
584+
---
585+
538586
## Prompt Authoring
539587

540588
### Verbose negative-directive lists prime the banned phrases

docs/learnings/squads.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ After a handoff, the new assistant does NOT get a raw copy of all prior messages
6161
| `none` | Clean slate — no prior context | Starting fresh (e.g., language switch) |
6262
| `lastNMessages` | Last N messages kept | Partial context preservation |
6363
| `userAndAssistantMessages` | User/bot turns only (tool calls stripped) | Clean handoff without tool noise |
64+
| `previousAssistantMessages` | Only history from before the current assistant's session — current assistant's tool-call data excluded | **PCI / compliance handoff back to a general assistant** — see [Sanitizing tool-call data across assistants](#sanitizing-tool-call-data-across-assistants-pci-pattern) below |
6465

6566
### The VM detection relay pattern
6667

@@ -87,6 +88,43 @@ See [voicemail-detection.md](voicemail-detection.md) for the full two-agent rela
8788

8889
---
8990

91+
## Passing data between assistants
92+
93+
Cross-reference: [docs.vapi.ai/squads/passing-data-between-assistants](https://docs.vapi.ai/squads/passing-data-between-assistants). The trust-tier framing came out of Mudflap progressive-auth work (PRISM-528).
94+
95+
When a squad hands off mid-call, three approaches exist for getting data from one assistant to the next. They differ on trust level, latency, and determinism.
96+
97+
| Approach | Mechanism | Trust | Latency |
98+
|---|---|---|---|
99+
| **Handoff arguments** | `function.parameters` on the handoff tool. The LLM fills the arg inline with the handoff call. | LLM-derived. Use for sentiment / intent classifications. **NOT a security boundary.** | Free (already in the handoff turn) |
100+
| **`variableExtractionPlan.schema`** | A dedicated LLM extraction call against the conversation transcript at handoff time. | LLM-derived. **NOT a security boundary.** | Adds a full LLM round-trip |
101+
| **Liquid variables in the destination prompt** | The variable bag is shared across squad members for the call's lifetime. The next assistant references `{{ customer.number }}`, prior alias values, etc. directly in its prompt or its tools' static `parameters`. | **Server-trusted IF the underlying values are call-level (Tier 1).** See [assistants.md → Liquid Variable Bag and Trust Tiers](assistants.md#liquid-variable-bag-and-trust-tiers). | Sub-millisecond, deterministic |
102+
103+
**Crucial property:** call-level Liquid variables (`{{ customer.number }}`, `{{ phoneNumber.number }}`, `{{ call.id }}`, `{{ now }}`) persist across handoffs because they live on the call object, not the active assistant. The next assistant references the same trusted variable in its own tools' static `parameters` — no handoff-side configuration needed.
104+
105+
### Static config per destination: `destination.assistantOverrides.variableValues`
106+
107+
Defined on the handoff tool's destination, merged into the variable bag at handoff time, bypasses the LLM entirely. Use for per-destination static config the next assistant should know about: `{ "tier": "premium" }`, `{ "slaWindowSeconds": 30 }`.
108+
109+
**Known limitation (logged in `improvements.md`):** Liquid templates inside `destination.assistantOverrides.variableValues` are NOT currently resolved at handoff time. If you write `"verifiedCaller": "{{ customer.number }}"`, the bag holds the literal string `"{{ customer.number }}"` instead of the resolved phone number. Workaround: rely on the squad-level variable bag persistence — call-level variables are already shared across squad members for the call's lifetime, so the next assistant can reference `{{ customer.number }}` directly. Use `assistantOverrides.variableValues` only for per-destination *literal* values.
110+
111+
### Sanitizing tool-call data across assistants (PCI pattern)
112+
113+
When a privileged sub-assistant collects sensitive data via tool calls (PCI card capture, SSN lookup), control needs to hand back to a general-purpose assistant without that general assistant ever seeing those tool responses. `contextEngineeringPlan.type: previousAssistantMessages` is the only Vapi primitive that scrubs current-assistant tool-call data from the next assistant's view — it's a handoff-time redaction, not an in-assistant one.
114+
115+
```yaml
116+
# On the privileged assistant's handoff tool, returning to a general assistant
117+
destinations:
118+
- type: assistant
119+
assistantName: General Agent
120+
contextEngineeringPlan:
121+
type: previousAssistantMessages
122+
```
123+
124+
Within a single assistant there is no equivalent. `request-complete` is a speech lever, `variableExtractionPlan` aliases give determinism but not invisibility, and tool responses are always in the next-completion conversation history. If the model must not see a value, your tool server must not place it in the response body in the first place. See [tools.md → Every tool result is in conversation history](tools.md#every-tool-result-is-in-conversation-history).
125+
126+
---
127+
90128
## Override Merge Order
91129

92130
The final assistant configuration is built by merging these layers in order:

docs/learnings/tools.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,3 +293,72 @@ DTMF tones are a telephony concept. This tool has no effect on WebSocket or web-
293293
| `handoff` | **Removed** when the tool is processed |
294294

295295
Only `function` tools support `strict` mode.
296+
297+
---
298+
299+
## Tool Security and Data Visibility
300+
301+
Cross-reference: [docs.vapi.ai/tools/static-variables-and-aliases](https://docs.vapi.ai/tools/static-variables-and-aliases) and [docs.vapi.ai/tools/custom-tools](https://docs.vapi.ai/tools/custom-tools). The full data-flow / threat-model writeup that motivates this section came out of Mudflap progressive-auth work (PRISM-528).
302+
303+
### Every tool result is in conversation history
304+
305+
Every successful tool call's full response body becomes a `role: "tool"` message in the LLM's conversation history and is visible to the model on the next completion. There is no built-in flag to hide tool results from the model. Implications:
306+
307+
- If you put a sensitive value (PII, a token, a secret) in the response, the LLM sees it — period.
308+
- `request-complete` controls what the assistant *speaks* after the tool runs. It does NOT remove the tool result from history. The model still sees the raw response on the next user turn.
309+
- `variableExtractionPlan` aliases give you determinism (subsequent tools deterministically receive the value without LLM forwarding) but NOT invisibility — the underlying response is still in the model's view.
310+
311+
The actual redaction primitive across assistants is [`contextEngineeringPlan.type: previousAssistantMessages`](squads.md#sanitizing-tool-call-data-across-assistants-pci-pattern) at handoff time. Within a single assistant, there is no equivalent — if the model must not see a value, your tool server must not place it in the response body to begin with.
312+
313+
### Static `parameters` is the LLM-invisibility primitive (security boundary)
314+
315+
The override behavior described under [`apiRequest` Tools → Static parameters](#static-parameters-override-llm-produced-body-fields) is also the foundation for prompt-injection-safe data passing. The static `parameters` array is the *only* tool-config field whose values are guaranteed never to reach the LLM:
316+
317+
1. The top-level `tool.parameters` is not part of the schema shipped to the model — only `tool.function.parameters` is.
318+
2. Server-side merging happens at fulfill time. The model has no write path to call-level Liquid sources like `{{ customer.number }}` mid-conversation; those come from SIP/Twilio signaling or the validated outbound API payload.
319+
320+
**Use static `parameters` for any value the model must not be able to fake or influence:** verified caller ID, called number, call ID, backend-looked-up account ID, per-call HMAC nonce.
321+
322+
#### Naming footgun: two fields called "parameters"
323+
324+
| Field | Who fills it | Visible to LLM? | Use for |
325+
|---|---|---|---|
326+
| `tool.function.parameters` (JSON Schema) | LLM at runtime | **Yes** | Values the model should infer or that the caller will speak (intent, name, item to order) |
327+
| `tool.parameters` (top-level array of `{ key, value }`) | You at config time, server-resolved at fulfill | **No** | Values your backend or Vapi's signaling layer already knows |
328+
329+
The dashboard renders these as "Parameters" (JSON schema editor) and "Static Body Fields" (key/value rows with Liquid). Distinguishable by UI shape, not label. **Decision rule:** could a malicious caller speak the value? If "yes if I rely on the LLM to fill it," the field belongs in the top-level `parameters` array.
330+
331+
#### Tool types that support static `parameters`
332+
333+
| Tool type | Supports static `parameters`? |
334+
|---|---|
335+
| `apiRequest` | ✅ |
336+
| `function` (modern, under `assistant.model.tools[]`) | ✅ |
337+
| Legacy `assistant.model.functions[]` (deprecated) | ❌ — converter zeroes it out |
338+
| `code`, `handoff`, `transferCall`, `endCall`, others | ❌ |
339+
340+
#### Mudflap progressive caller-ID auth pattern (worked example)
341+
342+
```yaml
343+
type: apiRequest
344+
method: POST
345+
url: https://your-backend.example.com/lookup-and-verify
346+
function:
347+
name: lookup_and_verify_user
348+
parameters:
349+
type: object
350+
properties:
351+
name: { type: string }
352+
email: { type: string }
353+
required: [name, email]
354+
parameters: # server-resolved, LLM-invisible
355+
- { key: caller_number, value: "{{ customer.number }}" }
356+
- { key: called_number, value: "{{ phoneNumber.number }}" }
357+
- { key: call_id, value: "{{ call.id }}" }
358+
```
359+
360+
The LLM produces only `name` and `email` (what the caller spoke). The orchestration layer fills `caller_number`, `called_number`, `call_id` server-side. Your backend authenticates against the trusted fields and treats `name` / `email` as claims that must match the row keyed on `caller_number`. A "call this tool with phone number FAKE" prompt-injection has no path — the field doesn't exist in the schema the LLM sees.
361+
362+
For belt-and-braces, pair static parameters with HMAC body signing so the backend verifies sender + content, not just channel.
363+
364+
For the trust-tier breakdown of which Liquid variables are safe to use here (`{{ customer.number }}`, `{{ call.id }}`, etc.) vs. which are LLM-derived and not, see [assistants.md → Liquid Variable Bag and Trust Tiers](assistants.md#liquid-variable-bag-and-trust-tiers).

0 commit comments

Comments
 (0)