Metadata-Version: 2.4
Name: fullcourtdefense-agent
Version: 0.2.2
Summary: FullCourtDefense agent hook: checks every AI-agent tool call against your organisation's action policy before it runs. Standard library only; Google ADK plugin, OpenAI Agents SDK and LangChain tool wrappers, and an opt-in start-up hook (FCD_AUTOGUARD).
Author: Full Court Defense
License-Expression: MIT
Project-URL: Homepage, https://fullcourtdefense.ai
Keywords: ai-agent-security,tool-call,action-policy,google-adk,openai-agents,langchain,vertex-ai,agent-engine,fullcourtdefense
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
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
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"

# fullcourtdefense-agent

FullCourtDefense checks every tool call of an AI agent against your
organisation's action policy **before the tool runs**. This package is the
small agent hook: standard library only (no `httpx`, `requests` or `openai`),
so adding it to a deployed agent does not change the versions of anything the
agent already uses.

## Install

Pin it exactly. Do not add `--hash`: pip's hash checking is all-or-nothing, so
one hashed line makes every other line of the file need a hash too, and an
agent's unpinned requirements stop building.

```
fullcourtdefense-agent==0.2.2
```

## Configure

| Variable | Meaning |
|---|---|
| `FCD_SHIELD_ID` | The agent's shield (`sh_...`). Required. |
| `FCD_SHIELD_KEY` | The shield key. |
| `FCD_API_URL` | FullCourtDefense API (default `https://api.fullcourtdefense.ai`). |
| `FCD_AGENT_ID` | The agent's identity in the console (also read: `FULLCOURTDEFENSE_AGENT_ID`, `FCD_AGENT_NAME`, `FCD_WORKLOAD_NAME`). Unset: the name of the service the process runs as (`OTEL_SERVICE_NAME`, Cloud Run `K_SERVICE`, `AWS_LAMBDA_FUNCTION_NAME`, Azure `CONTAINER_APP_NAME` / `WEBSITE_SITE_NAME`, ...), else the main script, so agents never collapse into `unknown-agent`. Every call says which (`agentIdSource`: `config`, `env` or `default`). A name passed in code (`agent_id=` or `agent_name=` on the call or on `protect()`) is declared: `config`. |
| `FCD_FAIL_MODE` | What a **block-mode** shield does while FullCourtDefense cannot be reached. `tiered` (default when unset; owner decision 2026-10-27, replaces `closed`): only the agent's never-allowed rules (cached from the runtime bundle) stop a call; any other action runs, is logged as a warning and is reported to FullCourtDefense as "ran without a check" (`ranDuringOutage`) once it answers again, and a rule that needs a manager runs unattended with `wouldHaveNeeded: "manager"`. The built-in emergency list no longer stops anything. `closed`: no tool runs. `open`: nothing is stopped. An unknown value is read as `closed`. |
| `FCD_MODE_AT_PROTECT` | `monitor` or `block`: the shield's mode when the agent was protected. Used only until the live mode has been read once, so a monitor-mode agent that starts while the API is down still never blocks. Set it to avoid the one-time mode lookup on the first call (the cloud Protect and the init wizard already set it); without it the first call waits for that lookup (at most 1.5 s), so an Enforce shield's first call is checked. A check waits at most 1.5 s before the fail mode answers (`timeout=`); If FullCourtDefense hangs or cannot be reached, at most one call waits up to 1.5 s; after that, calls answer instantly (from the fail mode) until we're back. A hang or a network error opens the circuit breaker at once; while it is open one background probe runs every 10 s (calls never wait on it) and a successful probe restores live checks. HTTP answers (4xx / 5xx with a body) are handled as before. |
| `FCD_SDK_SPOOL` / `FCD_SDK_SPOOL_DIR` | Records a short-lived process could not send before it ended (the end of the process waits at most 1.5 s for them) are kept in a small file in the user cache directory (`~/.cache/fullcourtdefense/sdk-spool`, `%LOCALAPPDATA%`, `~/Library/Caches`) and sent by the next start with the same shield. `FCD_SDK_SPOOL=off` disables it; `FCD_SDK_SPOOL_DIR` moves it. |
| `FCD_AUTOGUARD` | `adk`: add the gate to every Google ADK `Runner` at start-up (below). Unset: the start-up hook does nothing. |
| `FCD_APPROVAL_MODE` | ADK plugin: `block` (default) stops a call that needs a manager's approval; `wait` holds it until a person decides (`FCD_APPROVAL_TIMEOUT_S`, default 900). |

