autogen (ag2)
Governing autogen (ag2) agents in Python, zero-code.
Every registered function call in autogen (ag2) flows through one method:
autogen.ConversableAgent.execute_function (and its async twin a_execute_function). raxit's
auto-patcher wraps that method, so every function call resolves to permit / defer / deny
before it executes, with no governance code in the agent. Python only — no TS variant
(autogen/ag2 is Python-only).
Package note
The pip package pyautogen is an unrelated, essentially-empty stub — it is not Microsoft
AutoGen. The maintained continuation of the AutoGen codebase publishes under the pip package
ag2 (still importable as import autogen); raxit's test suite and this example
pin ag2==0.14.0.
Run it yourself
For your own autogen (ag2) agent, install the SDK and launch it under raxit run:
pip install raxit
raxit run -- python your_agent.pyRun the same file without raxit run and it runs ungoverned — governance is a deploy-time
choice, not a code change.
How it works
Functions are registered on a plain ConversableAgent with no governance code. The only
"wiring" is each function call payload's name, the policy resource it's governed as:
# imports NOTHING from raxit
from autogen import ConversableAgent
def db_write(payload: str) -> str: ...
agent = ConversableAgent(name="ops-bot", llm_config=False)
agent._function_map["db.write"] = db_write
agent.execute_function({"name": "db.write", "arguments": "{\"payload\": \"note\"}"})apiVersion: raxit.security/v1
agent: autogen-bot
default: block
rules:
- effect: allow
resource: fetch_docs
- effect: review # unconditional review == defer.policy
resource: db.write
# shell.exec is intentionally UNLISTED -> default-deny (deny.default_or_forbid).A denied or deferred call raises raxit.autopatch.RaxitDenied from inside execute_function,
carrying the reason code — the function body never executes. Catch it like any other exception,
or let it propagate to ag2's own reply handling. raxit explain <call_id> shows the full
decision record.
Real captured output from a run:
[PASS] autopatch: install() governed the autogen ConversableAgent.execute_function dispatch — patched={'autogen': ['execute_function', 'a_execute_function']}
fetch_docs -> permit -> ran: fetch_docs function body ran (ledger: ['fetch_docs'])
db.write -> defer -> RaxitDenied raised: reason=defer.policy
shell.exec -> deny -> RaxitDenied raised: reason=deny.default_or_forbid
audit.verify -> ok=True signatures_verified=True signed_count=3 count=3
all checks PASSThe >>>>>>>> EXECUTING FUNCTION / EXECUTED FUNCTION lines seen in a real run are ag2's own
console logging for the permitted call — expected framework noise, unrelated to the reason codes
above.
Scope: no MODIFY here
execute_function's tool input is a func_call dict whose payload the patcher reads for the
tool NAME — not the function's actual arguments spread at the top level — so argument-rewriting
verdicts aren't exercised here. Same scope note our test suite documents for this framework.
What got denied and why
defer.policy—db.writematched thereviewrule; it's suspended for human approval (see Approvals). The demo runs withRAXIT_DEFER_TIMEOUT=0, so the defer surfaces as a raisedRaxitDeniedrather than blocking inline.deny.default_or_forbid—shell.execappears nowhere in the policy, so it falls todefault: block. The function body never ran, confirmed by the demo's own call log.
Every decision — the permit, the defer, and the deny — lands in one hash-chained,
Ed25519-signed Decision Provenance log, verified green with client.audit.verify(signatures=True).
See Audit and Decisions for the full
permit / defer / deny vocabulary and reason-code catalog.
Related
- LangGraph (Python) — same zero-code autopatch story at a different interception point.
- LlamaIndex, pydantic-ai — the same docs/ops-bot story on other Python frameworks.

