Skip to content

Commit ea1ae3d

Browse files
docs(decisions): canonical /decisions verdict values for 9.0.0 (#192)
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 211750e commit ea1ae3d

7 files changed

Lines changed: 33 additions & 26 deletions

File tree

examples/list-decisions/src/main/java/com/getaxonflow/examples/ListDecisions.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
// mvn -q compile exec:java
1515
//
1616
// Optional filters via env:
17-
// AXONFLOW_LIST_DECISION allow|deny|require_approval
17+
// AXONFLOW_LIST_DECISION allowed|blocked|redacted|needs_approval|error
18+
// (canonical audit verdicts, platform 9.0.0+;
19+
// pre-9.0.0 allow|deny|require_approval now 400)
1820
// AXONFLOW_LIST_POLICY_ID e.g. sys_sqli_stacked_drop
1921
// AXONFLOW_LIST_LIMIT integer (server-capped per tier)
2022
package com.getaxonflow.examples;

src/main/java/com/getaxonflow/sdk/AxonFlow.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -756,7 +756,7 @@ public CompletableFuture<DecisionExplanation> explainDecisionAsync(String decisi
756756
* <pre>{@code
757757
* try {
758758
* List<DecisionSummary> decisions = axonflow.listDecisions(
759-
* ListDecisionsOptions.builder().decision("deny").limit(10).build());
759+
* ListDecisionsOptions.builder().decision("blocked").limit(10).build());
760760
* for (DecisionSummary d : decisions) {
761761
* System.out.println(d.getDecisionId() + " " + d.getDecision());
762762
* }

src/main/java/com/getaxonflow/sdk/types/DecisionExplanation.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ public List<ExplainRule> getMatchedRules() {
8888
return matchedRules;
8989
}
9090

91+
/** Canonical audit verdict: allowed | blocked | redacted | needs_approval | error (9.0.0+). */
9192
public String getDecision() {
9293
return decision;
9394
}

src/main/java/com/getaxonflow/sdk/types/DecisionSummary.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public Instant getTimestamp() {
6363
return timestamp;
6464
}
6565

66-
/** allow | deny | require_approval */
66+
/** Canonical audit verdict: allowed | blocked | redacted | needs_approval | error (9.0.0+). */
6767
public String getDecision() {
6868
return decision;
6969
}

src/main/java/com/getaxonflow/sdk/types/ListDecisionsOptions.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,12 @@
1111
* Optional filters for {@code AxonFlow.listDecisions}.
1212
*
1313
* <p>Every field is optional; null values are omitted from the URL so the
14-
* platform applies its tier-default page. {@code decision} must be one of
15-
* {@code "allow"}, {@code "deny"}, or {@code "require_approval"} when set.
14+
* platform applies its tier-default page. {@code decision}, when set, must be one
15+
* of the canonical audit verdicts {@code "allowed"}, {@code "blocked"},
16+
* {@code "redacted"}, {@code "needs_approval"}, or {@code "error"} (platform
17+
* 9.0.0+); the pre-9.0.0 values {@code "allow"} / {@code "deny"} /
18+
* {@code "require_approval"} are rejected with HTTP 400 by 9.0.0 (see
19+
* https://docs.getaxonflow.com/docs/deployment/v8-to-v9-migration/).
1620
* {@code limit} is server-capped per tier; over-cap requests yield a 429
1721
* with the V1 upgrade envelope (surfaced as {@link
1822
* com.getaxonflow.sdk.exceptions.RateLimitException} carrying upgrade info).
@@ -21,7 +25,7 @@
2125
*
2226
* <pre>{@code
2327
* ListDecisionsOptions opts = ListDecisionsOptions.builder()
24-
* .decision("deny")
28+
* .decision("blocked")
2529
* .limit(10)
2630
* .build();
2731
* List<DecisionSummary> decisions = client.listDecisions(opts);

src/test/java/com/getaxonflow/sdk/DecisionExplainTest.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ class DecisionExplainTest {
2626
"{"
2727
+ "\"decision_id\": \"dec_wf1_step2\","
2828
+ "\"timestamp\": \"2026-04-17T12:00:00Z\","
29-
+ "\"decision\": \"deny\","
29+
+ "\"decision\": \"blocked\","
3030
+ "\"reason\": \"SQL injection detected\","
3131
+ "\"risk_level\": \"high\","
3232
+ "\"policy_matches\": [{"
@@ -87,7 +87,7 @@ void parsesFullPayload() {
8787
DecisionExplanation exp = axonflow.explainDecision("dec_wf1_step2");
8888

8989
assertThat(exp.getDecisionId()).isEqualTo("dec_wf1_step2");
90-
assertThat(exp.getDecision()).isEqualTo("deny");
90+
assertThat(exp.getDecision()).isEqualTo("blocked");
9191
assertThat(exp.getReason()).isEqualTo("SQL injection detected");
9292
assertThat(exp.getRiskLevel()).isEqualTo("high");
9393
assertThat(exp.getPolicyMatches()).hasSize(1);
@@ -108,7 +108,7 @@ void surfacesRequestContext() {
108108
"{"
109109
+ "\"decision_id\": \"dec-ctx\","
110110
+ "\"timestamp\": \"2026-05-30T12:00:00Z\","
111-
+ "\"decision\": \"deny\","
111+
+ "\"decision\": \"blocked\","
112112
+ "\"reason\": \"\","
113113
+ "\"policy_matches\": [],"
114114
+ "\"override_available\": false,"
@@ -133,7 +133,7 @@ void surfacesRequestContext() {
133133
void contextAbsentDefaults() {
134134
String body =
135135
"{\"decision_id\":\"dec-1\",\"timestamp\":\"2026-04-17T12:00:00Z\","
136-
+ "\"decision\":\"allow\",\"reason\":\"\",\"policy_matches\":[],"
136+
+ "\"decision\":\"allowed\",\"reason\":\"\",\"policy_matches\":[],"
137137
+ "\"override_available\":false,\"historical_hit_count_session\":0}";
138138
stubFor(
139139
get(urlEqualTo("/api/v1/decisions/dec-1/explain"))
@@ -240,7 +240,7 @@ void decisionExplanationGetters() {
240240
java.time.Instant.now(),
241241
null, // policyMatches null should default to empty
242242
null,
243-
"allow",
243+
"allowed",
244244
"",
245245
null,
246246
false,

src/test/java/com/getaxonflow/sdk/ListDecisionsTest.java

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -45,23 +45,23 @@ void happyPath() {
4545
.withBody(
4646
"{\"decisions\":["
4747
+ "{\"decision_id\":\"dec-1\",\"timestamp\":\"2026-05-07T12:00:00Z\","
48-
+ "\"decision\":\"deny\",\"policy_id\":\"pol-sqli\","
48+
+ "\"decision\":\"blocked\",\"policy_id\":\"pol-sqli\","
4949
+ "\"tool_signature\":\"postgres.query\"},"
5050
+ "{\"decision_id\":\"dec-2\",\"timestamp\":\"2026-05-07T11:00:00Z\","
51-
+ "\"decision\":\"allow\",\"policy_id\":\"pol-default\","
51+
+ "\"decision\":\"allowed\",\"policy_id\":\"pol-default\","
5252
+ "\"tool_signature\":\"github.status\"},"
5353
+ "{\"decision_id\":\"dec-3\",\"timestamp\":\"2026-05-07T10:00:00Z\","
54-
+ "\"decision\":\"require_approval\",\"policy_id\":\"pol-amount\","
54+
+ "\"decision\":\"needs_approval\",\"policy_id\":\"pol-amount\","
5555
+ "\"tool_signature\":\"stripe.charge\"}"
5656
+ "]}")));
5757

5858
List<DecisionSummary> got = axonflow.listDecisions(null);
5959
assertThat(got).hasSize(3);
6060
assertThat(got.get(0).getDecisionId()).isEqualTo("dec-1");
61-
assertThat(got.get(0).getDecision()).isEqualTo("deny");
61+
assertThat(got.get(0).getDecision()).isEqualTo("blocked");
6262
assertThat(got.get(0).getPolicyId()).isEqualTo("pol-sqli");
6363
assertThat(got.get(0).getToolSignature()).isEqualTo("postgres.query");
64-
assertThat(got.get(2).getDecision()).isEqualTo("require_approval");
64+
assertThat(got.get(2).getDecision()).isEqualTo("needs_approval");
6565
}
6666

6767
@Test
@@ -76,11 +76,11 @@ void surfacesRequestContext() {
7676
.withBody(
7777
"{\"decisions\":["
7878
+ "{\"decision_id\":\"dec-ctx\",\"timestamp\":\"2026-05-30T12:00:00Z\","
79-
+ "\"decision\":\"deny\",\"context\":{"
79+
+ "\"decision\":\"blocked\",\"context\":{"
8080
+ "\"x_ai_agent\":\"refund-bot\",\"x_session_id\":\"sess-42\","
8181
+ "\"x_leader_identity\":\"ops-lead\"}},"
8282
+ "{\"decision_id\":\"dec-noctx\",\"timestamp\":\"2026-05-30T11:00:00Z\","
83-
+ "\"decision\":\"allow\"}"
83+
+ "\"decision\":\"allowed\"}"
8484
+ "]}")));
8585

8686
List<DecisionSummary> got = axonflow.listDecisions(null);
@@ -100,7 +100,7 @@ void filterSerialization() {
100100
stubFor(
101101
get(urlPathEqualTo("/api/v1/decisions"))
102102
.withQueryParam("since", equalTo("2026-05-07T00:00:00Z"))
103-
.withQueryParam("decision", equalTo("deny"))
103+
.withQueryParam("decision", equalTo("blocked"))
104104
.withQueryParam("policy_id", equalTo("pol-sqli"))
105105
.withQueryParam("tool_signature", equalTo("postgres.query"))
106106
.withQueryParam("limit", equalTo("25"))
@@ -109,7 +109,7 @@ void filterSerialization() {
109109
ListDecisionsOptions opts =
110110
ListDecisionsOptions.builder()
111111
.since(Instant.parse("2026-05-07T00:00:00Z"))
112-
.decision("deny")
112+
.decision("blocked")
113113
.policyId("pol-sqli")
114114
.toolSignature("postgres.query")
115115
.limit(25)
@@ -123,15 +123,15 @@ void filterSerialization() {
123123
void omitsUnsetFilters() {
124124
stubFor(
125125
get(urlPathEqualTo("/api/v1/decisions"))
126-
.withQueryParam("decision", equalTo("deny"))
126+
.withQueryParam("decision", equalTo("blocked"))
127127
// wiremock fails the test if the URL contains any of these:
128128
.withQueryParam("policy_id", absent())
129129
.withQueryParam("tool_signature", absent())
130130
.withQueryParam("limit", absent())
131131
.withQueryParam("since", absent())
132132
.willReturn(aResponse().withStatus(200).withBody("{\"decisions\":[]}")));
133133

134-
axonflow.listDecisions(ListDecisionsOptions.builder().decision("deny").build());
134+
axonflow.listDecisions(ListDecisionsOptions.builder().decision("blocked").build());
135135
}
136136

137137
@Test
@@ -219,7 +219,7 @@ void forwardCompat() {
219219
"{\"decisions\":[{"
220220
+ "\"decision_id\":\"dec-fwd\","
221221
+ "\"timestamp\":\"2026-05-07T12:00:00Z\","
222-
+ "\"decision\":\"deny\","
222+
+ "\"decision\":\"blocked\","
223223
+ "\"policy_id\":\"pol-x\","
224224
+ "\"tool_signature\":\"tool-x\","
225225
+ "\"policy_version\":7,"
@@ -244,7 +244,7 @@ void summaryMinimalShape() {
244244
"{\"decisions\":[{"
245245
+ "\"decision_id\":\"dec-min\","
246246
+ "\"timestamp\":\"2026-05-07T12:00:00Z\","
247-
+ "\"decision\":\"deny\""
247+
+ "\"decision\":\"blocked\""
248248
+ "}]}")));
249249

250250
List<DecisionSummary> got = axonflow.listDecisions(null);
@@ -263,8 +263,8 @@ void buildQueryEmpty() {
263263
@DisplayName("buildListDecisionsQuery — partial options omit None fields")
264264
void buildQueryPartial() {
265265
ListDecisionsOptions opts =
266-
ListDecisionsOptions.builder().decision("deny").limit(7).build();
267-
assertThat(AxonFlow.buildListDecisionsQuery(opts)).isEqualTo("?decision=deny&limit=7");
266+
ListDecisionsOptions.builder().decision("blocked").limit(7).build();
267+
assertThat(AxonFlow.buildListDecisionsQuery(opts)).isEqualTo("?decision=blocked&limit=7");
268268
}
269269

270270
@Test

0 commit comments

Comments
 (0)