Skip to content

Commit 2e135b8

Browse files
feat(release): v8.4.0 — decision request context + pasal_56b_dpa transfer basis (#190)
Targets AxonFlow platform v8.5.0 (epic #2508). - DecisionSummary + DecisionExplanation gain a context (Map<String, String>, nullable) surfacing the sanitized request context a PEP attaches to a Decision Mode call (platform #2509). listDecisions() returns the platform-truncated 5-key summary; explainDecision() returns the full map plus an isContextTruncated() boolean. Jackson resolves both via the @JsonCreator constructor; the wire-shape baseline is regenerated at the same pinned spec SHA. - AuditLogEntry adds TRANSFER_BASIS_* constants (adequacy, safeguards, pasal_56b_dpa, consent). The transfer_basis field stays a String surfaced verbatim, so existing 'safeguards' consumers are unaffected. - version 8.3.0 -> 8.4.0, CHANGELOG entry, JUnit tests, decisions example pom bumps, and a runtime-e2e driver that creates a decision via the PEP path and reads context back through the SDK. Signed-off-by: Saurabh Jain <saurabh.jain@getaxonflow.com>
1 parent 770dd70 commit 2e135b8

13 files changed

Lines changed: 398 additions & 7 deletions

File tree

CHANGELOG.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,35 @@ All notable changes to the AxonFlow Java SDK will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [8.4.0] - 2026-05-30 — Decision request context + Pasal 56(b) transfer basis
9+
10+
Targets AxonFlow platform **v8.5.0**.
11+
12+
### Added
13+
14+
- **`context` field on `DecisionSummary` and `DecisionExplanation`**
15+
`Map<String, String>` (nullable). Surfaces the sanitized request context a PEP
16+
attaches to a Decision Mode call (canonical `lower_snake_case` keys such as
17+
`x_ai_agent`, `x_session_id`, `x_leader_identity`, and `x-bukuwarung-*`),
18+
persisted by the platform at the audit row's `policy_details->'context'`.
19+
`listDecisions()` returns the platform-truncated summary (5 keys);
20+
`explainDecision()` returns the full map. `null` for pre-v8.4.0 audit rows.
21+
- **`contextTruncated` accessor on `DecisionExplanation`** (`boolean`,
22+
`isContextTruncated()`). True when the agent dropped surplus context keys at
23+
write time.
24+
- **`AuditLogEntry.TRANSFER_BASIS_*` constants** (`TRANSFER_BASIS_ADEQUACY`,
25+
`TRANSFER_BASIS_SAFEGUARDS`, `TRANSFER_BASIS_PASAL_56B_DPA` = `"pasal_56b_dpa"`,
26+
`TRANSFER_BASIS_CONSENT`). Type-safe access to the Indonesia UU PDP Pasal 56
27+
legal bases.
28+
29+
### Changed
30+
31+
- **`AuditLogEntry.getTransferBasis()` documentation** now records `pasal_56b_dpa`
32+
(Pasal 56(b) explicit DPA tag) alongside `adequacy`, `safeguards`, and
33+
`consent`. The field stays a `String` (surfaced verbatim), so existing code
34+
reading `safeguards` is unaffected and the SDK never rejects a value a newer
35+
platform may add.
36+
837
## [8.3.0] - 2026-05-27 — Indonesia PII category + cross-border audit fields
938

1039
### Added

examples/explain-decision/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
<dependency>
2626
<groupId>com.getaxonflow</groupId>
2727
<artifactId>axonflow-sdk</artifactId>
28-
<version>8.0.0</version>
28+
<version>8.4.0</version>
2929
</dependency>
3030
</dependencies>
3131

examples/list-decisions/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
<dependency>
2525
<groupId>com.getaxonflow</groupId>
2626
<artifactId>axonflow-sdk</artifactId>
27-
<version>8.0.0</version>
27+
<version>8.4.0</version>
2828
</dependency>
2929
</dependencies>
3030

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
<groupId>com.getaxonflow</groupId>
88
<artifactId>axonflow-sdk</artifactId>
9-
<version>8.3.0</version>
9+
<version>8.4.0</version>
1010
<packaging>jar</packaging>
1111

1212
<name>AxonFlow Java SDK</name>
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
/*
2+
* runtime-e2e/decision_context_transfer_basis/DecisionContextTransferBasisTest.java
3+
*
4+
* Real-wire test of the v8.4.0 SDK surface (platform #2509, epic #2508)
5+
* against a running AxonFlow agent:
6+
*
7+
* 1. DecisionSummary.getContext() / DecisionExplanation.getContext() surface
8+
* the sanitized request context a PEP attaches to a Decision Mode call. We
9+
* act as the PEP via a raw POST /api/v1/decide (that endpoint is not
10+
* SDK-wrapped per ADR-056), then read the decision back through the SDK's
11+
* listDecisions + explainDecision and assert getContext() is populated.
12+
* 2. AuditLogEntry transfer_basis = "pasal_56b_dpa" round-trips through
13+
* Jackson serialize -> deserialize verbatim.
14+
*
15+
* Run:
16+
* mvn -DskipTests dependency:build-classpath -Dmdep.outputFile=/tmp/cp.txt -q
17+
* mvn -DskipTests -q package # build the SDK jar
18+
* SDK_JAR=$(ls target/axonflow-sdk-*.jar | head -1)
19+
* CP="$SDK_JAR:$(cat /tmp/cp.txt)"
20+
* AXONFLOW_AGENT_URL=http://localhost:8080 \
21+
* AXONFLOW_TENANT_ID=buku-e-java-e2e AXONFLOW_TENANT_SECRET=buku-e-secret \
22+
* java -cp "$CP" \
23+
* runtime-e2e/decision_context_transfer_basis/DecisionContextTransferBasisTest.java
24+
*/
25+
import com.fasterxml.jackson.databind.ObjectMapper;
26+
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
27+
import com.getaxonflow.sdk.AxonFlow;
28+
import com.getaxonflow.sdk.AxonFlowConfig;
29+
import com.getaxonflow.sdk.types.AuditLogEntry;
30+
import com.getaxonflow.sdk.types.DecisionExplanation;
31+
import com.getaxonflow.sdk.types.DecisionSummary;
32+
import com.getaxonflow.sdk.types.ListDecisionsOptions;
33+
import java.net.URI;
34+
import java.net.http.HttpClient;
35+
import java.net.http.HttpRequest;
36+
import java.net.http.HttpResponse;
37+
import java.util.Base64;
38+
import java.util.List;
39+
import java.util.Map;
40+
41+
public class DecisionContextTransferBasisTest {
42+
43+
static void fail(String msg) {
44+
System.err.println("FAIL: " + msg);
45+
System.exit(1);
46+
}
47+
48+
public static void main(String[] args) throws Exception {
49+
String endpoint = System.getenv().getOrDefault("AXONFLOW_AGENT_URL", "http://localhost:8080");
50+
String clientId = System.getenv().getOrDefault("AXONFLOW_TENANT_ID", "buku-e-java-e2e");
51+
String secret = System.getenv().getOrDefault("AXONFLOW_TENANT_SECRET", "buku-e-secret");
52+
Map<String, String> want =
53+
Map.of(
54+
"x_ai_agent", "refund-bot",
55+
"x_session_id", "sess-buku-42",
56+
"x_leader_identity", "ops-lead");
57+
58+
// 1. PEP: create a decision carrying request context (body 'context' map).
59+
String decisionId = createDecision(endpoint, clientId, secret);
60+
System.out.println("PEP decide -> decision_id=" + decisionId);
61+
62+
AxonFlow client =
63+
AxonFlow.create(
64+
AxonFlowConfig.builder()
65+
.endpoint(endpoint)
66+
.clientId(clientId)
67+
.clientSecret(secret)
68+
.build());
69+
70+
// 2. Read it back through the SDK.
71+
List<DecisionSummary> rows =
72+
client.listDecisions(ListDecisionsOptions.builder().limit(5).build());
73+
DecisionSummary found =
74+
rows.stream().filter(r -> decisionId.equals(r.getDecisionId())).findFirst().orElse(null);
75+
if (found == null) {
76+
fail("listDecisions did not return " + decisionId + " (got " + rows.size() + " rows)");
77+
}
78+
System.out.println("SDK listDecisions -> context=" + found.getContext());
79+
if (found.getContext() == null || !found.getContext().entrySet().containsAll(want.entrySet())) {
80+
fail("listDecisions context = " + found.getContext() + ", want superset of " + want);
81+
}
82+
System.out.println(
83+
"PASS: listDecisions DecisionSummary.getContext() populated with "
84+
+ found.getContext().size()
85+
+ " PEP-forwarded keys");
86+
87+
DecisionExplanation exp = client.explainDecision(decisionId);
88+
System.out.println(
89+
"SDK explainDecision -> context="
90+
+ exp.getContext()
91+
+ " contextTruncated="
92+
+ exp.isContextTruncated());
93+
if (exp.getContext() == null || !exp.getContext().entrySet().containsAll(want.entrySet())) {
94+
fail("explainDecision context = " + exp.getContext());
95+
}
96+
System.out.println(
97+
"PASS: explainDecision returned full context (contextTruncated="
98+
+ exp.isContextTruncated()
99+
+ ")");
100+
101+
// 3. transfer_basis = pasal_56b_dpa round-trip (Pasal 56(b)).
102+
ObjectMapper mapper = new ObjectMapper().registerModule(new JavaTimeModule());
103+
String json =
104+
"{\"id\":\"e2e-audit\",\"timestamp\":\"2026-05-30T10:00:00Z\","
105+
+ "\"data_residency\":\"ID\",\"transfer_basis\":\""
106+
+ AuditLogEntry.TRANSFER_BASIS_PASAL_56B_DPA
107+
+ "\"}";
108+
AuditLogEntry entry = mapper.readValue(json, AuditLogEntry.class);
109+
String reserialized = mapper.writeValueAsString(entry);
110+
AuditLogEntry back = mapper.readValue(reserialized, AuditLogEntry.class);
111+
if (!"pasal_56b_dpa".equals(back.getTransferBasis())) {
112+
fail("transfer_basis round-trip = " + back.getTransferBasis() + ", want pasal_56b_dpa");
113+
}
114+
System.out.println("SDK AuditLogEntry round-trip -> " + reserialized);
115+
System.out.println(
116+
"PASS: AuditLogEntry.getTransferBasis() = \"" + back.getTransferBasis() + "\" round-trips verbatim");
117+
118+
System.out.println("ALL PASS: v8.4.0 context + pasal_56b_dpa verified through SDK runtime");
119+
}
120+
121+
/** Acts as the PEP: the request context lives in the body's 'context' map. */
122+
static String createDecision(String endpoint, String clientId, String secret) throws Exception {
123+
String body =
124+
"{\"stage\":\"llm\",\"query\":\"summarize this support ticket\","
125+
+ "\"target\":{\"type\":\"llm\",\"model\":\"gpt-4\",\"provider\":\"openai\"},"
126+
+ "\"context\":{\"x-ai-agent\":\"refund-bot\",\"x-session-id\":\"sess-buku-42\","
127+
+ "\"x-leader-identity\":\"ops-lead\"}}";
128+
String auth = Base64.getEncoder().encodeToString((clientId + ":" + secret).getBytes());
129+
HttpRequest req =
130+
HttpRequest.newBuilder(URI.create(endpoint + "/api/v1/decide"))
131+
.header("Content-Type", "application/json")
132+
.header("X-Client-ID", clientId)
133+
.header("Authorization", "Basic " + auth)
134+
.POST(HttpRequest.BodyPublishers.ofString(body))
135+
.build();
136+
HttpResponse<String> resp =
137+
HttpClient.newHttpClient().send(req, HttpResponse.BodyHandlers.ofString());
138+
if (resp.statusCode() != 200) {
139+
fail("decide HTTP " + resp.statusCode() + ": " + resp.body());
140+
}
141+
System.out.println("server /decide response: " + resp.body());
142+
String marker = "\"decision_id\":\"";
143+
int i = resp.body().indexOf(marker);
144+
if (i < 0) {
145+
fail("no decision_id in response: " + resp.body());
146+
}
147+
int start = i + marker.length();
148+
return resp.body().substring(start, resp.body().indexOf('"', start));
149+
}
150+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# decision_context_transfer_basis (v8.4.0)
2+
3+
Real-stack proof for the v8.4.0 SDK surface (platform epic #2508):
4+
5+
- **`DecisionSummary.getContext()` / `DecisionExplanation.getContext()`
6+
(+ `isContextTruncated()`)** — the sanitized request context a PEP attaches to a
7+
Decision Mode call is surfaced back through `listDecisions` and `explainDecision`.
8+
- **`AuditLogEntry` `transfer_basis = "pasal_56b_dpa"`** — the Pasal 56(b) explicit
9+
DPA tag round-trips through Jackson verbatim.
10+
11+
The driver acts as the PEP (raw `POST /api/v1/decide` — that endpoint is not
12+
SDK-wrapped per ADR-056), then reads the decision back through the SDK against a
13+
real running agent.
14+
15+
## Run
16+
17+
```
18+
mvn -DskipTests dependency:build-classpath -Dmdep.outputFile=/tmp/cp.txt -q
19+
mvn -DskipTests -q package
20+
SDK_JAR=$(ls target/axonflow-sdk-*.jar | grep -v sources | grep -v javadoc | head -1)
21+
CP="$SDK_JAR:$(cat /tmp/cp.txt)"
22+
AXONFLOW_AGENT_URL=http://localhost:8080 \
23+
AXONFLOW_TENANT_ID=buku-e-java-e2e AXONFLOW_TENANT_SECRET=buku-e-secret \
24+
java -cp "$CP" runtime-e2e/decision_context_transfer_basis/DecisionContextTransferBasisTest.java
25+
```
26+
27+
Exits non-zero if the SDK does not surface the new fields.

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

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,28 @@
2727
@JsonIgnoreProperties(ignoreUnknown = true)
2828
public final class AuditLogEntry {
2929

30+
/**
31+
* Cross-border transfer-basis values recognized under Indonesia UU PDP Pasal 56,
32+
* for the {@link #getTransferBasis()} field:
33+
*
34+
* <ul>
35+
* <li>{@code adequacy} — Pasal 56(a): destination with adequate protection
36+
* <li>{@code safeguards} — Pasal 56(b): binding legal instrument (generic label)
37+
* <li>{@code pasal_56b_dpa} — Pasal 56(b): binding legal instrument, explicit DPA tag
38+
* <li>{@code consent} — Pasal 56(c): explicit data-subject consent
39+
* </ul>
40+
*
41+
* <p>{@code safeguards} and {@code pasal_56b_dpa} are semantic equivalents; the
42+
* platform surfaces whichever was recorded at decision time, verbatim. The field
43+
* itself stays a {@code String} so the SDK never rejects a value a newer platform
44+
* may add. (platform #2513 / epic #2508)
45+
*/
46+
public static final String TRANSFER_BASIS_ADEQUACY = "adequacy";
47+
48+
public static final String TRANSFER_BASIS_SAFEGUARDS = "safeguards";
49+
public static final String TRANSFER_BASIS_PASAL_56B_DPA = "pasal_56b_dpa";
50+
public static final String TRANSFER_BASIS_CONSENT = "consent";
51+
3052
@JsonProperty("id")
3153
private final String id;
3254

@@ -215,7 +237,12 @@ public String getDataResidency() {
215237
return dataResidency;
216238
}
217239

218-
/** Returns the cross-border transfer basis (adequacy, safeguards, or consent), or null if not set. */
240+
/**
241+
* Returns the cross-border transfer basis under Indonesia UU PDP Pasal 56
242+
* ({@code adequacy}, {@code safeguards}, {@code pasal_56b_dpa}, or
243+
* {@code consent}), or null if not set. Surfaced verbatim — see the
244+
* {@code TRANSFER_BASIS_*} constants.
245+
*/
219246
public String getTransferBasis() {
220247
return transferBasis;
221248
}

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

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import java.time.Instant;
1212
import java.util.Collections;
1313
import java.util.List;
14+
import java.util.Map;
1415

1516
/**
1617
* Canonical payload returned by {@code AxonFlow.explainDecision}.
@@ -36,6 +37,8 @@ public final class DecisionExplanation {
3637
private final int historicalHitCountSession;
3738
private final String policySourceLink;
3839
private final String toolSignature;
40+
private final Map<String, String> context;
41+
private final boolean contextTruncated;
3942

4043
@JsonCreator
4144
public DecisionExplanation(
@@ -50,7 +53,9 @@ public DecisionExplanation(
5053
@JsonProperty("override_existing_id") String overrideExistingId,
5154
@JsonProperty("historical_hit_count_session") int historicalHitCountSession,
5255
@JsonProperty("policy_source_link") String policySourceLink,
53-
@JsonProperty("tool_signature") String toolSignature) {
56+
@JsonProperty("tool_signature") String toolSignature,
57+
@JsonProperty("context") Map<String, String> context,
58+
@JsonProperty("context_truncated") boolean contextTruncated) {
5459
this.decisionId = decisionId;
5560
this.timestamp = timestamp;
5661
this.policyMatches = policyMatches != null ? policyMatches : Collections.emptyList();
@@ -63,6 +68,8 @@ public DecisionExplanation(
6368
this.historicalHitCountSession = historicalHitCountSession;
6469
this.policySourceLink = policySourceLink;
6570
this.toolSignature = toolSignature;
71+
this.context = context;
72+
this.contextTruncated = contextTruncated;
6673
}
6774

6875
public String getDecisionId() {
@@ -112,4 +119,21 @@ public String getPolicySourceLink() {
112119
public String getToolSignature() {
113120
return toolSignature;
114121
}
122+
123+
/**
124+
* The FULL sanitized request context the PEP attached to the decision
125+
* (canonical {@code lower_snake_case} keys, string values), read from the
126+
* audit row's {@code policy_details->'context'}. Unlike {@link DecisionSummary}
127+
* (truncated to 5 keys), explain returns every persisted key up to the
128+
* platform's 10-key cap. May be {@code null} for pre-v8.4.0 audit rows.
129+
* (platform #2509 / epic #2508)
130+
*/
131+
public Map<String, String> getContext() {
132+
return context;
133+
}
134+
135+
/** True when the agent dropped surplus context keys at write time. */
136+
public boolean isContextTruncated() {
137+
return contextTruncated;
138+
}
115139
}

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
1010
import com.fasterxml.jackson.annotation.JsonProperty;
1111
import java.time.Instant;
12+
import java.util.Map;
1213

1314
/**
1415
* Slim 5-field row returned by {@code AxonFlow.listDecisions}.
@@ -36,19 +37,22 @@ public final class DecisionSummary {
3637
private final String decision;
3738
private final String policyId;
3839
private final String toolSignature;
40+
private final Map<String, String> context;
3941

4042
@JsonCreator
4143
public DecisionSummary(
4244
@JsonProperty("decision_id") String decisionId,
4345
@JsonProperty("timestamp") Instant timestamp,
4446
@JsonProperty("decision") String decision,
4547
@JsonProperty("policy_id") String policyId,
46-
@JsonProperty("tool_signature") String toolSignature) {
48+
@JsonProperty("tool_signature") String toolSignature,
49+
@JsonProperty("context") Map<String, String> context) {
4750
this.decisionId = decisionId;
4851
this.timestamp = timestamp;
4952
this.decision = decision;
5053
this.policyId = policyId;
5154
this.toolSignature = toolSignature;
55+
this.context = context;
5256
}
5357

5458
public String getDecisionId() {
@@ -73,4 +77,16 @@ public String getPolicyId() {
7377
public String getToolSignature() {
7478
return toolSignature;
7579
}
80+
81+
/**
82+
* The sanitized request context the PEP attached to the decision (canonical
83+
* {@code lower_snake_case} keys, string values), surfaced from the audit
84+
* row's {@code policy_details->'context'}. The list summary is truncated by
85+
* the platform to the 5 most-correlated keys; the full map is available via
86+
* {@code AxonFlow.explainDecision}. May be {@code null} for pre-v8.4.0 audit
87+
* rows or decisions with no context. (platform #2509 / epic #2508)
88+
*/
89+
public Map<String, String> getContext() {
90+
return context;
91+
}
7692
}

0 commit comments

Comments
 (0)