From 373916c970ebdcb0df0de55c088b3591a09b0f47 Mon Sep 17 00:00:00 2001 From: Saurabh Jain Date: Thu, 7 May 2026 14:32:00 +0200 Subject: [PATCH 1/2] docs(examples): add explain_decision example end-to-end MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the example-parity gap: client.explain_decision() shipped in April but no runnable example existed. Mirror the Rust SDK pattern (axonflow-sdk-rust#29) so callers can copy a complete demo without reading the SDK source. Reads AXONFLOW_DECISION_ID from env (typically harvested from a recent block response or audit row), calls explain_decision, and prints every ADR-043 field — policy matches, matched rules, override availability, historical hit count. Signed-off-by: Saurabh Jain --- examples/explain_decision.py | 92 ++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 examples/explain_decision.py diff --git a/examples/explain_decision.py b/examples/explain_decision.py new file mode 100644 index 0000000..14e3140 --- /dev/null +++ b/examples/explain_decision.py @@ -0,0 +1,92 @@ +"""Example: explain a previously-made AxonFlow policy decision. + +Implements the ADR-043 explainability flow. Given a decision_id (typically +surfaced on the response of a blocked governed call, an audit_logs row, or +the ``explain_decision`` MCP tool), this example fetches the structured +explanation and renders the matched policies, risk level, and override +availability. + +Required env vars: + +* ``AXONFLOW_AGENT_URL`` (default: http://localhost:8080) +* ``AXONFLOW_CLIENT_ID`` (default: community) +* ``AXONFLOW_CLIENT_SECRET`` (default: empty) +* ``AXONFLOW_DECISION_ID`` the decision to explain + +Get a decision_id quickly by hitting a known-blocked policy:: + + curl -u "$AXONFLOW_CLIENT_ID:$AXONFLOW_CLIENT_SECRET" \\ + -X POST $AXONFLOW_AGENT_URL/api/v1/mcp/check-input \\ + -H 'Content-Type: application/json' \\ + -d '{"connector_type":"postgres","operation":"execute", + "statement":"SELECT 1; DROP TABLE users;--","user_token":"u1"}' + +then read decision_id from the block response or the most recent audit row. +""" + +import asyncio +import os +import sys + +from axonflow import AxonFlow + + +async def main() -> None: + decision_id = os.environ.get("AXONFLOW_DECISION_ID", "") + if not decision_id: + print( + "AXONFLOW_DECISION_ID must be set " + "(a decision_id from a recent blocked call)", + file=sys.stderr, + ) + sys.exit(2) + + endpoint = os.environ.get("AXONFLOW_AGENT_URL", "http://localhost:8080") + print(f"Initializing AxonFlow client at {endpoint}...") + async with AxonFlow( + endpoint=endpoint, + client_id=os.environ.get("AXONFLOW_CLIENT_ID", "community"), + client_secret=os.environ.get("AXONFLOW_CLIENT_SECRET", ""), + ) as client: + print(f"Explaining decision {decision_id}...\n") + exp = await client.explain_decision(decision_id) + + print("=== Decision Explanation ===") + print(f" decision_id: {exp.decision_id}") + print(f" timestamp: {exp.timestamp.isoformat()}") + print(f" decision: {exp.decision}") + print(f" reason: {exp.reason}") + if exp.risk_level: + print(f" risk_level: {exp.risk_level}") + if exp.tool_signature: + print(f" tool: {exp.tool_signature}") + + print(f"\n policy_matches ({len(exp.policy_matches)}):") + for i, m in enumerate(exp.policy_matches): + print( + f" [{i}] {m.policy_id} ({m.policy_name or '(unnamed)'}) — " + f"action={m.action or '-'} risk={m.risk_level or '-'} " + f"allow_override={m.allow_override}" + ) + + if exp.matched_rules: + print(f"\n matched_rules ({len(exp.matched_rules)}):") + for r in exp.matched_rules: + print( + f" {r.policy_id} on {r.rule_id or '(no rule id)'}: " + f"matched={r.matched_on or '-'}" + ) + + print(f"\n override_available: {exp.override_available}") + if exp.override_existing_id: + print(f" override_existing_id: {exp.override_existing_id}") + print( + f" historical_hit_count_session: " + f"{exp.historical_hit_count_session}" + ) + if exp.policy_source_link: + print(f" policy_source_link: {exp.policy_source_link}") + + +if __name__ == "__main__": + asyncio.run(main()) From 6f472cd15449037d83b5e092713f4d0f9bd4399c Mon Sep 17 00:00:00 2001 From: Saurabh Jain Date: Fri, 8 May 2026 01:11:58 +0200 Subject: [PATCH 2/2] fix(lint): apply ruff format to examples/explain_decision.py Signed-off-by: Saurabh Jain --- examples/explain_decision.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/examples/explain_decision.py b/examples/explain_decision.py index 14e3140..db81815 100644 --- a/examples/explain_decision.py +++ b/examples/explain_decision.py @@ -35,8 +35,7 @@ async def main() -> None: decision_id = os.environ.get("AXONFLOW_DECISION_ID", "") if not decision_id: print( - "AXONFLOW_DECISION_ID must be set " - "(a decision_id from a recent blocked call)", + "AXONFLOW_DECISION_ID must be set (a decision_id from a recent blocked call)", file=sys.stderr, ) sys.exit(2) @@ -80,10 +79,7 @@ async def main() -> None: print(f"\n override_available: {exp.override_available}") if exp.override_existing_id: print(f" override_existing_id: {exp.override_existing_id}") - print( - f" historical_hit_count_session: " - f"{exp.historical_hit_count_session}" - ) + print(f" historical_hit_count_session: {exp.historical_hit_count_session}") if exp.policy_source_link: print(f" policy_source_link: {exp.policy_source_link}")