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
60 changes: 60 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,66 @@ All notable changes to the AxonFlow Java SDK will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [8.5.0] - 2026-06-09 — Decision Mode PEP: decide → fulfill → forward

Adds the SDK analog of the platform PEP client (`platform/shared/pep`, ADR-056,
epic #2563). A Policy Enforcement Point now follows one path —
**decide → fulfill → forward** — and the SDK makes the engine-fulfillable
obligation contract impossible to misuse: there is **no local redaction path**,
so a `redact_pii` obligation can only be discharged by round-tripping content
through the engine endpoint the obligation names.

This is a minor, additive release (the SDK's semver is decoupled from the
platform's).

### Added

- **`AxonFlow.decide(DecideRequest)`** — the PDP step. `POST /api/v1/decide`
returns a `DecideResponse` whose `getObligations()` is always a (possibly
empty) list of self-describing `Obligation`s. Decision Mode auth is HTTP Basic
(org:license), which the client already sends; wrong/demo credentials are
refused with `AuthenticationException`. A `deny` verdict is returned in the
body (HTTP 200), not as an error. `decideAsync(...)` mirror provided.
- **`AxonFlow.fulfillRequest(DecideResponse, String)`** — discharges every
request-phase `redact_pii` obligation by POSTing the statement to the engine's
`check-input` endpoint and returning the **engine-redacted** statement
(`FulfillResult`: content + `didRedact()`). Fails closed with
`ObligationNotFulfillableException` when an obligation names no request-phase
fulfillment, advertises a content-type the PEP is not holding, names an
endpoint the client will not call, the engine call fails, or the engine reports
`redaction_evaluated=false`. Never redacts locally.
- **`AxonFlow.decideAndFulfill(DecideRequest)`** — the blessed one-call path
(decide, then fulfill any request-phase obligation; `DecideAndFulfillResult`
carries verdict, content, and decision); fail-closed by construction.
`decideAndFulfillAsync(...)` mirror provided.
- **New types**: `DecideRequest` (fluent builder), `DecideResponse`,
`Obligation`, `ObligationFulfillment`, `DecisionCallerIdentity`,
`DecisionTarget`.
- **New exception**: `ObligationNotFulfillableException` (a fail-closed signal,
extends `AxonFlowException`).
- **PEP constants + `Pep.hasRequestRedaction(List<Obligation>)` helper**
(`OBLIGATION_REDACT_PII`, `PHASE_REQUEST`/`PHASE_RESPONSE`,
`CONTENT_TYPE_TEXT`, `VERDICT_ALLOW`/`VERDICT_DENY`/`VERDICT_NEEDS_APPROVAL`,
endpoint-path constants).
- **`redacted` / `redactedStatement` / `redactionEvaluated` on
`MCPCheckInputResponse`** and **`redactionEvaluated` on
`MCPCheckOutputResponse`** — the request-redaction contract fields the agent
emits (ADR-056). A PEP fulfilling an obligation fails closed when
`redactionEvaluated` is false.
- **`contentType` on `MCPCheckInputRequest`** (new 5-arg constructor) and a
`content_type` option on `mcpCheckInput(connectorType, statement, options)` —
selects the request-redaction detector (defaults to `text/plain`
server-side).

### Notes

- Wire field names are byte-identical across the Go / Python / TypeScript / Java
SDKs (snake_case on the wire). The new MCP response fields are an acknowledged
SDK superset of the pinned community OpenAPI spec; the wire-shape baseline is
annotated without bumping the pinned spec SHA.
- Existing source-compatible `MCPCheckInputResponse` / `MCPCheckOutputResponse`
constructors are preserved; the new fields default to `false` / `null`.

## [8.4.0] - 2026-05-30 — Decision request context + Pasal 56(b) transfer basis

Targets AxonFlow platform **v8.5.0**.
Expand Down
2 changes: 1 addition & 1 deletion examples/explain-decision/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
<dependency>
<groupId>com.getaxonflow</groupId>
<artifactId>axonflow-sdk</artifactId>
<version>8.4.0</version>
<version>8.5.0</version>
</dependency>
</dependencies>

Expand Down
2 changes: 1 addition & 1 deletion examples/list-decisions/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
<dependency>
<groupId>com.getaxonflow</groupId>
<artifactId>axonflow-sdk</artifactId>
<version>8.4.0</version>
<version>8.5.0</version>
</dependency>
</dependencies>

Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>com.getaxonflow</groupId>
<artifactId>axonflow-sdk</artifactId>
<version>8.4.0</version>
<version>8.5.0</version>
<packaging>jar</packaging>

<name>AxonFlow Java SDK</name>
Expand Down
135 changes: 135 additions & 0 deletions runtime-e2e/decide_fulfill_obligation/DecideFulfillObligationTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* runtime-e2e/decide_fulfill_obligation/DecideFulfillObligationTest.java
*
* Real-wire test of the Decision Mode PEP surface (ADR-056, epic #2563,
* tracking #2571) against a running AxonFlow enterprise agent.
*
* Proves, with NO mocks, that the SDK can run the decide -> fulfill -> forward
* path end-to-end:
*
* 1. decide() on a PII-bearing query returns an allow verdict carrying a
* request-phase redact_pii obligation whose fulfillment names the
* request-redaction engine endpoint.
* 2. fulfillRequest() discharges that obligation through the engine and
* returns engine-masked content in which neither the email
* (john.doe@example.com) nor the card (4111111111111111) survives, and
* the content differs from the original. (No local redaction exists in
* the SDK — only the engine can produce this.)
* 3. decideAndFulfill() yields the same masked content in one call.
* 4. Demo credentials (demo-org / demo-license-not-real) are refused with an
* AuthenticationException (HTTP 401).
*
* Run:
* source /tmp/axonflow-e2e-env.sh
* mvn -q -DskipTests package
* mvn -q -DskipTests dependency:build-classpath -Dmdep.outputFile=/tmp/cp.txt
* SDK_JAR=$(ls target/axonflow-sdk-*.jar | grep -v sources | grep -v javadoc | head -1)
* java -cp "$SDK_JAR:$(cat /tmp/cp.txt)" \
* runtime-e2e/decide_fulfill_obligation/DecideFulfillObligationTest.java
*/
import com.getaxonflow.sdk.AxonFlow;
import com.getaxonflow.sdk.AxonFlowConfig;
import com.getaxonflow.sdk.Pep;
import com.getaxonflow.sdk.exceptions.AuthenticationException;
import com.getaxonflow.sdk.types.DecideRequest;
import com.getaxonflow.sdk.types.DecideResponse;
import com.getaxonflow.sdk.types.DecisionTarget;

public class DecideFulfillObligationTest {

static final String EMAIL = "john.doe@example.com";
static final String CARD = "4111111111111111";
static final String QUERY = "Send the receipt to " + EMAIL + " and charge card " + CARD;

static void fail(String msg) {
System.err.println("FAIL: " + msg);
System.exit(1);
}

static void check(boolean cond, String msg) {
if (!cond) {
fail(msg);
}
}

public static void main(String[] args) {
String endpoint = System.getenv().getOrDefault("AXONFLOW_ENDPOINT", "http://localhost:8080");
String clientId = System.getenv("AXONFLOW_CLIENT_ID");
String clientSecret = System.getenv("AXONFLOW_CLIENT_SECRET");
String tenantId = System.getenv("AXONFLOW_TENANT_ID");
String userToken = System.getenv("AXONFLOW_USER_TOKEN");
if (clientId == null || clientSecret == null) {
fail("AXONFLOW_CLIENT_ID / AXONFLOW_CLIENT_SECRET unset — source /tmp/axonflow-e2e-env.sh");
}

AxonFlow client =
AxonFlow.create(
AxonFlowConfig.builder()
.endpoint(endpoint)
.clientId(clientId)
.clientSecret(clientSecret)
.build());

DecideRequest req =
DecideRequest.builder("tool", QUERY)
.target(new DecisionTarget("tool", null, null, "send_receipt"))
.userToken(userToken)
.build();

// 1. decide -> allow + request-phase redact_pii obligation.
DecideResponse decision = client.decide(req);
System.out.println(
"decide -> verdict="
+ decision.getVerdict()
+ " decision_id="
+ decision.getDecisionId()
+ " obligations="
+ decision.getObligations().size()
+ " evaluated_policies="
+ decision.getEvaluatedPolicies());
check(Pep.VERDICT_ALLOW.equals(decision.getVerdict()), "expected allow, got " + decision.getVerdict());
check(
Pep.hasRequestRedaction(decision.getObligations()),
"expected a request-phase redact_pii obligation, got " + decision.getObligations());
System.out.println("PASS step 1: decide returned allow + redact_pii request-phase obligation");

// 2. fulfillRequest -> engine-masked content; PII must NOT survive.
AxonFlow.FulfillResult fr = client.fulfillRequest(decision, QUERY);
System.out.println("fulfillRequest -> didRedact=" + fr.didRedact() + " content=" + fr.getContent());
assertMasked(fr.getContent());
check(fr.didRedact(), "expected the engine to have changed the content (didRedact=true)");
System.out.println("PASS step 2: fulfillRequest masked email + card via the engine (no local redaction)");

// 3. decideAndFulfill -> same masked content in one call.
AxonFlow.DecideAndFulfillResult daf = client.decideAndFulfill(req);
System.out.println(
"decideAndFulfill -> verdict=" + daf.getVerdict() + " content=" + daf.getContent());
check(Pep.VERDICT_ALLOW.equals(daf.getVerdict()), "decideAndFulfill verdict=" + daf.getVerdict());
assertMasked(daf.getContent());
System.out.println("PASS step 3: decideAndFulfill returned engine-masked content in one call");

// 4. Demo credentials are refused with 401.
AxonFlow demo =
AxonFlow.create(
AxonFlowConfig.builder()
.endpoint(endpoint)
.clientId("demo-org")
.clientSecret("demo-license-not-real")
.build());
try {
demo.decide(DecideRequest.builder("tool", "ping").build());
fail("expected demo credentials to be refused with AuthenticationException");
} catch (AuthenticationException e) {
System.out.println("PASS step 4: demo credentials refused -> AuthenticationException: " + e.getMessage());
}

System.out.println("ALL PASS: decide -> fulfill -> forward verified through the SDK against the live agent");
}

static void assertMasked(String content) {
check(content != null, "content is null");
check(!content.contains(EMAIL), "email '" + EMAIL + "' survived in: " + content);
check(!content.contains(CARD), "card '" + CARD + "' survived in: " + content);
check(!content.equals(QUERY), "content equals the original (no redaction happened): " + content);
}
}
31 changes: 31 additions & 0 deletions runtime-e2e/decide_fulfill_obligation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# decide_fulfill_obligation (v8.5.0 — Decision Mode PEP, #2563 / #2571)

Real-stack proof that the SDK runs the Decision Mode PEP path
**decide → fulfill → forward** against a live AxonFlow enterprise agent, with
NO mocks and NO local redaction:

1. **`decide()`** on the PII-bearing query
`"Send the receipt to john.doe@example.com and charge card 4111111111111111"`
returns an `allow` verdict carrying a request-phase `redact_pii` obligation
whose fulfillment names the request-redaction engine endpoint.
2. **`fulfillRequest()`** discharges that obligation through the engine and
returns engine-masked content in which neither `john.doe@example.com` nor
`4111111111111111` survives, and the content differs from the original. The
SDK has no local redaction path — only the engine can produce this.
3. **`decideAndFulfill()`** yields the same masked content in one call.
4. **Demo credentials** (`demo-org` / `demo-license-not-real`) are refused with
an `AuthenticationException` (HTTP 401).

## Run

```
source /tmp/axonflow-e2e-env.sh # AXONFLOW_CLIENT_ID / _SECRET / _TENANT_ID / _USER_TOKEN
mvn -q -DskipTests package
mvn -q -DskipTests dependency:build-classpath -Dmdep.outputFile=/tmp/cp.txt
SDK_JAR=$(ls target/axonflow-sdk-*.jar | grep -v sources | grep -v javadoc | head -1)
java -cp "$SDK_JAR:$(cat /tmp/cp.txt)" \
runtime-e2e/decide_fulfill_obligation/DecideFulfillObligationTest.java
```

Exits non-zero (and prints `FAIL: ...`) if any step fails — e.g. if the PII
survives fulfillment or demo credentials are not refused.
Loading
Loading