← All Heat demos
Optional adopter example ยท PydanticAI-shaped tool boundary

Stop unsafe PydanticAI tool egress before it ships. Fast to try, easy to adopt.

Add one narrow boundary where model-selected tool calls become filesystem or webhook effects. PydanticAI keeps its agent, tools, and orchestration; Heat proves the risky egress cannot build until values are laundered. The ask is small: one optional example, zero framework rewrite.

Boundary Flow

Same tool call, one boundary check, compile-time refusal when it matters.

PydanticAI agent

No framework rewrite: prompts, tools, context, and orchestration stay in Python.

export_case_file(path, content)Model-controlled path and content. notify_webhook(url, body)Same reusable boundary shape for network egress.

Heat boundary adapter

One adapter call checks the sink contract before unsafe code can be emitted.

REFUSED@user_input cannot reach @path_safe. BUILTLaundered values are allowed through the repaired boundary.

External action

Only calls that satisfy the proof reach filesystems, webhooks, or partner APIs.

blockedNo unsafe binary is emitted for the bad flow. written / postedRepaired filesystem and webhook boundaries still build.

Before

Today, a model-shaped path can reach a real write.

A ticket contains attacker-controlled text. The agent selects export_case_file. Python can validate ordinary shapes, but the path/content still reach the side-effecting tool unless every code path remembered the same policy check.

Ticket / retrieved textUntrusted text influences the tool call.
->
export_case_fileWrites /tmp/heat_pydanticai_public_case.txt.

After

With Heat, the unsafe path fails before release.

The app adds one boundary adapter. Heat refuses model-controlled paths at compile time unless the value is laundered into @path_safe; repaired flows still build.

Same Python agentSame tool-call intent and app-level code path.
->
Heat boundaryProject policy becomes a compiler diagnostic.

Real Proof

$ bash examples/demos/pydanticai_reference_boundary/run_demo.sh

== capture Heat proof ==
wrote examples/demos/pydanticai_reference_boundary/heat_boundary/proof.json
proof and boundary contract are valid

== before: Python tool egress runs ==
before: wrote /tmp/heat_pydanticai_public_case.txt
before helper bypass: helper wrote /tmp/heat_pydanticai_public_case.txt

== after: same egress class is stopped at the boundary ==
after: blocked by Heat boundary:
error[NL-OBL-001] bank_support_boundary.heat:18:12: project obligation 'NoTicketTextToFilesystem' refuses '@user_input' reaching '@path_safe' at 'write_file' arg 0

== pydanticai-shaped slice: same guarded tool adapter ==
pydanticai slice: blocked by Heat boundary:
error[NL-OBL-001] bank_support_boundary.heat:18:12: project obligation 'NoTicketTextToFilesystem' refuses '@user_input' reaching '@path_safe' at 'write_file' arg 0

== repaired slice: laundered path is allowed ==
pydanticai repaired: allowed by Heat boundary (BUILT): wrote /tmp/heat_pydanticai_safe_exports/case_4142.txt

fixed_verdict: BUILT
No framework rewriteShip as an optional example or cookbook entry.
Lightweight layerOne adapter call at the egress edge; no proxy runtime.
Missed helpers still failA direct helper write is refused at the Heat sink.
Filesystem + webhook proofThe same boundary shape covers two real sink classes.

Why Not Just Python?

Python guards help when they are called. Heat catches the path that forgot.

Decorators and callbacks are useful, but they are conventions. If a future helper writes a file directly, the policy is bypassed unless a test or reviewer catches it. In this boundary, filesystem egress requires @path_safe. Any path still carrying @user_input fails the build at the sink.

Proof Coverage

REFUSED model-controlled file path -> write_file(path: @path_safe)
REFUSED missed helper -> write_file(path: @path_safe)
REFUSED model-controlled webhook URL/body -> http_post(@audit_safe)
BUILT repaired file boundary
BUILT repaired webhook boundary

What Changes

One tool implementation gets one boundary call.

~ @support_agent.tool
async def export_case_file(ctx, path: str, content: str) -> str:
+    allowed, reason = heat_boundary.check_export_case_file(path=path, content=content)
+    if not allowed:
+        return "blocked by Heat boundary: " + reason
     Path(path).write_text(content)
     return "wrote file"

Files: before_bank_support.py, after_bank_support.py, pydanticai_bank_support.py, heat_boundary/bank_support_boundary.heat, and heat_boundary/proof.json. Related demo: AI authorship round trip timing.