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
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,21 @@ 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).

## [2.1.2] - 2026-01-07

### Fixed

- **Gateway Mode clientId not sent in request body**: Fixed `getPolicyApprovedContext()` to auto-populate `client_id` in request body from config when not explicitly provided
- Server requires `client_id` in JSON body for `/api/policy/pre-check` endpoint
- Previously only sent as header (X-Client-ID), causing "client_id field is required" errors
- Now matches Go SDK behavior which auto-populates from `config.ClientID`
- Affects all Gateway Mode pre-check calls

- **executePlan() using non-existent endpoint**: Fixed `executePlan()` to use correct Agent API endpoint
- Changed from `/api/v1/orchestrator/plan/{planId}/execute` (404) to `/api/request` with `request_type: "execute-plan"`
- Now matches Go SDK pattern for plan execution
- Fixes MAP (Multi-Agent Planning) two-step execution flow

## [2.1.1] - 2026-01-06

### Fixed
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>2.1.1</version>
<version>2.1.2</version>
<packaging>jar</packaging>

<name>AxonFlow Java SDK</name>
Expand Down
64 changes: 60 additions & 4 deletions src/main/java/com/getaxonflow/sdk/AxonFlow.java
Original file line number Diff line number Diff line change
Expand Up @@ -261,8 +261,23 @@ public PolicyApprovalResult getPolicyApprovedContext(PolicyApprovalRequest reque
// Gateway Mode: Let server decide if credentials are required based on DEPLOYMENT_MODE
// Community/self-hosted deployments work without credentials

// Auto-populate clientId from config if not set in request (matches Go SDK behavior)
PolicyApprovalRequest effectiveRequest = request;
if ((request.getClientId() == null || request.getClientId().isEmpty())
&& config.getClientId() != null && !config.getClientId().isEmpty()) {
Map<String, Object> ctx = request.getContext();
effectiveRequest = PolicyApprovalRequest.builder()
.userToken(request.getUserToken())
.query(request.getQuery())
.dataSources(request.getDataSources())
.context(ctx == null || ctx.isEmpty() ? null : ctx)
.clientId(config.getClientId())
.build();
}

final PolicyApprovalRequest finalRequest = effectiveRequest;
return retryExecutor.execute(() -> {
Request httpRequest = buildRequest("POST", "/api/policy/pre-check", request);
Request httpRequest = buildRequest("POST", "/api/policy/pre-check", finalRequest);
try (Response response = httpClient.newCall(httpRequest).execute()) {
PolicyApprovalResult result = parseResponse(response, PolicyApprovalResult.class);

Expand Down Expand Up @@ -648,14 +663,55 @@ public PlanResponse executePlan(String planId) {
Objects.requireNonNull(planId, "planId cannot be null");

return retryExecutor.execute(() -> {
Request httpRequest = buildRequest("POST",
"/api/v1/orchestrator/plan/" + planId + "/execute", null);
// Build agent request format - like generatePlan but with request_type "execute-plan"
String userToken = config.getClientId() != null ? config.getClientId() : "default";
String clientId = config.getClientId() != null ? config.getClientId() : "default";

Map<String, Object> agentRequest = new java.util.HashMap<>();
agentRequest.put("query", "");
agentRequest.put("user_token", userToken);
agentRequest.put("client_id", clientId);
agentRequest.put("request_type", "execute-plan");
agentRequest.put("context", Map.of("plan_id", planId));

Request httpRequest = buildRequest("POST", "/api/request", agentRequest);
try (Response response = httpClient.newCall(httpRequest).execute()) {
return parseResponse(response, PlanResponse.class);
return parseExecutePlanResponse(response, planId);
}
}, "executePlan");
}

/**
* Parses the execute plan response.
*/
@SuppressWarnings("unchecked")
private PlanResponse parseExecutePlanResponse(Response response, String planId) throws IOException {
handleErrorResponse(response);

ResponseBody body = response.body();
if (body == null) {
throw new AxonFlowException("Empty response body", response.code(), null);
}

String json = body.string();
Map<String, Object> agentResponse = objectMapper.readValue(json,
new TypeReference<Map<String, Object>>() {});

// Check for errors
Boolean success = (Boolean) agentResponse.get("success");
if (success == null || !success) {
String error = (String) agentResponse.get("error");
throw new PlanExecutionException(error != null ? error : "Plan execution failed");
}

// Extract result - this is the completed plan output
String result = (String) agentResponse.get("result");

// Build response with execution status
return new PlanResponse(planId, Collections.emptyList(), null, null, null,
null, null, "completed", result);
}

/**
* Gets the status of a plan.
*
Expand Down
76 changes: 73 additions & 3 deletions src/test/java/com/getaxonflow/sdk/AxonFlowTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,69 @@ void getPolicyApprovedContextAsyncShouldReturnFuture() throws Exception {
assertThat(result.isApproved()).isTrue();
}

@Test
@DisplayName("getPolicyApprovedContext should auto-populate clientId from config")
void getPolicyApprovedContextShouldAutoPopulateClientId(WireMockRuntimeInfo wmRuntimeInfo) {
// Create client with clientId configured
AxonFlow client = AxonFlow.create(AxonFlowConfig.builder()
.endpoint(wmRuntimeInfo.getHttpBaseUrl())
.clientId("my-client-id")
.clientSecret("my-secret")
.build());

stubFor(post(urlEqualTo("/api/policy/pre-check"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("{\"context_id\":\"ctx_123\",\"approved\":true}")));

// Request WITHOUT explicit clientId - SDK should auto-populate from config
PolicyApprovalRequest request = PolicyApprovalRequest.builder()
.userToken("user-123")
.query("What is the capital of France?")
.build();

PolicyApprovalResult result = client.getPolicyApprovedContext(request);

assertThat(result.isApproved()).isTrue();

// Verify clientId was sent in request body (server requires this)
verify(postRequestedFor(urlEqualTo("/api/policy/pre-check"))
.withRequestBody(matchingJsonPath("$.client_id", equalTo("my-client-id"))));
}

@Test
@DisplayName("getPolicyApprovedContext should use explicit clientId if provided")
void getPolicyApprovedContextShouldUseExplicitClientId(WireMockRuntimeInfo wmRuntimeInfo) {
// Create client with clientId configured
AxonFlow client = AxonFlow.create(AxonFlowConfig.builder()
.endpoint(wmRuntimeInfo.getHttpBaseUrl())
.clientId("config-client-id")
.clientSecret("my-secret")
.build());

stubFor(post(urlEqualTo("/api/policy/pre-check"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("{\"context_id\":\"ctx_123\",\"approved\":true}")));

// Request WITH explicit clientId - should use this one, not config
PolicyApprovalRequest request = PolicyApprovalRequest.builder()
.userToken("user-123")
.query("What is the capital of France?")
.clientId("explicit-client-id")
.build();

PolicyApprovalResult result = client.getPolicyApprovedContext(request);

assertThat(result.isApproved()).isTrue();

// Verify explicit clientId was sent (not the config one)
verify(postRequestedFor(urlEqualTo("/api/policy/pre-check"))
.withRequestBody(matchingJsonPath("$.client_id", equalTo("explicit-client-id"))));
}

// ========================================================================
// Gateway Mode - Audit
// ========================================================================
Expand Down Expand Up @@ -257,18 +320,25 @@ void executePlanShouldRequirePlanId() {
}

@Test
@DisplayName("executePlan should execute plan")
@DisplayName("executePlan should execute plan via Agent API")
void executePlanShouldExecutePlan() {
stubFor(post(urlEqualTo("/api/v1/orchestrator/plan/plan_123/execute"))
// executePlan now uses /api/request with request_type: "execute-plan" (matches Go SDK)
stubFor(post(urlEqualTo("/api/request"))
.willReturn(aResponse()
.withStatus(200)
.withHeader("Content-Type", "application/json")
.withBody("{\"plan_id\":\"plan_123\",\"status\":\"completed\"}")));
.withBody("{\"success\":true,\"result\":\"Plan executed successfully\"}")));

PlanResponse response = axonflow.executePlan("plan_123");

assertThat(response.getPlanId()).isEqualTo("plan_123");
assertThat(response.getStatus()).isEqualTo("completed");
assertThat(response.getResult()).isEqualTo("Plan executed successfully");

// Verify correct request format
verify(postRequestedFor(urlEqualTo("/api/request"))
.withRequestBody(matchingJsonPath("$.request_type", equalTo("execute-plan")))
.withRequestBody(matchingJsonPath("$.context.plan_id", equalTo("plan_123"))));
}

@Test
Expand Down
Loading