diff --git a/src/main/java/com/getaxonflow/sdk/AxonFlow.java b/src/main/java/com/getaxonflow/sdk/AxonFlow.java index ccdef2b..cdcf0be 100644 --- a/src/main/java/com/getaxonflow/sdk/AxonFlow.java +++ b/src/main/java/com/getaxonflow/sdk/AxonFlow.java @@ -3365,6 +3365,7 @@ public void streamExecutionStatus( new Request.Builder() .url(url) .header("User-Agent", config.getUserAgent()) + .header("X-Axonflow-Client", config.getClientHeader()) .header("Accept", "text/event-stream") .get(); @@ -3573,6 +3574,7 @@ private Request buildRequest(String method, String path, Object body) { new Request.Builder() .url(url) .header("User-Agent", config.getUserAgent()) + .header("X-Axonflow-Client", config.getClientHeader()) .header("Accept", "application/json"); // Add authentication headers @@ -3624,6 +3626,7 @@ private Request buildPatchRequest(String path, Object body) { new Request.Builder() .url(url) .header("User-Agent", config.getUserAgent()) + .header("X-Axonflow-Client", config.getClientHeader()) .header("Accept", "application/json"); addAuthHeaders(builder); @@ -3763,6 +3766,10 @@ private void addAuthHeaders(Request.Builder builder) { String encoded = Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8)); builder.header("Authorization", "Basic " + encoded); + // ADR-050 §4: every governed request to the agent carries X-Axonflow-Client + // so the agent can derive request scope (sdk) and validate it against the + // token's aud.scope via HasScope(). Sourced from SDK_VERSION; no env override. + builder.header("X-Axonflow-Client", config.getClientHeader()); } /** @@ -4372,6 +4379,7 @@ private Request buildOrchestratorRequest(String method, String path, Object body new Request.Builder() .url(url) .header("User-Agent", config.getUserAgent()) + .header("X-Axonflow-Client", config.getClientHeader()) .header("Accept", "application/json"); addAuthHeaders(builder); @@ -4440,6 +4448,7 @@ private Request buildPortalRequest(String method, String path, Object body) { new Request.Builder() .url(url) .header("User-Agent", config.getUserAgent()) + .header("X-Axonflow-Client", config.getClientHeader()) .header("Accept", "application/json"); addPortalSessionCookie(builder); diff --git a/src/main/java/com/getaxonflow/sdk/AxonFlowConfig.java b/src/main/java/com/getaxonflow/sdk/AxonFlowConfig.java index 2dc0fb3..90e44d8 100644 --- a/src/main/java/com/getaxonflow/sdk/AxonFlowConfig.java +++ b/src/main/java/com/getaxonflow/sdk/AxonFlowConfig.java @@ -292,6 +292,20 @@ public String getUserAgent() { return userAgent; } + /** + * Returns the X-Axonflow-Client header value identifying this SDK + version. + * + *
Per ADR-050 §4, every governed request to the agent carries this header so the agent can
+ * derive request scope (sdk) and validate it against the token's aud.scope via HasScope().
+ * Sourced from the bundled {@link #SDK_VERSION}; there is intentionally no env / config
+ * override (the consumer doesn't get to spoof its own client identity to the agent).
+ *
+ * @return the agent-parseable {@code "sdk-java/ Asserts every governed request forwards {@code X-Axonflow-Client:
+ * sdk-java/ Header value is sourced from the bundled {@link AxonFlowConfig#SDK_VERSION}; the consumer
+ * cannot spoof its own client identity through config (intentional — honest-99% header injection
+ * per ADR-050 §4).
+ */
+@WireMockTest
+@DisplayName("X-Axonflow-Client header injection")
+class ClientHeaderTest {
+
+ private static final String EXPECTED_CLIENT = "sdk-java/" + AxonFlowConfig.SDK_VERSION;
+
+ @Test
+ @DisplayName("should send X-Axonflow-Client on proxyLLMCall")
+ void shouldSendClientHeaderOnProxy(WireMockRuntimeInfo wmRuntimeInfo) {
+ stubFor(
+ post(urlEqualTo("/api/request"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "{" + "\"success\": true," + "\"data\": {\"answer\": \"ok\"}" + "}")));
+
+ AxonFlow client =
+ AxonFlow.create(
+ AxonFlowConfig.builder()
+ .agentUrl(wmRuntimeInfo.getHttpBaseUrl())
+ .clientId("test-client")
+ .clientSecret("test-secret")
+ .build());
+
+ client.proxyLLMCall(ClientRequest.builder().userToken("").query("ping").build());
+
+ verify(
+ postRequestedFor(urlEqualTo("/api/request"))
+ .withHeader("X-Axonflow-Client", equalTo(EXPECTED_CLIENT)));
+ }
+
+ @Test
+ @DisplayName("getClientHeader returns sdk-java/