Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
// mvn -q compile exec:java
//
// Optional filters via env:
// AXONFLOW_LIST_DECISION allow|deny|require_approval
// AXONFLOW_LIST_DECISION allowed|blocked|redacted|needs_approval|error
// (canonical audit verdicts, platform 9.0.0+;
// pre-9.0.0 allow|deny|require_approval now 400)
// AXONFLOW_LIST_POLICY_ID e.g. sys_sqli_stacked_drop
// AXONFLOW_LIST_LIMIT integer (server-capped per tier)
package com.getaxonflow.examples;
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/getaxonflow/sdk/AxonFlow.java
Original file line number Diff line number Diff line change
Expand Up @@ -756,7 +756,7 @@ public CompletableFuture<DecisionExplanation> explainDecisionAsync(String decisi
* <pre>{@code
* try {
* List<DecisionSummary> decisions = axonflow.listDecisions(
* ListDecisionsOptions.builder().decision("deny").limit(10).build());
* ListDecisionsOptions.builder().decision("blocked").limit(10).build());
* for (DecisionSummary d : decisions) {
* System.out.println(d.getDecisionId() + " " + d.getDecision());
* }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ public List<ExplainRule> getMatchedRules() {
return matchedRules;
}

/** Canonical audit verdict: allowed | blocked | redacted | needs_approval | error (9.0.0+). */
public String getDecision() {
return decision;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ public Instant getTimestamp() {
return timestamp;
}

/** allow | deny | require_approval */
/** Canonical audit verdict: allowed | blocked | redacted | needs_approval | error (9.0.0+). */
public String getDecision() {
return decision;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@
* Optional filters for {@code AxonFlow.listDecisions}.
*
* <p>Every field is optional; null values are omitted from the URL so the
* platform applies its tier-default page. {@code decision} must be one of
* {@code "allow"}, {@code "deny"}, or {@code "require_approval"} when set.
* platform applies its tier-default page. {@code decision}, when set, must be one
* of the canonical audit verdicts {@code "allowed"}, {@code "blocked"},
* {@code "redacted"}, {@code "needs_approval"}, or {@code "error"} (platform
* 9.0.0+); the pre-9.0.0 values {@code "allow"} / {@code "deny"} /
* {@code "require_approval"} are rejected with HTTP 400 by 9.0.0 (see
* https://docs.getaxonflow.com/docs/deployment/v8-to-v9-migration/).
* {@code limit} is server-capped per tier; over-cap requests yield a 429
* with the V1 upgrade envelope (surfaced as {@link
* com.getaxonflow.sdk.exceptions.RateLimitException} carrying upgrade info).
Expand All @@ -21,7 +25,7 @@
*
* <pre>{@code
* ListDecisionsOptions opts = ListDecisionsOptions.builder()
* .decision("deny")
* .decision("blocked")
* .limit(10)
* .build();
* List<DecisionSummary> decisions = client.listDecisions(opts);
Expand Down
10 changes: 5 additions & 5 deletions src/test/java/com/getaxonflow/sdk/DecisionExplainTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ class DecisionExplainTest {
"{"
+ "\"decision_id\": \"dec_wf1_step2\","
+ "\"timestamp\": \"2026-04-17T12:00:00Z\","
+ "\"decision\": \"deny\","
+ "\"decision\": \"blocked\","
+ "\"reason\": \"SQL injection detected\","
+ "\"risk_level\": \"high\","
+ "\"policy_matches\": [{"
Expand Down Expand Up @@ -87,7 +87,7 @@ void parsesFullPayload() {
DecisionExplanation exp = axonflow.explainDecision("dec_wf1_step2");

assertThat(exp.getDecisionId()).isEqualTo("dec_wf1_step2");
assertThat(exp.getDecision()).isEqualTo("deny");
assertThat(exp.getDecision()).isEqualTo("blocked");
assertThat(exp.getReason()).isEqualTo("SQL injection detected");
assertThat(exp.getRiskLevel()).isEqualTo("high");
assertThat(exp.getPolicyMatches()).hasSize(1);
Expand All @@ -108,7 +108,7 @@ void surfacesRequestContext() {
"{"
+ "\"decision_id\": \"dec-ctx\","
+ "\"timestamp\": \"2026-05-30T12:00:00Z\","
+ "\"decision\": \"deny\","
+ "\"decision\": \"blocked\","
+ "\"reason\": \"\","
+ "\"policy_matches\": [],"
+ "\"override_available\": false,"
Expand All @@ -133,7 +133,7 @@ void surfacesRequestContext() {
void contextAbsentDefaults() {
String body =
"{\"decision_id\":\"dec-1\",\"timestamp\":\"2026-04-17T12:00:00Z\","
+ "\"decision\":\"allow\",\"reason\":\"\",\"policy_matches\":[],"
+ "\"decision\":\"allowed\",\"reason\":\"\",\"policy_matches\":[],"
+ "\"override_available\":false,\"historical_hit_count_session\":0}";
stubFor(
get(urlEqualTo("/api/v1/decisions/dec-1/explain"))
Expand Down Expand Up @@ -240,7 +240,7 @@ void decisionExplanationGetters() {
java.time.Instant.now(),
null, // policyMatches null should default to empty
null,
"allow",
"allowed",
"",
null,
false,
Expand Down
30 changes: 15 additions & 15 deletions src/test/java/com/getaxonflow/sdk/ListDecisionsTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,23 +45,23 @@ void happyPath() {
.withBody(
"{\"decisions\":["
+ "{\"decision_id\":\"dec-1\",\"timestamp\":\"2026-05-07T12:00:00Z\","
+ "\"decision\":\"deny\",\"policy_id\":\"pol-sqli\","
+ "\"decision\":\"blocked\",\"policy_id\":\"pol-sqli\","
+ "\"tool_signature\":\"postgres.query\"},"
+ "{\"decision_id\":\"dec-2\",\"timestamp\":\"2026-05-07T11:00:00Z\","
+ "\"decision\":\"allow\",\"policy_id\":\"pol-default\","
+ "\"decision\":\"allowed\",\"policy_id\":\"pol-default\","
+ "\"tool_signature\":\"github.status\"},"
+ "{\"decision_id\":\"dec-3\",\"timestamp\":\"2026-05-07T10:00:00Z\","
+ "\"decision\":\"require_approval\",\"policy_id\":\"pol-amount\","
+ "\"decision\":\"needs_approval\",\"policy_id\":\"pol-amount\","
+ "\"tool_signature\":\"stripe.charge\"}"
+ "]}")));

List<DecisionSummary> got = axonflow.listDecisions(null);
assertThat(got).hasSize(3);
assertThat(got.get(0).getDecisionId()).isEqualTo("dec-1");
assertThat(got.get(0).getDecision()).isEqualTo("deny");
assertThat(got.get(0).getDecision()).isEqualTo("blocked");
assertThat(got.get(0).getPolicyId()).isEqualTo("pol-sqli");
assertThat(got.get(0).getToolSignature()).isEqualTo("postgres.query");
assertThat(got.get(2).getDecision()).isEqualTo("require_approval");
assertThat(got.get(2).getDecision()).isEqualTo("needs_approval");
}

@Test
Expand All @@ -76,11 +76,11 @@ void surfacesRequestContext() {
.withBody(
"{\"decisions\":["
+ "{\"decision_id\":\"dec-ctx\",\"timestamp\":\"2026-05-30T12:00:00Z\","
+ "\"decision\":\"deny\",\"context\":{"
+ "\"decision\":\"blocked\",\"context\":{"
+ "\"x_ai_agent\":\"refund-bot\",\"x_session_id\":\"sess-42\","
+ "\"x_leader_identity\":\"ops-lead\"}},"
+ "{\"decision_id\":\"dec-noctx\",\"timestamp\":\"2026-05-30T11:00:00Z\","
+ "\"decision\":\"allow\"}"
+ "\"decision\":\"allowed\"}"
+ "]}")));

List<DecisionSummary> got = axonflow.listDecisions(null);
Expand All @@ -100,7 +100,7 @@ void filterSerialization() {
stubFor(
get(urlPathEqualTo("/api/v1/decisions"))
.withQueryParam("since", equalTo("2026-05-07T00:00:00Z"))
.withQueryParam("decision", equalTo("deny"))
.withQueryParam("decision", equalTo("blocked"))
.withQueryParam("policy_id", equalTo("pol-sqli"))
.withQueryParam("tool_signature", equalTo("postgres.query"))
.withQueryParam("limit", equalTo("25"))
Expand All @@ -109,7 +109,7 @@ void filterSerialization() {
ListDecisionsOptions opts =
ListDecisionsOptions.builder()
.since(Instant.parse("2026-05-07T00:00:00Z"))
.decision("deny")
.decision("blocked")
.policyId("pol-sqli")
.toolSignature("postgres.query")
.limit(25)
Expand All @@ -123,15 +123,15 @@ void filterSerialization() {
void omitsUnsetFilters() {
stubFor(
get(urlPathEqualTo("/api/v1/decisions"))
.withQueryParam("decision", equalTo("deny"))
.withQueryParam("decision", equalTo("blocked"))
// wiremock fails the test if the URL contains any of these:
.withQueryParam("policy_id", absent())
.withQueryParam("tool_signature", absent())
.withQueryParam("limit", absent())
.withQueryParam("since", absent())
.willReturn(aResponse().withStatus(200).withBody("{\"decisions\":[]}")));

axonflow.listDecisions(ListDecisionsOptions.builder().decision("deny").build());
axonflow.listDecisions(ListDecisionsOptions.builder().decision("blocked").build());
}

@Test
Expand Down Expand Up @@ -219,7 +219,7 @@ void forwardCompat() {
"{\"decisions\":[{"
+ "\"decision_id\":\"dec-fwd\","
+ "\"timestamp\":\"2026-05-07T12:00:00Z\","
+ "\"decision\":\"deny\","
+ "\"decision\":\"blocked\","
+ "\"policy_id\":\"pol-x\","
+ "\"tool_signature\":\"tool-x\","
+ "\"policy_version\":7,"
Expand All @@ -244,7 +244,7 @@ void summaryMinimalShape() {
"{\"decisions\":[{"
+ "\"decision_id\":\"dec-min\","
+ "\"timestamp\":\"2026-05-07T12:00:00Z\","
+ "\"decision\":\"deny\""
+ "\"decision\":\"blocked\""
+ "}]}")));

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

@Test
Expand Down
Loading