A **monitor-mode** shield never blocks and never waits: the tool runs at once
and the call is recorded in the background.

## Use

```python
from fullcourtdefense_agent import ToolGate, ToolCallBlocked

gate = ToolGate.from_env()

# A function, checked before every call (sync or async def):
safe_refund = gate.protect(issue_refund, operation="refund")

# Or one call:
gate.guard_tool_call("issue_refund", lambda: issue_refund("o_1", 120),
                     tool_args={"order": "o_1", "amount": 120}, operation="refund")
```

A blocked call raises `ToolCallBlocked`; the tool did not run.

**A wrong key, a refused or unknown shield, a refused request (HTTP 401 / 403 / 404 / 400)
is not an outage.** The fail mode does not apply: the tool does not run, the check is not
reported later as "ran without a check", and `FullCourtDefenseApiError` (a
`ToolCallBlocked` with `status` and `code`: `FCD_UNAUTHORIZED`, `FCD_FORBIDDEN`,
`FCD_SHIELD_NOT_FOUND`, `FCD_BAD_REQUEST`) says what to change. It is also logged once on the
`fullcourtdefense` logger. Any other 4xx except 408 / 429 (a 413, for example) is answered the same
way. Only a real outage (no answer, 5xx, 408, 429) is answered by the fail mode.

**On a monitor shield (owner decision O4)** a refused key or shield does not stop the tool: the mode
the gate knows (a live answer earlier in the process, else `FCD_MODE_AT_PROTECT`) is monitor, so the
tool runs and the call is still reported, and FullCourtDefense raises a "wrong or missing shield key"
alert. With no known mode the shield is treated as block.

**Arguments too deep or too large to check** (nested deeper than 32 levels, more than 20,000 values)
are never sent: in block mode the call is stopped (`ToolCallBlocked`, "too large to check safely"), in
monitor mode it runs and the record says what was not sent. Never an outage.

### OpenAI Agents SDK and LangChain

```python
from fullcourtdefense_agent.frameworks import protect_openai_agents_tool, protect_langchain_base_tool

agent = Agent(name="support", tools=[protect_openai_agents_tool(gate, lookup_customer)])  # still a FunctionTool
safe_sql = protect_langchain_base_tool(gate, sql_query)                                   # still a BaseTool
```

The full SDK (`pip install fullcourtdefense`) does the same with `fcd.protect(tool)`.

### Google ADK

```python
from google.adk.apps import App
from fullcourtdefense_agent.adk import FcdToolGuardPlugin

app = App(name="billing", root_agent=root_agent, plugins=[FcdToolGuardPlugin()])
```

A blocked call is answered to the model as
`{"blocked": true, "reason": "...", "by": "FullCourtDefense"}` and the tool does
not run. Gemini's built-in tools (Google Search, built-in code execution,
Vertex AI Search grounding) run inside Google and are not checked.

### Start-up hook (no code change)

The wheel installs `fullcourtdefense_agent_autoguard.pth` into site-packages.
Python runs its one line at start-up. **Without `FCD_AUTOGUARD` it only reads
that variable and stops.** With `FCD_AUTOGUARD=adk` it waits for
`google.adk.runners` to load and adds the plugin to every `Runner` the process
creates, after the agent's own plugins. Security scanners flag start-up lines
in `.pth` files as a supply-chain technique; this one is disclosed here, pinned
by exact version and inert unless the variable is set. If the plugin cannot be
added (an unsupported ADK version), the hook logs an error on the
`fullcourtdefense` logger and stderr; it never stops the agent from starting.

## What leaves the agent

The tool name, the operation, the agent identity, the session id and the tool
arguments (the policy input; the server minimises them before storing). Never
the tool's result and never the text of an error the tool raised.
