Managed governance (approval policies)¶
A connected agent can honor governance the platform admin defines centrally — no policy code in the agent. When the agent is about to call a tool the admin has flagged, the SDK asks the platform whether it may proceed; a high-stakes call pauses for human approval in the console and resumes once approved.
This builds on guardrails (local, in-process checks) by adding the
managed policy + pause/resume over the wire. It needs fa.connect().
For connect-time enrollment and the opt-in fail-closed posture (refuse rather
than run ungoverned when the plane is unreachable), see
Connected governance.
How it works¶
connect() ──▶ GET /policy (cache)
tool call ──▶ matches a cached approval policy?
│ no ─▶ run normally
│ yes ─▶ POST /policy/decide
├─ allow ─▶ run
├─ deny ─▶ refuse (tell the model why)
└─ require_approval ─▶ POST /runs/{id}/pending + PAUSE (checkpoint)
console approves ─▶ resume ─▶ run
connect()pulls and caches the policy (GET /policy). On a pull failure it keeps the last-known cache.- Before a tool call whose name matches a cached approval policy's
tool_pattern, the SDK callsPOST /policy/decide. deny→ the call is refused and the model is told why (the run continues).require_approval→ the SDK postsPOST /runs/{id}/pendingand pauses the agent (a real checkpoint). It then pollsGET /runs/{id}/pending; when the console flips the status toapproved, the agent resumes. Arejecteddecision refuses the call.
Only tools that match a configured policy incur a /policy/decide round-trip;
everything else runs untouched.
Plane-authored guardrails (enforced at the edge)¶
The same GET /policy pull also carries guardrails authored on the plane. A
connected agent turns each distributed rule into a runtime guardrail and enforces
it alongside its local guardrails=[...] — at all four positions. An agent with
no local guardrails still blocks on a centrally-authored rule:
import fastaiagent as fa
from fastaiagent import Agent, LLMClient
# An admin created a "block-ssn-output" regex guardrail in the console.
fa.connect(api_key="fa_k_...", target="https://app.fastaiagent.net")
agent = Agent(name="support", llm=LLMClient(provider="openai", model="gpt-4o"))
# ^ no guardrails=[...] defined locally
agent.run("Confirm my record: name Dana, SSN 123-45-6789.")
# -> GuardrailBlockedError(guardrail_name="block-ssn-output")
- No new check engine. A rule is mapped onto the SDK's own
regex/schema/classifier/llm_judgerunners (fastaiagent.guardrail.from_policy), so plane rules enforce exactly like local ones — including theon_errorfail policy. Acoderule (a server-side callable the SDK doesn't have) is skipped rather than silently passing. - Scoping. A rule attached to specific agents applies only to them; an
unattached rule is domain-wide. Built guardrails are memoized by policy
version, so an edit on the plane is picked up on the next pull. - Refresh.
fa.refresh_policy()re-pulls the policy mid-session to pick up a guardrail authored afterconnect(). - Local-only is unchanged. With no connection there is no policy cache, so a
local run enforces exactly its own
guardrails=[...]and pays nothing. - Plane rules stay plane rules. A reconstructed guardrail is marked
origin="plane", which keeps it out of the agent definition sent byagent.push()and stops it being enforced twice if you also pass it inguardrails=[...]. Both matter: the plane upserts pushed guardrails by name and attaches them to the pushing agent, so echoing a domain-wide rule back would narrow it to just that agent — silently dropping it for every other agent in the domain. You never need to pass plane rules in explicitly; the runtime injects them.
You don't need to pull them yourself
plane_guardrails_for_agent(...) exists for runtimes that aren't fa.Agent
(see Guardrails & evals without the runtime).
A connected fa.Agent already enforces plane rules with no code at all.
Enforcement is always local — the runtime is the enforcement point (see Who actually blocks). The plane authors and distributes; it never reaches into a running agent.
Enrolling an agent¶
Two things make an agent governable:
import fastaiagent as fa
from fastaiagent import Agent, FunctionTool, LLMClient
from fastaiagent.checkpointers.sqlite import SQLiteCheckpointer
fa.connect(api_key="fa_k_...", target="https://app.fastaiagent.net")
agent = Agent(
name="banker",
agent_id="<platform agent uuid>", # (1) enroll: the id /policy/decide matches on
llm=LLMClient(provider="openai", model="gpt-4o-mini"),
tools=[FunctionTool(name="transfer_funds", fn=transfer_funds)],
checkpointer=SQLiteCheckpointer("agent.db"), # (2) needed to pause/resume
)
# Blocking by default: arun() waits for the console decision and resumes.
result = await agent.arun("Transfer $500 to Bob.")
agent_idis the agent's platform UUID — it's sent to/policy/decideso the plane can match approval policies (and it validates the id). Without it, the agent isn't enrolled and the gate is a no-op.- A
checkpointeris required so a paused run can be resumed.
Blocking vs. non-blocking¶
arun() blocks by default (wait_for_approval=True): it waits for the console
decision and resumes for you. For your own control loop, pass
wait_for_approval=False to get the paused AgentResult and resume yourself once
the run is approved:
res = await agent.arun("Transfer $500 to Bob.", wait_for_approval=False)
if res.status == "paused": # res.pending_interrupt["reason"] == "policy_approval_required"
# ... wait for the console to approve (GET /public/v1/runs/{id}/pending) ...
res = await agent.aresume(res.execution_id, resume_value=Resume(approved=True))
A runnable end-to-end example is in examples/84_governed_agent.py.
Verified end-to-end¶
Against a live plane, a connected agent pausing for approval and resuming once the
console approves (real gpt-4o-mini, both modes):
connected. cached approval_policies: 1
=== NON-BLOCKING: arun(wait_for_approval=False) ===
paused? paused | reason: policy_approval_required
pending(before): pending | console approve -> 200 approved
resumed: completed | output: 'I have successfully transferred $500 to Bob.'
=== BLOCKING: arun() auto-waits for approval + resumes ===
pending appeared: pending | console approve -> 200 approved
auto-resumed: completed | output: 'I have successfully transferred $250 to Alice.'
In the console¶
A connected agent's high-stakes calls surface in the console for review. Here three
transfer_funds calls are Pending approval (one per paused run), under the
agent's API key:

Each paused run is also a normal trace — the connected agent's run pushed to the
platform (here agent.banker):

The admin manages the rules on the Approval Policies page (the tool_pattern
set) and resolves a connected run's pending approval via the Public API
(POST /api/v1/pending-runs/{id}/approve), which flips the status the SDK polls.