Metadata-Version: 2.5
Name: integrable-firewall
Version: 0.1.2
Summary: Connect an AI agent built in code to its firewall, which screens every user message, tool call and tool result.
Project-URL: Homepage, https://integrable.tech
Author-email: Integrable <enquiries@integrable.tech>
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: agents,ai,firewall,google-adk,guardrails,langchain,openai-agents,prompt injection,pydantic-ai
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Libraries
Classifier: Typing :: Typed
Requires-Python: >=3.9
Provides-Extra: adk
Requires-Dist: google-adk>=1.18; extra == 'adk'
Provides-Extra: langchain
Requires-Dist: langchain>=1.3; extra == 'langchain'
Provides-Extra: openai-agents
Requires-Dist: openai-agents>=0.6; extra == 'openai-agents'
Provides-Extra: pydantic-ai
Requires-Dist: pydantic-ai-slim>=2.16; extra == 'pydantic-ai'
Description-Content-Type: text/markdown

# integrable-firewall

Connects an AI agent you build in code to its firewall. The firewall screens
every message from the agent's user, every tool call before it runs and every
tool result before the model reads it. Each step passes, is stopped, or has its
result rewritten. A stopped step never waits for a person: the model reads the
reason and tells its user.

Documentation: `/docs/agents.html` on your firewall, for example
`https://<your firewall>.cloudfront.net/docs/agents.html`.

## Install

```sh
pip install "integrable-firewall[langchain]"      # or [openai-agents], [adk], [pydantic-ai]
```

The core has no dependencies and runs on Python 3.9 or later. Each framework
is an optional extra, and an adapter imports its framework only when you
import the adapter.

## Settings

One adapter protects one agent, named in code. Your firewall's setup prompt
has your coding agent write its address and the agent's key into the code, as
the `url=` and `key=` options. That key is made to be committed, like an error
tracker's DSN: it only lets that one agent send its own steps to your firewall
and read the answers, and it belongs to the first agent name it is used with.
To keep a key out of the code, leave the options out and put two settings
where the agent runs:

- `FIREWALL_URL`: your firewall's address.
- `FIREWALL_KEY_<AGENT>`: the agent's key (`support-widget` reads
  `FIREWALL_KEY_SUPPORT_WIDGET`). `FIREWALL_KEY` is read when that is not set.

The `url=` and `key=` options take precedence over both. If the key was made
for another agent, the adapter raises when you create it. The key is never
logged or put in an error message.

`goals=` and `permissions=` say what the agent is for and what it may do, the
way you would on its page in your dashboard. The setup prompt has your coding
agent write them from the agent's own instructions and tools, for example:

```python
from integrable_firewall.langchain import FirewallMiddleware

firewall = FirewallMiddleware(
    "support-widget",
    goals="Answers our customers' questions about their orders and their account.",
    permissions=(
        "May look up customers and their orders and hand a conversation to our team. "
        "Leave refunds, email address changes and account deletions to our team. "
        "Never reveal one customer's details to another."
    ),
)
```

They become the agent's rules the first time its key connects, unless
someone has already set them on the dashboard, and the dashboard is where to
change them after that: the firewall does not take them again from a later
deploy. A sentence beginning "Leave" names what is left to a person (that
step is stopped, and the model is told a person has to do it); one beginning
"Never" is something it must not do. Every adapter takes both.

## LangChain

```python
from langchain.agents import create_agent
from integrable_firewall import FirewallBlocked
from integrable_firewall.langchain import FirewallMiddleware

agent = create_agent(model, tools, middleware=[FirewallMiddleware(agent="support-widget")])

try:
    result = agent.invoke({"messages": [{"role": "user", "content": text}]}, {"configurable": {"thread_id": conversation_id}})
except FirewallBlocked as stop:
    reply = stop.reason
```

## OpenAI Agents SDK

```python
from agents import Agent, InputGuardrailTripwireTriggered, Runner
from integrable_firewall.openai_agents import FirewallGuardrails, blocked_reason

firewall = FirewallGuardrails(agent="support-widget")
support = firewall.protect(Agent(name="Support", instructions="...", tools=[lookup_order, issue_refund]))

try:
    result = await Runner.run(support, text)
except InputGuardrailTripwireTriggered as stop:
    reply = blocked_reason(stop)
```

## Google ADK

```python
from google.adk.apps import App
from google.adk.runners import Runner
from integrable_firewall.adk import FirewallPlugin

firewall = FirewallPlugin(agent="support-widget")
runner = Runner(app=App(name="support", root_agent=root_agent, plugins=[firewall]), session_service=session_service)

async for event in runner.run_async(user_id=user_id, session_id=session_id, new_message=message):
    reason = firewall.blocked_reason(event)   # set when the message was blocked
```

## Pydantic AI

```python
from pydantic_ai import Agent
from integrable_firewall import FirewallBlocked
from integrable_firewall.pydantic_ai import FirewallCapability

agent = Agent(model, tools=[lookup_order, issue_refund], capabilities=[FirewallCapability(agent="support-widget")])

try:
    result = await agent.run(text, message_history=history)
except FirewallBlocked as stop:
    reply = stop.reason
```

## When the firewall cannot be reached

Nothing passes without an answer from the firewall. If the firewall cannot be
reached, answers with an error, refuses the key, or has not answered after
9.5 s, the user's message is blocked, the tool does not run, or the tool's
result is withheld. The model, or your app for a user message, reads a reason
that starts with `Firewall:`. There is no setting that lets steps through
instead.

## Notes

- OpenAI Agents SDK: hosted tools such as web search run outside the SDK's
  tool guardrails, so `protect()` logs a warning that names them. When a
  guardrail stops a user message, the SDK still saves that message to a
  `Session`; call `await session.pop_item()` before the next run.
- Google ADK: list the plugin first in `plugins=[...]`.
- Without a framework adapter, use `integrable_firewall.Firewall` directly:
  `user_message()`, `before_tool()`, `after_tool()` and their `a`-prefixed
  async forms each return a `Verdict`.
