Skip to content

Commit bbec0a8

Browse files
fix: gateway mode clientId and executePlan endpoint issues
## Issues Fixed 1. **Gateway Mode clientId not sent in request body** - `getPolicyApprovedContext()` now auto-populates `client_id` in request body from config - 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 2. **executePlan() using non-existent endpoint** - Changed from `/api/v1/orchestrator/plan/{planId}/execute` (404) - Now uses `/api/request` with `request_type: "execute-plan"` - Matches Go SDK pattern for plan execution - Fixes MAP (Multi-Agent Planning) two-step execution ## Testing - All 80 unit tests pass - Fixed potential NPE in context null check - Verified with live examples (PII, SQLi, MAP, Execution Replay)
1 parent d37b347 commit bbec0a8

4 files changed

Lines changed: 149 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,21 @@ 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+
## [2.1.2] - 2026-01-07
9+
10+
### Fixed
11+
12+
- **Gateway Mode clientId not sent in request body**: Fixed `getPolicyApprovedContext()` to auto-populate `client_id` in request body from config when not explicitly provided
13+
- Server requires `client_id` in JSON body for `/api/policy/pre-check` endpoint
14+
- Previously only sent as header (X-Client-ID), causing "client_id field is required" errors
15+
- Now matches Go SDK behavior which auto-populates from `config.ClientID`
16+
- Affects all Gateway Mode pre-check calls
17+
18+
- **executePlan() using non-existent endpoint**: Fixed `executePlan()` to use correct Agent API endpoint
19+
- Changed from `/api/v1/orchestrator/plan/{planId}/execute` (404) to `/api/request` with `request_type: "execute-plan"`
20+
- Now matches Go SDK pattern for plan execution
21+
- Fixes MAP (Multi-Agent Planning) two-step execution flow
22+
823
## [2.1.1] - 2026-01-06
924

1025
### Fixed

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>2.1.1</version>
9+
<version>2.1.2</version>
1010
<packaging>jar</packaging>
1111

