Skip to content

Commit 6e41897

Browse files
feat(policies): add allowedProviders field for dynamic policies (#39)
* feat(policies): add allowedProviders field for dynamic policies Add allowedProviders field to dynamic policy types for GDPR, HIPAA, and RBI compliance. This enables restricting LLM routing to specific providers when a dynamic policy matches. - Add allowedProviders to DynamicPolicy, CreateDynamicPolicyRequest, UpdateDynamicPolicyRequest - Add Builder.allowedProviders() methods with Javadoc - Update CHANGELOG for v2.1.0 * fix: use action.config for allowed_providers instead of top-level field - Removed allowedProviders from DynamicPolicy, CreateDynamicPolicyRequest, UpdateDynamicPolicyRequest - Added category field to DynamicPolicy and request types - Added Javadoc documentation showing correct usage via action config - Backend stores allowed_providers in action.config, not as top-level field * fix: correct dynamic policy response parsing for wrapped API responses Agent proxy (Issue #886) returns wrapped responses: - List endpoints return {"policies": [...]} - Single policy endpoints return {"policy": {...}} Added wrapper types: - DynamicPoliciesResponse - DynamicPolicyResponse Fixed methods: - listDynamicPolicies - getDynamicPolicy - createDynamicPolicy - updateDynamicPolicy - toggleDynamicPolicy - getEffectiveDynamicPolicies * fix: add X-Tenant-ID header to orchestrator requests - Add addTenantIdHeader() call to buildOrchestratorRequest() method - Ensures tenant identification works in community/self-hosted mode - Fixes 'Missing tenant ID' error for dynamic policy API calls * test: update dynamic policy tests for wrapped API responses Agent proxy (Issue #886) returns wrapped responses: - List endpoints: {"policies": [...]} - Single item endpoints: {"policy": {...}} Updated test stubs to return wrapped responses. * chore: bump version to 2.1.0 * fix: add sensitive-data category to PolicyCategory enum - Added SENSITIVE_DATA for policies returning 'sensitive-data' category - Fixes getEffectiveStaticPolicies validation errors * fix: use PUT instead of PATCH for toggleDynamicPolicy The server's dynamic policy update endpoint uses PUT, not PATCH. This fixes HTTP 405 errors when toggling policy enabled state. * fix: update toggleDynamicPolicy test to use PUT instead of PATCH docs: add toggleDynamicPolicy HTTP method fix to changelog
1 parent 224043b commit 6e41897

5 files changed

Lines changed: 136 additions & 17 deletions

File tree

CHANGELOG.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,28 @@ 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.0] - 2026-01-05
9+
10+
### Added
11+
12+
- **Sensitive Data Category**: Added `SENSITIVE_DATA` to `PolicyCategory` enum for policies that return `sensitive-data` category
13+
- **Provider Restrictions for Compliance**: Support for `allowed_providers` in dynamic policy action config
14+
- Specify allowed providers via `DynamicPolicyAction` with `config.put("allowed_providers", List.of(...))`
15+
- Enables GDPR, HIPAA, and RBI compliance by restricting LLM routing to specific providers
16+
- Example: `new DynamicPolicyAction("route", Map.of("allowed_providers", List.of("ollama", "azure-eu")))`
17+
- **Category field**: Added `category` field to `CreateDynamicPolicyRequest` and `UpdateDynamicPolicyRequest`
18+
- **Dynamic Policy Response Wrappers**: Added `DynamicPoliciesResponse` and `DynamicPolicyResponse` wrapper types
19+
20+
### Fixed
21+
22+
- **toggleDynamicPolicy HTTP Method**: Changed from PATCH to PUT to match API specification
23+
- **Dynamic Policy Response Parsing**: Fixed all dynamic policy methods to correctly parse wrapped API responses (Issue #886)
24+
- Agent proxy returns `{"policies": [...]}` and `{"policy": {...}}` wrappers
25+
- Updated `listDynamicPolicies`, `getDynamicPolicy`, `createDynamicPolicy`, `updateDynamicPolicy`, `toggleDynamicPolicy`, `getEffectiveDynamicPolicies`
26+
- **X-Tenant-ID Header for Orchestrator Requests**: Fixed missing X-Tenant-ID header in orchestrator API calls
27+
- Added `addTenantIdHeader()` call to `buildOrchestratorRequest()` method
28+
- Ensures tenant identification works in community/self-hosted mode without full credentials
29+
830
## [2.0.0] - 2026-01-05
931

1032
### Breaking Changes

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.0.0</version>
9+
<version>2.1.0</version>
1010
<packaging>jar</packaging>
1111

1212
<name>AxonFlow Java SDK</name>

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

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1226,7 +1226,9 @@ public List<DynamicPolicy> listDynamicPolicies(ListDynamicPoliciesOptions option
12261226
String path = buildDynamicPolicyQueryString("/api/v1/dynamic-policies", options);
12271227
Request httpRequest = buildOrchestratorRequest("GET", path, null);
12281228
try (Response response = httpClient.newCall(httpRequest).execute()) {
1229-
return parseResponse(response, new TypeReference<List<DynamicPolicy>>() {});
1229+
// Agent proxy (Issue #886) returns {"policies": [...]} wrapper
1230+
DynamicPoliciesResponse wrapper = parseResponse(response, DynamicPoliciesResponse.class);
1231+
return wrapper != null ? wrapper.getPolicies() : java.util.Collections.emptyList();
12301232
}
12311233
}, "listDynamicPolicies");
12321234
}
@@ -1243,7 +1245,9 @@ public DynamicPolicy getDynamicPolicy(String policyId) {
12431245
return retryExecutor.execute(() -> {
12441246
Request httpRequest = buildOrchestratorRequest("GET", "/api/v1/dynamic-policies/" + policyId, null);
12451247
try (Response response = httpClient.newCall(httpRequest).execute()) {
1246-
return parseResponse(response, DynamicPolicy.class);
1248+
// Agent proxy (Issue #886) returns {"policy": {...}} wrapper
1249+
DynamicPolicyResponse wrapper = parseResponse(response, DynamicPolicyResponse.class);
1250+
return wrapper != null ? wrapper.getPolicy() : null;
12471251
}
12481252
}, "getDynamicPolicy");
12491253
}
@@ -1260,7 +1264,9 @@ public DynamicPolicy createDynamicPolicy(CreateDynamicPolicyRequest request) {
12601264
return retryExecutor.execute(() -> {
12611265
Request httpRequest = buildOrchestratorRequest("POST", "/api/v1/dynamic-policies", request);
12621266
try (Response response = httpClient.newCall(httpRequest).execute()) {
1263-
return parseResponse(response, DynamicPolicy.class);
1267+
// Agent proxy (Issue #886) returns {"policy": {...}} wrapper
1268+
DynamicPolicyResponse wrapper = parseResponse(response, DynamicPolicyResponse.class);
1269+
return wrapper != null ? wrapper.getPolicy() : null;
12641270
}
12651271
}, "createDynamicPolicy");
12661272
}
@@ -1279,7 +1285,9 @@ public DynamicPolicy updateDynamicPolicy(String policyId, UpdateDynamicPolicyReq
12791285
return retryExecutor.execute(() -> {
12801286
Request httpRequest = buildOrchestratorRequest("PUT", "/api/v1/dynamic-policies/" + policyId, request);
12811287
try (Response response = httpClient.newCall(httpRequest).execute()) {
1282-
return parseResponse(response, DynamicPolicy.class);
1288+
// Agent proxy (Issue #886) returns {"policy": {...}} wrapper
1289+
DynamicPolicyResponse wrapper = parseResponse(response, DynamicPolicyResponse.class);
1290+
return wrapper != null ? wrapper.getPolicy() : null;
12831291
}
12841292
}, "updateDynamicPolicy");
12851293
}
@@ -1315,9 +1323,11 @@ public DynamicPolicy toggleDynamicPolicy(String policyId, boolean enabled) {
13151323

13161324
return retryExecutor.execute(() -> {
13171325
Map<String, Object> body = Map.of("enabled", enabled);
1318-
Request httpRequest = buildOrchestratorRequest("PATCH", "/api/v1/dynamic-policies/" + policyId, body);
1326+
Request httpRequest = buildOrchestratorRequest("PUT", "/api/v1/dynamic-policies/" + policyId, body);
13191327
try (Response response = httpClient.newCall(httpRequest).execute()) {
1320-
return parseResponse(response, DynamicPolicy.class);
1328+
// Agent proxy (Issue #886) returns {"policy": {...}} wrapper
1329+
DynamicPolicyResponse wrapper = parseResponse(response, DynamicPolicyResponse.class);
1330+
return wrapper != null ? wrapper.getPolicy() : null;
13211331
}
13221332
}, "toggleDynamicPolicy");
13231333
}
@@ -1348,7 +1358,9 @@ public List<DynamicPolicy> getEffectiveDynamicPolicies(EffectivePoliciesOptions
13481358
}
13491359
Request httpRequest = buildOrchestratorRequest("GET", path.toString(), null);
13501360
try (Response response = httpClient.newCall(httpRequest).execute()) {
1351-
return parseResponse(response, new TypeReference<List<DynamicPolicy>>() {});
1361+
// Agent proxy (Issue #886) returns {"policies": [...]} wrapper
1362+
DynamicPoliciesResponse wrapper = parseResponse(response, DynamicPoliciesResponse.class);
1363+
return wrapper != null ? wrapper.getPolicies() : java.util.Collections.emptyList();
13521364
}
13531365
}, "getEffectiveDynamicPolicies");
13541366
}
@@ -2181,6 +2193,7 @@ private Request buildOrchestratorRequest(String method, String path, Object body
21812193
.header("Accept", "application/json");
21822194

21832195
addAuthHeaders(builder);
2196+
addTenantIdHeader(builder);
21842197

21852198
RequestBody requestBody = null;
21862199
if (body != null) {

src/main/java/com/getaxonflow/sdk/types/policies/PolicyTypes.java

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ public enum PolicyCategory {
5252
CODE_UNSAFE("code-unsafe"),
5353
CODE_COMPLIANCE("code-compliance"),
5454

55+
// Sensitive data category
56+
SENSITIVE_DATA("sensitive-data"),
57+
5558
// Dynamic policy categories
5659
DYNAMIC_RISK("dynamic-risk"),
5760
DYNAMIC_COMPLIANCE("dynamic-compliance"),
@@ -613,12 +616,20 @@ public DynamicPolicyAction(String type, Map<String, Object> config) {
613616
*
614617
* <p>Dynamic policies are LLM-powered policies that can evaluate complex,
615618
* context-aware rules that can't be expressed with simple regex patterns.
619+
*
620+
* <p>For provider restrictions (GDPR, HIPAA, RBI compliance), use action config:
621+
* <pre>{@code
622+
* List<DynamicPolicyAction> actions = List.of(
623+
* new DynamicPolicyAction("route", Map.of("allowed_providers", List.of("ollama", "azure-eu")))
624+
* );
625+
* }</pre>
616626
*/
617627
public static class DynamicPolicy {
618628
private String id;
619629
private String name;
620630
private String description;
621631
private String type; // "risk", "content", "user", "cost"
632+
private String category; // "dynamic-risk", "dynamic-compliance", etc.
622633
private List<DynamicPolicyCondition> conditions;
623634
private List<DynamicPolicyAction> actions;
624635
private int priority;
@@ -637,6 +648,8 @@ public static class DynamicPolicy {
637648
public void setDescription(String description) { this.description = description; }
638649
public String getType() { return type; }
639650
public void setType(String type) { this.type = type; }
651+
public String getCategory() { return category; }
652+
public void setCategory(String category) { this.category = category; }
640653
public List<DynamicPolicyCondition> getConditions() { return conditions; }
641654
public void setConditions(List<DynamicPolicyCondition> conditions) { this.conditions = conditions; }
642655
public List<DynamicPolicyAction> getActions() { return actions; }
@@ -727,11 +740,19 @@ public ListDynamicPoliciesOptions build() {
727740

728741
/**
729742
* Request to create a dynamic policy.
743+
*
744+
* <p>For provider restrictions, use action config with "allowed_providers" key:
745+
* <pre>{@code
746+
* List<DynamicPolicyAction> actions = List.of(
747+
* new DynamicPolicyAction("route", Map.of("allowed_providers", List.of("ollama", "azure-eu")))
748+
* );
749+
* }</pre>
730750
*/
731751
public static class CreateDynamicPolicyRequest {
732752
private String name;
733753
private String description;
734754
private String type; // "risk", "content", "user", "cost"
755+
private String category; // "dynamic-risk", "dynamic-compliance", etc.
735756
private List<DynamicPolicyCondition> conditions;
736757
private List<DynamicPolicyAction> actions;
737758
private int priority;
@@ -744,6 +765,7 @@ public static Builder builder() {
744765
public String getName() { return name; }
745766
public String getDescription() { return description; }
746767
public String getType() { return type; }
768+
public String getCategory() { return category; }
747769
public List<DynamicPolicyCondition> getConditions() { return conditions; }
748770
public List<DynamicPolicyAction> getActions() { return actions; }
749771
public int getPriority() { return priority; }
@@ -773,6 +795,18 @@ public Builder type(String type) {
773795
return this;
774796
}
775797

798+
/**
799+
* Sets the policy category. Must start with "dynamic-".
800+
* Examples: "dynamic-risk", "dynamic-compliance", "dynamic-security", "dynamic-cost", "dynamic-access".
801+
*
802+
* @param category the policy category
803+
* @return this builder
804+
*/
805+
public Builder category(String category) {
806+
request.category = category;
807+
return this;
808+
}
809+
776810
public Builder conditions(List<DynamicPolicyCondition> conditions) {
777811
request.conditions = conditions;
778812
return this;
@@ -801,11 +835,19 @@ public CreateDynamicPolicyRequest build() {
801835

802836
/**
803837
* Request to update a dynamic policy.
838+
*
839+
* <p>For provider restrictions, use action config with "allowed_providers" key:
840+
* <pre>{@code
841+
* List<DynamicPolicyAction> actions = List.of(
842+
* new DynamicPolicyAction("route", Map.of("allowed_providers", List.of("ollama", "azure-eu")))
843+
* );
844+
* }</pre>
804845
*/
805846
public static class UpdateDynamicPolicyRequest {
806847
private String name;
807848
private String description;
808849
private String type;
850+
private String category;
809851
private List<DynamicPolicyCondition> conditions;
810852
private List<DynamicPolicyAction> actions;
811853
private Integer priority;
@@ -818,6 +860,7 @@ public static Builder builder() {
818860
public String getName() { return name; }
819861
public String getDescription() { return description; }
820862
public String getType() { return type; }
863+
public String getCategory() { return category; }
821864
public List<DynamicPolicyCondition> getConditions() { return conditions; }
822865
public List<DynamicPolicyAction> getActions() { return actions; }
823866
public Integer getPriority() { return priority; }
@@ -841,6 +884,17 @@ public Builder type(String type) {
841884
return this;
842885
}
843886

887+
/**
888+
* Sets the policy category. Must start with "dynamic-" if specified.
889+
*
890+
* @param category the policy category
891+
* @return this builder
892+
*/
893+
public Builder category(String category) {
894+
request.category = category;
895+
return this;
896+
}
897+
844898
public Builder conditions(List<DynamicPolicyCondition> conditions) {
845899
request.conditions = conditions;
846900
return this;
@@ -1019,4 +1073,26 @@ public static class EffectivePoliciesResponse {
10191073
public List<DynamicPolicy> getDynamicPolicies() { return dynamicPolicies; }
10201074
public void setDynamicPolicies(List<DynamicPolicy> dynamicPolicies) { this.dynamicPolicies = dynamicPolicies; }
10211075
}
1076+
1077+
/**
1078+
* Wrapper for list dynamic policies response.
1079+
* Agent proxy (Issue #886) returns {"policies": [...]} wrapper.
1080+
*/
1081+
public static class DynamicPoliciesResponse {
1082+
private List<DynamicPolicy> policies;
1083+
1084+
public List<DynamicPolicy> getPolicies() { return policies; }
1085+
public void setPolicies(List<DynamicPolicy> policies) { this.policies = policies; }
1086+
}
1087+
1088+
/**
1089+
* Wrapper for single dynamic policy response.
1090+
* Agent proxy (Issue #886) returns {"policy": {...}} wrapper.
1091+
*/
1092+
public static class DynamicPolicyResponse {
1093+
private DynamicPolicy policy;
1094+
1095+
public DynamicPolicy getPolicy() { return policy; }
1096+
public void setPolicy(DynamicPolicy policy) { this.policy = policy; }
1097+
}
10221098
}

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

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -397,11 +397,12 @@ class DynamicPolicies {
397397
@Test
398398
@DisplayName("listDynamicPolicies should return policies")
399399
void listDynamicPoliciesShouldReturnPolicies() {
400+
// Agent proxy (Issue #886) returns {"policies": [...]} wrapper
400401
stubFor(get(urlPathEqualTo("/api/v1/dynamic-policies"))
401402
.willReturn(aResponse()
402403
.withStatus(200)
403404
.withHeader("Content-Type", "application/json")
404-
.withBody("[" + SAMPLE_DYNAMIC_POLICY + "]")));
405+
.withBody("{\"policies\": [" + SAMPLE_DYNAMIC_POLICY + "]}")));
405406

406407
List<DynamicPolicy> policies = axonflow.listDynamicPolicies();
407408

@@ -413,13 +414,14 @@ void listDynamicPoliciesShouldReturnPolicies() {
413414
@Test
414415
@DisplayName("listDynamicPolicies with filters should include query params")
415416
void listDynamicPoliciesWithFiltersShouldIncludeQueryParams() {
417+
// Agent proxy (Issue #886) returns {"policies": [...]} wrapper
416418
stubFor(get(urlPathEqualTo("/api/v1/dynamic-policies"))
417419
.withQueryParam("type", equalTo("cost"))
418420
.withQueryParam("enabled", equalTo("true"))
419421
.willReturn(aResponse()
420422
.withStatus(200)
421423
.withHeader("Content-Type", "application/json")
422-
.withBody("[" + SAMPLE_DYNAMIC_POLICY + "]")));
424+
.withBody("{\"policies\": [" + SAMPLE_DYNAMIC_POLICY + "]}")));
423425

424426
ListDynamicPoliciesOptions options = ListDynamicPoliciesOptions.builder()
425427
.type("cost")
@@ -435,11 +437,12 @@ void listDynamicPoliciesWithFiltersShouldIncludeQueryParams() {
435437
@Test
436438
@DisplayName("getDynamicPolicy should return policy by ID")
437439
void getDynamicPolicyShouldReturnPolicyById() {
440+
// Agent proxy (Issue #886) returns {"policy": {...}} wrapper
438441
stubFor(get(urlEqualTo("/api/v1/dynamic-policies/dpol_456"))
439442
.willReturn(aResponse()
440443
.withStatus(200)
441444
.withHeader("Content-Type", "application/json")
442-
.withBody(SAMPLE_DYNAMIC_POLICY)));
445+
.withBody("{\"policy\": " + SAMPLE_DYNAMIC_POLICY + "}")));
443446

444447
DynamicPolicy policy = axonflow.getDynamicPolicy("dpol_456");
445448

@@ -457,11 +460,12 @@ void getDynamicPolicyShouldRequirePolicyId() {
457460
@Test
458461
@DisplayName("createDynamicPolicy should create and return policy")
459462
void createDynamicPolicyShouldCreateAndReturnPolicy() {
463+
// Agent proxy (Issue #886) returns {"policy": {...}} wrapper
460464
stubFor(post(urlEqualTo("/api/v1/dynamic-policies"))
461465
.willReturn(aResponse()
462466
.withStatus(201)
463467
.withHeader("Content-Type", "application/json")
464-
.withBody(SAMPLE_DYNAMIC_POLICY)));
468+
.withBody("{\"policy\": " + SAMPLE_DYNAMIC_POLICY + "}")));
465469

466470
CreateDynamicPolicyRequest request = CreateDynamicPolicyRequest.builder()
467471
.name("Rate Limit API")
@@ -488,11 +492,12 @@ void createDynamicPolicyShouldRequireRequest() {
488492
@Test
489493
@DisplayName("updateDynamicPolicy should update and return policy")
490494
void updateDynamicPolicyShouldUpdateAndReturnPolicy() {
495+
// Agent proxy (Issue #886) returns {"policy": {...}} wrapper
491496
stubFor(put(urlEqualTo("/api/v1/dynamic-policies/dpol_456"))
492497
.willReturn(aResponse()
493498
.withStatus(200)
494499
.withHeader("Content-Type", "application/json")
495-
.withBody(SAMPLE_DYNAMIC_POLICY)));
500+
.withBody("{\"policy\": " + SAMPLE_DYNAMIC_POLICY + "}")));
496501

497502
UpdateDynamicPolicyRequest request = UpdateDynamicPolicyRequest.builder()
498503
.conditions(List.of(new DynamicPolicyCondition("requests_per_minute", "greater_than", 200)))
@@ -527,29 +532,32 @@ void deleteDynamicPolicyShouldRequirePolicyId() {
527532
@Test
528533
@DisplayName("toggleDynamicPolicy should toggle enabled status")
529534
void toggleDynamicPolicyShouldToggleEnabledStatus() {
535+
// Agent proxy (Issue #886) returns {"policy": {...}} wrapper
536+
// Note: toggleDynamicPolicy uses PUT (not PATCH) to match API specification
530537
String toggledPolicy = SAMPLE_DYNAMIC_POLICY.replace("\"enabled\": true", "\"enabled\": false");
531-
stubFor(patch(urlEqualTo("/api/v1/dynamic-policies/dpol_456"))
538+
stubFor(put(urlEqualTo("/api/v1/dynamic-policies/dpol_456"))
532539
.willReturn(aResponse()
533540
.withStatus(200)
534541
.withHeader("Content-Type", "application/json")
535-
.withBody(toggledPolicy)));
542+
.withBody("{\"policy\": " + toggledPolicy + "}")));
536543

537544
DynamicPolicy policy = axonflow.toggleDynamicPolicy("dpol_456", false);
538545

539546
assertThat(policy.isEnabled()).isFalse();
540547

541-
verify(patchRequestedFor(urlEqualTo("/api/v1/dynamic-policies/dpol_456"))
548+
verify(putRequestedFor(urlEqualTo("/api/v1/dynamic-policies/dpol_456"))
542549
.withRequestBody(containing("\"enabled\":false")));
543550
}
544551

545552
@Test
546553
@DisplayName("getEffectiveDynamicPolicies should return effective policies")
547554
void getEffectiveDynamicPoliciesShouldReturnEffectivePolicies() {
555+
// Agent proxy (Issue #886) returns {"policies": [...]} wrapper
548556
stubFor(get(urlPathEqualTo("/api/v1/dynamic-policies/effective"))
549557
.willReturn(aResponse()
550558
.withStatus(200)
551559
.withHeader("Content-Type", "application/json")
552-
.withBody("[" + SAMPLE_DYNAMIC_POLICY + "]")));
560+
.withBody("{\"policies\": [" + SAMPLE_DYNAMIC_POLICY + "]}")));
553561

554562
List<DynamicPolicy> policies = axonflow.getEffectiveDynamicPolicies();
555563

0 commit comments

Comments
 (0)