Skip to content

Commit 35e3cce

Browse files
docs(decisions): canonical /decisions verdict values for 9.0.0 (#212)
The /api/v1/decisions read surface canonicalized in platform 9.0.0: the endpoint returns allowed|blocked|redacted|needs_approval|error and the ?decision= filter rejects the old allow|deny|require_approval with HTTP 400. The SDK code is a string passthrough and is unaffected, but the list_decisions / explain_decision docstrings, the example env-var docs, and the test fixtures used the pre-9.0.0 values. Update them to the canonical set and add a pointer to the v8 to v9 migration guide. Docs/examples/fixtures only — no type or logic change. The wire /decide verdict (allow|deny|needs_approval) and the workflow-control gate decision (allow|block|require_approval) are deliberately untouched. Held for the 9.0.0 release. Signed-off-by: Saurabh Jain <saurabh.jain@getaxonflow.com>
1 parent 008ebe0 commit 35e3cce

4 files changed

Lines changed: 43 additions & 33 deletions

File tree

axonflow/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2821,7 +2821,7 @@ async def list_decisions(
28212821
28222822
Example:
28232823
>>> from axonflow.decisions import ListDecisionsOptions
2824-
>>> opts = ListDecisionsOptions(decision="deny", limit=10)
2824+
>>> opts = ListDecisionsOptions(decision="blocked", limit=10)
28252825
>>> for d in await client.list_decisions(opts):
28262826
... print(d.decision_id, d.decision, d.timestamp)
28272827
"""

axonflow/decisions.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,11 @@ class DecisionExplanation(BaseModel):
4747
with risk level and overridability.
4848
* ``matched_rules`` — rule-level detail (optional, populated when the
4949
upstream engine supports it).
50-
* ``decision`` — ``allow`` | ``deny`` | ``require_approval``.
50+
* ``decision`` — the canonical audit verdict ``allowed`` | ``blocked`` |
51+
``redacted`` | ``needs_approval`` | ``error`` (platform 9.0.0+). Pre-9.0.0
52+
this field used ``allow`` | ``deny`` | ``require_approval``; see the
53+
v8 → v9 migration guide:
54+
https://docs.getaxonflow.com/docs/deployment/v8-to-v9-migration/
5155
* ``reason`` — human-readable reason string.
5256
* ``risk_level`` — aggregate risk label for the decision.
5357
* ``override_available`` — True iff at least one non-critical policy
@@ -74,7 +78,7 @@ class DecisionExplanation(BaseModel):
7478
timestamp: datetime
7579
policy_matches: list[ExplainPolicy] = Field(default_factory=list)
7680
matched_rules: list[ExplainRule] | None = None
77-
decision: str # allow | deny | require_approval
81+
decision: str # allowed | blocked | redacted | needs_approval | error (9.0.0+)
7882
reason: str = ""
7983
risk_level: str | None = None
8084
override_available: bool = False
@@ -113,7 +117,7 @@ class DecisionSummary(BaseModel):
113117

114118
decision_id: str
115119
timestamp: datetime
116-
decision: str # allow | deny | require_approval
120+
decision: str # allowed | blocked | redacted | needs_approval | error (9.0.0+)
117121
policy_id: str | None = None
118122
tool_signature: str | None = None
119123
context: dict[str, str] | None = None
@@ -123,8 +127,12 @@ class ListDecisionsOptions(BaseModel):
123127
"""Optional filters for ``client.list_decisions``.
124128
125129
Every field is optional; ``None`` values are omitted from the URL so
126-
the platform applies its tier-default page. ``decision`` must be one
127-
of ``"allow"``, ``"deny"``, or ``"require_approval"`` when set.
130+
the platform applies its tier-default page. ``decision``, when set, must be
131+
one of the canonical audit verdicts ``"allowed"``, ``"blocked"``,
132+
``"redacted"``, ``"needs_approval"``, or ``"error"`` (platform 9.0.0+). The
133+
pre-9.0.0 values ``"allow"`` / ``"deny"`` / ``"require_approval"`` are
134+
rejected with HTTP 400 by 9.0.0 — see the v8 → v9 migration guide:
135+
https://docs.getaxonflow.com/docs/deployment/v8-to-v9-migration/
128136
``limit`` is server-capped per tier; over-cap requests yield a 429
129137
with the V1 upgrade envelope (surfaced as
130138
:class:`axonflow.exceptions.RateLimitError`).

examples/list_decisions.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
1313
Optional filters:
1414
15-
AXONFLOW_LIST_DECISION allow|deny|require_approval
15+
AXONFLOW_LIST_DECISION allowed|blocked|redacted|needs_approval|error
16+
(canonical audit verdicts, platform 9.0.0+;
17+
pre-9.0.0 allow|deny|require_approval now 400)
1618
AXONFLOW_LIST_POLICY_ID e.g. sys_sqli_stacked_drop
1719
AXONFLOW_LIST_LIMIT integer (server-capped per tier)
1820
"""

tests/test_decisions.py

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,10 @@ def test_minimum_fields_parse(self) -> None:
1717
exp = DecisionExplanation(
1818
decision_id="dec-1",
1919
timestamp=datetime(2026, 4, 17, tzinfo=timezone.utc),
20-
decision="deny",
20+
decision="blocked",
2121
)
2222
assert exp.decision_id == "dec-1"
23-
assert exp.decision == "deny"
23+
assert exp.decision == "blocked"
2424
assert exp.policy_matches == []
2525
assert exp.override_available is False
2626
assert exp.historical_hit_count_session == 0
@@ -29,7 +29,7 @@ def test_full_fields_round_trip(self) -> None:
2929
raw = {
3030
"decision_id": "dec_wf1_step2",
3131
"timestamp": "2026-04-17T12:00:00Z",
32-
"decision": "deny",
32+
"decision": "blocked",
3333
"reason": "SQL injection detected",
3434
"risk_level": "high",
3535
"policy_matches": [
@@ -57,7 +57,7 @@ def test_full_fields_round_trip(self) -> None:
5757
"tool_signature": "Bash",
5858
}
5959
exp = DecisionExplanation.model_validate(raw)
60-
assert exp.decision == "deny"
60+
assert exp.decision == "blocked"
6161
assert len(exp.policy_matches) == 1
6262
assert exp.policy_matches[0].policy_id == "pol-sqli"
6363
assert exp.policy_matches[0].allow_override is True
@@ -71,11 +71,11 @@ def test_extra_fields_are_ignored_for_forward_compat(self) -> None:
7171
raw = {
7272
"decision_id": "dec-1",
7373
"timestamp": "2026-04-17T12:00:00Z",
74-
"decision": "allow",
74+
"decision": "allowed",
7575
"future_field_we_dont_know_yet": {"nested": True},
7676
}
7777
exp = DecisionExplanation.model_validate(raw)
78-
assert exp.decision == "allow"
78+
assert exp.decision == "allowed"
7979

8080

8181
class TestExplainPolicy:
@@ -119,7 +119,7 @@ async def fake_request(
119119
return {
120120
"decision_id": "dec-1",
121121
"timestamp": "2026-04-17T12:00:00Z",
122-
"decision": "deny",
122+
"decision": "blocked",
123123
"reason": "blocked",
124124
"policy_matches": [
125125
{"policy_id": "p-1", "policy_name": "Test", "allow_override": True}
@@ -150,7 +150,7 @@ async def fake_request(
150150
return {
151151
"decision_id": "a/b",
152152
"timestamp": "2026-04-17T12:00:00Z",
153-
"decision": "allow",
153+
"decision": "allowed",
154154
"reason": "",
155155
"policy_matches": [],
156156
"override_available": False,
@@ -225,7 +225,7 @@ def test_minimum_fields_parse(self) -> None:
225225
d = DecisionSummary(
226226
decision_id="dec-1",
227227
timestamp=datetime(2026, 5, 7, 12, 0, 0, tzinfo=timezone.utc),
228-
decision="deny",
228+
decision="blocked",
229229
)
230230
assert d.policy_id is None
231231
assert d.tool_signature is None
@@ -234,12 +234,12 @@ def test_full_fields_round_trip(self) -> None:
234234
raw = {
235235
"decision_id": "dec-x",
236236
"timestamp": "2026-05-07T12:00:00Z",
237-
"decision": "allow",
237+
"decision": "allowed",
238238
"policy_id": "pol-default",
239239
"tool_signature": "github.status",
240240
}
241241
d = DecisionSummary.model_validate(raw)
242-
assert d.decision == "allow"
242+
assert d.decision == "allowed"
243243
assert d.policy_id == "pol-default"
244244
# extra='ignore' accepts arbitrary unknown fields
245245
raw_extra = {**raw, "policy_version": 7, "future_field": "shrug"}
@@ -254,15 +254,15 @@ def test_summary_context_absent_is_none(self) -> None:
254254
d = DecisionSummary(
255255
decision_id="dec-noctx",
256256
timestamp=datetime(2026, 5, 30, tzinfo=timezone.utc),
257-
decision="allow",
257+
decision="allowed",
258258
)
259259
assert d.context is None
260260

261261
def test_summary_context_round_trip(self) -> None:
262262
raw = {
263263
"decision_id": "dec-ctx",
264264
"timestamp": "2026-05-30T12:00:00Z",
265-
"decision": "deny",
265+
"decision": "blocked",
266266
"context": {
267267
"x_ai_agent": "refund-bot",
268268
"x_session_id": "sess-42",
@@ -283,7 +283,7 @@ def test_explanation_full_context_and_truncated_flag(self) -> None:
283283
raw = {
284284
"decision_id": "dec-x",
285285
"timestamp": "2026-05-30T12:00:00Z",
286-
"decision": "deny",
286+
"decision": "blocked",
287287
"context": {"x_ai_agent": "a", "x_session_id": "s"},
288288
"context_truncated": True,
289289
}
@@ -295,7 +295,7 @@ def test_explanation_context_defaults_none(self) -> None:
295295
exp = DecisionExplanation(
296296
decision_id="dec-1",
297297
timestamp=datetime(2026, 5, 30, tzinfo=timezone.utc),
298-
decision="allow",
298+
decision="allowed",
299299
)
300300
assert exp.context is None
301301
assert exp.context_truncated is None
@@ -316,21 +316,21 @@ async def test_happy_path(self, httpx_mock) -> None:
316316
{
317317
"decision_id": "dec-1",
318318
"timestamp": "2026-05-07T12:00:00Z",
319-
"decision": "deny",
319+
"decision": "blocked",
320320
"policy_id": "pol-sqli",
321321
"tool_signature": "postgres.query",
322322
},
323323
{
324324
"decision_id": "dec-2",
325325
"timestamp": "2026-05-07T11:00:00Z",
326-
"decision": "allow",
326+
"decision": "allowed",
327327
"policy_id": "pol-default",
328328
"tool_signature": "github.status",
329329
},
330330
{
331331
"decision_id": "dec-3",
332332
"timestamp": "2026-05-07T10:00:00Z",
333-
"decision": "require_approval",
333+
"decision": "needs_approval",
334334
"policy_id": "pol-amount",
335335
"tool_signature": "stripe.charge",
336336
},
@@ -341,7 +341,7 @@ async def test_happy_path(self, httpx_mock) -> None:
341341
got = await client.list_decisions()
342342
assert len(got) == 3
343343
assert got[0].decision_id == "dec-1"
344-
assert got[2].decision == "require_approval"
344+
assert got[2].decision == "needs_approval"
345345

346346
@pytest.mark.asyncio
347347
async def test_filter_serialization(self, httpx_mock) -> None:
@@ -354,7 +354,7 @@ async def test_filter_serialization(self, httpx_mock) -> None:
354354
url=(
355355
"http://localhost:8080/api/v1/decisions?"
356356
"since=2026-05-07T00%3A00%3A00Z&"
357-
"decision=deny&"
357+
"decision=blocked&"
358358
"policy_id=pol-sqli&"
359359
"tool_signature=postgres.query&"
360360
"limit=25"
@@ -364,7 +364,7 @@ async def test_filter_serialization(self, httpx_mock) -> None:
364364
client = AxonFlow(endpoint="http://localhost:8080")
365365
opts = ListDecisionsOptions(
366366
since=datetime(2026, 5, 7, 0, 0, 0, tzinfo=timezone.utc),
367-
decision="deny",
367+
decision="blocked",
368368
policy_id="pol-sqli",
369369
tool_signature="postgres.query",
370370
limit=25,
@@ -379,11 +379,11 @@ async def test_omits_unset_filters(self, httpx_mock) -> None:
379379
# Only decision is set; URL must omit the others entirely.
380380
httpx_mock.add_response(
381381
method="GET",
382-
url="http://localhost:8080/api/v1/decisions?decision=deny",
382+
url="http://localhost:8080/api/v1/decisions?decision=blocked",
383383
json={"decisions": []},
384384
)
385385
client = AxonFlow(endpoint="http://localhost:8080")
386-
await client.list_decisions(ListDecisionsOptions(decision="deny"))
386+
await client.list_decisions(ListDecisionsOptions(decision="blocked"))
387387

388388
@pytest.mark.asyncio
389389
async def test_429_upgrade_envelope(self, httpx_mock) -> None:
@@ -474,7 +474,7 @@ async def test_forward_compat_unknown_fields(self, httpx_mock) -> None:
474474
{
475475
"decision_id": "dec-fwd",
476476
"timestamp": "2026-05-07T12:00:00Z",
477-
"decision": "deny",
477+
"decision": "blocked",
478478
"policy_id": "pol-x",
479479
"tool_signature": "tool-x",
480480
"policy_version": 7,
@@ -505,6 +505,6 @@ def test_empty_options_returns_empty(self) -> None:
505505
def test_partial_options_omit_none_fields(self) -> None:
506506
from axonflow.client import _build_list_decisions_query
507507

508-
opts = ListDecisionsOptions(decision="deny", limit=7)
508+
opts = ListDecisionsOptions(decision="blocked", limit=7)
509509
qs = _build_list_decisions_query(opts)
510-
assert qs == "decision=deny&limit=7"
510+
assert qs == "decision=blocked&limit=7"

0 commit comments

Comments
 (0)