1212
<name>AxonFlow Java SDK</name>

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

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -261,8 +261,23 @@ public PolicyApprovalResult getPolicyApprovedContext(PolicyApprovalRequest reque
261261
// Gateway Mode: Let server decide if credentials are required based on DEPLOYMENT_MODE
262262
// Community/self-hosted deployments work without credentials
263263

264+
// Auto-populate clientId from config if not set in request (matches Go SDK behavior)
265+
PolicyApprovalRequest effectiveRequest = request;
266+
if ((request.getClientId() == null || request.getClientId().isEmpty())
267+
&& config.getClientId() != null && !config.getClientId().isEmpty()) {
268+
Map<String, Object> ctx = request.getContext();
269+
effectiveRequest = PolicyApprovalRequest.builder()
270+
.userToken(request.getUserToken())
271+
.query(request.getQuery())
272+
.dataSources(request.getDataSources())
273+
.context(ctx == null || ctx.isEmpty() ? null : ctx)
274+
.clientId(config.getClientId())
275+
.build();
276+
}
277+
278+
final PolicyApprovalRequest finalRequest = effectiveRequest;
264279
return retryExecutor.execute(() -> {
265-
Request httpRequest = buildRequest("POST", "/api/policy/pre-check", request);
280+
Request httpRequest = buildRequest("POST", "/api/policy/pre-check", finalRequest);
266281
try (Response response = httpClient.newCall(httpRequest).execute()) {
267282
PolicyApprovalResult result = parseResponse(response, PolicyApprovalResult.class);
268283

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

650665
return retryExecutor.execute(() -> {
651-
Request httpRequest = buildRequest("POST",
652-
"/api/v1/orchestrator/plan/" + planId + "/execute", null);
666+
// Build agent request format - like generatePlan but with request_type "execute-plan"
667+
String userToken = config.getClientId() != null ? config.getClientId() : "default";
668+
String clientId = config.getClientId() != null ? config.getClientId() : "default";
669+
670+
Map<String, Object> agentRequest = new java.util.HashMap<>();
671+
agentRequest.put("query", "");
672+
agentRequest.put("user_token", userToken);
673+
agentRequest.put("client_id", clientId);
674+
agentRequest.put("request_type", "execute-plan");
675+
agentRequest.put("context", Map.of("plan_id", planId));
676+
677+
Request httpRequest = buildRequest("POST", "/api/request", agentRequest);
653678
try (Response response = httpClient.newCall(httpRequest).execute()) {
654-
return parseResponse(response, PlanResponse.class);
679+
return parseExecutePlanResponse(response, planId);
655680
}
656681
}, "executePlan");
657682
}
658683

684+
/**
685+
* Parses the execute plan response.
686+
*/
687+
@SuppressWarnings("unchecked")
688+
private PlanResponse parseExecutePlanResponse(Response response, String planId) throws IOException {
689+
handleErrorResponse(response);
690+
691+
ResponseBody body = response.body();
692+
if (body == null) {
693+
throw new AxonFlowException("Empty response body", response.code(), null);
694+
}
695+
696+
String json = body.string();
697+
Map<String, Object> agentResponse = objectMapper.readValue(json,
698+
new TypeReference<Map<String, Object>>() {});
699+
700+
// Check for errors
701+
Boolean success = (Boolean) agentResponse.get("success");
702+
if (success == null || !success) {
703+
String error = (String) agentResponse.get("error");
704+
throw new PlanExecutionException(error != null ? error : "Plan execution failed");
705+
}
706+
707+
// Extract result - this is the completed plan output
708+
String result = (String) agentResponse.get("result");
709+
710+
// Build response with execution status
711+
return new PlanResponse(planId, Collections.emptyList(), null, null, null,
712+
null, null, "completed", result);
713+
}
714+
659715
/**
660716
* Gets the status of a plan.
661717
*

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

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,69 @@ void getPolicyApprovedContextAsyncShouldReturnFuture() throws Exception {
158158
assertThat(result.isApproved()).isTrue();
159159
}
160160

161+
@Test
162+
@DisplayName("getPolicyApprovedContext should auto-populate clientId from config")
163+
void getPolicyApprovedContextShouldAutoPopulateClientId(WireMockRuntimeInfo wmRuntimeInfo) {
164+
// Create client with clientId configured
165+
AxonFlow client = AxonFlow.create(AxonFlowConfig.builder()
166+
.endpoint(wmRuntimeInfo.getHttpBaseUrl())
167+
.clientId("my-client-id")
168+
.clientSecret("my-secret")
169+
.build());
170+
171+
stubFor(post(urlEqualTo("/api/policy/pre-check"))
172+
.willReturn(aResponse()
173+
.withStatus(200)
174+
.withHeader("Content-Type", "application/json")
175+
.withBody("{\"context_id\":\"ctx_123\",\"approved\":true}")));
176+
177+
// Request WITHOUT explicit clientId - SDK should auto-populate from config
178+
PolicyApprovalRequest request = PolicyApprovalRequest.builder()
179+
.userToken("user-123")
180+
.query("What is the capital of France?")
181+
.build();
182+
183+
PolicyApprovalResult result = client.getPolicyApprovedContext(request);
184+
185+
assertThat(result.isApproved()).isTrue();
186+
187+
// Verify clientId was sent in request body (server requires this)
188+
verify(postRequestedFor(urlEqualTo("/api/policy/pre-check"))
189+
.withRequestBody(matchingJsonPath("$.client_id", equalTo("my-client-id"))));
190+
}
191+
192+
@Test
193+
@DisplayName("getPolicyApprovedContext should use explicit clientId if provided")
194+
void getPolicyApprovedContextShouldUseExplicitClientId(WireMockRuntimeInfo wmRuntimeInfo) {
195+
// Create client with clientId configured
196+
AxonFlow client = AxonFlow.create(AxonFlowConfig.builder()
197+
.endpoint(wmRuntimeInfo.getHttpBaseUrl())
198+
.clientId("config-client-id")
199+
.clientSecret("my-secret")
200+
.build());
201+
202+
stubFor(post(urlEqualTo("/api/policy/pre-check"))
203+
.willReturn(aResponse()
204+
.withStatus(200)
205+
.withHeader("Content-Type", "application/json")
206+
.withBody("{\"context_id\":\"ctx_123\",\"approved\":true}")));
207+
208+
// Request WITH explicit clientId - should use this one, not config
209+
PolicyApprovalRequest request = PolicyApprovalRequest.builder()
210+
.userToken("user-123")
211+
.query("What is the capital of France?")
212+
.clientId("explicit-client-id")
213+
.build();
214+
215+
PolicyApprovalResult result = client.getPolicyApprovedContext(request);
216+
217+
assertThat(result.isApproved()).isTrue();
218+
219+
// Verify explicit clientId was sent (not the config one)
220+
verify(postRequestedFor(urlEqualTo("/api/policy/pre-check"))
221+
.withRequestBody(matchingJsonPath("$.client_id", equalTo("explicit-client-id"))));
222+
}
223+
161224
// ========================================================================
162225
// Gateway Mode - Audit
163226
// ========================================================================
@@ -257,18 +320,25 @@ void executePlanShouldRequirePlanId() {
257320
}
258321

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

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

270334
assertThat(response.getPlanId()).isEqualTo("plan_123");
271335
assertThat(response.getStatus()).isEqualTo("completed");
336+
assertThat(response.getResult()).isEqualTo("Plan executed successfully");
337+
338+
// Verify correct request format
339+
verify(postRequestedFor(urlEqualTo("/api/request"))
340+
.withRequestBody(matchingJsonPath("$.request_type", equalTo("execute-plan")))
341+
.withRequestBody(matchingJsonPath("$.context.plan_id", equalTo("plan_123"))));
272342
}
273343

274344
@Test

0 commit comments

Comments
 (0)