Skip to content

feat(security): opt-in destination policy for the built-in transfer_call tool#206

Merged
nicolotognoni merged 3 commits into
mainfrom
fix/transfer-destination-allowlist
Jul 3, 2026
Merged

feat(security): opt-in destination policy for the built-in transfer_call tool#206
nicolotognoni merged 3 commits into
mainfrom
fix/transfer-destination-allowlist

Conversation

@nicolotognoni

Copy link
Copy Markdown
Collaborator

Summary

  • Closes Security: transfer_call has no destination allowlist — prompt injection enables toll fraud #205transfer_call's destination is chosen by the LLM (steerable by caller speech), and the only gate was E.164 format: a successful prompt injection could direct a billable outbound leg to any attacker-chosen premium-rate / international number (toll fraud billed to the operator).
  • Adds an opt-in destination policy: transfer_allowed_numbers / transfer_allowed_prefixes (Python) — transferAllowedNumbers / transferAllowedPrefixes (TypeScript). Exact-number allowlist and/or E.164 prefix allowlist, union semantics.
  • Enforced deterministically at every guard site BEFORE the carrier REST call: OpenAI Realtime function_call path, Pipeline built-in handler, ElevenLabs ConvAI client-tool path. A denied destination gets the standard {"error": "Transfer destination not allowed by policy", "status": "rejected"} envelope, so the agent keeps the call and can tell the caller.

Implementation

  • Shared policy helper: _transfer_destination_allowed (libraries/python/getpatter/telephony/common.py) / isTransferDestinationAllowed (libraries/typescript/src/stream-handler.ts). Semantics: no policy → unrestricted (byte-identical to today); either list set → exact-number OR prefix match required; configured-but-empty → deny all (explicit lockdown).
  • New frozen/readonly Agent fields (models.py / types.ts), threaded through Patter.agent() with fail-fast validation at construction (allowlist numbers must be E.164; prefixes must be '+' + 1-14 digits) — a typo'd entry fails at build time, not silently mid-call.
  • Guard sites read the current agent (self.agent / this.currentAgent), so the policy follows multi-agent handoffs; pipeline mode threads the policy into _augment_with_builtin_handoff_tools / augmentWithBuiltinHandoffTools, which is rebuilt after each handoff.
  • Denied attempts log a transfer_call rejected: destination not allowed by policy ***1234 warning with the number masked (PII-safe).
  • Programmatic transfers (control.transfer(...)) are intentionally NOT gated — the policy targets LLM-chosen destinations, not developer-initiated ones.
  • Also syncs libraries/typescript/package-lock.json's version stanza to 0.7.0 (it was left at 0.6.9 by the release bump).
  • No new dependencies.

Breaking change?

No. Both options are opt-in with None/undefined defaults; unset keeps today's behaviour byte-identical (verified by a dedicated no-policy regression test in each SDK).

Test plan

  • Python: pytest tests/ — 2918 passed (17 new in tests/test_transfer_allowlist.py: helper semantics, factory validation, pipeline handler, Realtime function_call, ConvAI client tool)
  • TypeScript: npm test (2328 passed, 17 new in tests/transfer-allowlist.test.ts) + npm run lint + npm run build — all clean
  • Parity: field names snake_case ↔ camelCase, identical defaults, identical rejection envelope, identical guard-site coverage

Docs updates

  • docs/python-sdk/tools.mdx — new "Restricting transfer destinations" section
  • docs/typescript-sdk/tools.mdx — same, TS invocation
  • CHANGELOG.md## Unreleased### Added

…all tool

The transfer destination is chosen by the LLM, which is steerable by
caller speech, and the only gate was E.164 format -- a successful prompt
injection could direct a billable outbound leg to any attacker-chosen
(premium-rate) number (GH #205).

New opt-in Agent options transfer_allowed_numbers /
transfer_allowed_prefixes (Py) -- transferAllowedNumbers /
transferAllowedPrefixes (TS): exact-number allowlist and/or E.164 prefix
allowlist (union), enforced at every guard site BEFORE the carrier REST
call (OpenAI Realtime function_call, Pipeline built-in handler,
ElevenLabs ConvAI client tool) with the standard rejection envelope so
the agent keeps the call. Unset (default) preserves current behaviour;
an empty list denies all transfers. Entries validated at agent()
construction. 17 new tests per SDK.
@mintlify

mintlify Bot commented Jul 1, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
patter-06b046ce 🟢 Ready View Preview Jul 1, 2026, 11:15 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

nicolotognoni and others added 2 commits July 2, 2026 11:23
Integrates releases #202 (0.7.1) and #208 (low-latency pipeline) now on main.
Keeps this PR's transfer-allowlist Unreleased entry alongside #208's entries
under the single Unreleased section, adopts 0.7.1 across version files (no
extra bump), and regenerates package-lock.json from the merged package.json.
Full suites green post-merge: Python 3083, TypeScript 2493, tsc clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D52f6rBsvzicXg45C6MKVM
MDX v3 parses `{...}` as a JS expression; `{far-field + tuned server_vad}`
(space-separated identifiers) is invalid JS, so acorn failed at openclaw.mdx
818:24 and the whole Mintlify docs deploy errored on every docs PR. Wrap the
two set-notation snippets in backticks (consistent with the doc's inline-code
style for config terms) so they render literally. Pre-existing issue on main
(introduced with #207); `mint broken-links` now reports no errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D52f6rBsvzicXg45C6MKVM
@nicolotognoni nicolotognoni merged commit 91216d2 into main Jul 3, 2026
10 checks passed
nicolotognoni added a commit that referenced this pull request Jul 3, 2026
Version files already bumped to 0.7.1 by #202; #206 (transfer allowlist) and
#208 (low-latency voice pipeline) then merged onto main at 0.7.1 but their
entries sat under Unreleased. Fold them into the 0.7.1 section so the published
release notes match the code that ships. No version change (already 0.7.1); no
code change. Merge this, then tag v0.7.1 to publish to npm + PyPI.


Claude-Session: https://claude.ai/code/session_01D52f6rBsvzicXg45C6MKVM

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot deleted the fix/transfer-destination-allowlist branch July 4, 2026 06:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Security: transfer_call has no destination allowlist — prompt injection enables toll fraud

1 participant