Skip to content

Commit b02059c

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 - Verified with live examples (PII, SQLi, MAP, Execution Replay)
1 parent d37b347 commit b02059c

4 files changed

Lines changed: 148 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: 59 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -261,8 +261,22 @@ 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+
effectiveRequest = PolicyApprovalRequest.builder()
269+
.userToken(request.getUserToken())
270+
.query(request.getQuery())
271+
.dataSources(request.getDataSources())
272+
.context(request.getContext().isEmpty() ? null : request.getContext())
273+
.clientId(config.getClientId())
274+
.build();
275+
}
276+
277+
final PolicyApprovalRequest finalRequest = effectiveRequest;
264278
return retryExecutor.execute(() -> {
265-
Request httpRequest = buildRequest("POST", "/api/policy/pre-check", request);
279+
Request httpRequest = buildRequest("POST", "/api/policy/pre-check", finalRequest);
266280
try (Response response = httpClient.newCall(httpRequest).execute()) {
267281
PolicyApprovalResult result = parseResponse(response, PolicyApprovalResult.class);
268282

@@ -648,14 +662,55 @@ public PlanResponse executePlan(String planId) {
648662
Objects.requireNonNull(planId, "planId cannot be null");
649663

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

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

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)