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
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,23 @@ 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.4.0] - 2026-01-14

### Added

- **MCP Exfiltration Detection** (Issue #966): `ConnectorPolicyInfo` now includes `getExfiltrationCheck()` with row/volume limit information
- `ExfiltrationCheckInfo` type with `getRowsReturned()`, `getRowLimit()`, `getBytesReturned()`, `getByteLimit()`, `isWithinLimits()` methods
- Prevents large-scale data extraction via MCP queries
- Configurable via `MCP_MAX_ROWS_PER_QUERY` and `MCP_MAX_BYTES_PER_QUERY` environment variables

- **MCP Dynamic Policies** (Issue #968): `ConnectorPolicyInfo` now includes `getDynamicPolicyInfo()` for Orchestrator-evaluated policies
- `DynamicPolicyInfo` type with `getPoliciesEvaluated()`, `getMatchedPolicies()`, `isOrchestratorReachable()`, `getProcessingTimeMs()`
- `DynamicPolicyMatch` type with `getPolicyId()`, `getPolicyName()`, `getPolicyType()`, `getAction()`, `getReason()`
- Supports rate limiting, budget controls, time-based access, and role-based access policies
- Optional feature - enable via `MCP_DYNAMIC_POLICIES_ENABLED=true`

---

## [2.3.0] - 2026-01-09

### Added
Expand Down
51 changes: 48 additions & 3 deletions src/main/java/com/getaxonflow/sdk/types/ConnectorPolicyInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,43 @@ public final class ConnectorPolicyInfo {
@JsonProperty("matched_policies")
private final List<PolicyMatchInfo> matchedPolicies;

@JsonProperty("exfiltration_check")
private final ExfiltrationCheckInfo exfiltrationCheck;

@JsonProperty("dynamic_policy_info")
private final DynamicPolicyInfo dynamicPolicyInfo;

public ConnectorPolicyInfo(
@JsonProperty("policies_evaluated") int policiesEvaluated,
@JsonProperty("blocked") boolean blocked,
@JsonProperty("block_reason") String blockReason,
@JsonProperty("redactions_applied") int redactionsApplied,
@JsonProperty("processing_time_ms") long processingTimeMs,
@JsonProperty("matched_policies") List<PolicyMatchInfo> matchedPolicies) {
@JsonProperty("matched_policies") List<PolicyMatchInfo> matchedPolicies,
@JsonProperty("exfiltration_check") ExfiltrationCheckInfo exfiltrationCheck,
@JsonProperty("dynamic_policy_info") DynamicPolicyInfo dynamicPolicyInfo) {
this.policiesEvaluated = policiesEvaluated;
this.blocked = blocked;
this.blockReason = blockReason;
this.redactionsApplied = redactionsApplied;
this.processingTimeMs = processingTimeMs;
this.matchedPolicies = matchedPolicies != null ? matchedPolicies : Collections.emptyList();
this.exfiltrationCheck = exfiltrationCheck;
this.dynamicPolicyInfo = dynamicPolicyInfo;
}

/**
* Backward-compatible constructor without exfiltration and dynamic policy fields.
*/
public ConnectorPolicyInfo(
int policiesEvaluated,
boolean blocked,
String blockReason,
int redactionsApplied,
long processingTimeMs,
List<PolicyMatchInfo> matchedPolicies) {
this(policiesEvaluated, blocked, blockReason, redactionsApplied,
processingTimeMs, matchedPolicies, null, null);
}

public int getPoliciesEvaluated() {
Expand All @@ -88,6 +112,22 @@ public List<PolicyMatchInfo> getMatchedPolicies() {
return matchedPolicies;
}

/**
* Returns exfiltration check information (Issue #966).
* May be null if exfiltration checking is disabled.
*/
public ExfiltrationCheckInfo getExfiltrationCheck() {
return exfiltrationCheck;
}

/**
* Returns dynamic policy evaluation information (Issue #968).
* May be null if dynamic policies are disabled.
*/
public DynamicPolicyInfo getDynamicPolicyInfo() {
return dynamicPolicyInfo;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
Expand All @@ -98,12 +138,15 @@ public boolean equals(Object o) {
redactionsApplied == that.redactionsApplied &&
processingTimeMs == that.processingTimeMs &&
Objects.equals(blockReason, that.blockReason) &&
Objects.equals(matchedPolicies, that.matchedPolicies);
Objects.equals(matchedPolicies, that.matchedPolicies) &&
Objects.equals(exfiltrationCheck, that.exfiltrationCheck) &&
Objects.equals(dynamicPolicyInfo, that.dynamicPolicyInfo);
}

@Override
public int hashCode() {
return Objects.hash(policiesEvaluated, blocked, blockReason, redactionsApplied, processingTimeMs, matchedPolicies);
return Objects.hash(policiesEvaluated, blocked, blockReason, redactionsApplied,
processingTimeMs, matchedPolicies, exfiltrationCheck, dynamicPolicyInfo);
}

@Override
Expand All @@ -115,6 +158,8 @@ public String toString() {
", redactionsApplied=" + redactionsApplied +
", processingTimeMs=" + processingTimeMs +
", matchedPolicies=" + matchedPolicies +
", exfiltrationCheck=" + exfiltrationCheck +
", dynamicPolicyInfo=" + dynamicPolicyInfo +
'}';
}
}
110 changes: 110 additions & 0 deletions src/main/java/com/getaxonflow/sdk/types/DynamicPolicyInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Copyright 2026 AxonFlow
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.getaxonflow.sdk.types;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;

import java.util.Collections;
import java.util.List;
import java.util.Objects;

/**
* Information about dynamic policy evaluation (Issue #968).
*
* <p>Dynamic policies are evaluated by the Orchestrator and can include
* rate limiting, budget controls, time-based access, and role-based access.</p>
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public final class DynamicPolicyInfo {

@JsonProperty("policies_evaluated")
private final int policiesEvaluated;

@JsonProperty("matched_policies")
private final List<DynamicPolicyMatch> matchedPolicies;

@JsonProperty("orchestrator_reachable")
private final boolean orchestratorReachable;

@JsonProperty("processing_time_ms")
private final long processingTimeMs;

public DynamicPolicyInfo(
@JsonProperty("policies_evaluated") int policiesEvaluated,
@JsonProperty("matched_policies") List<DynamicPolicyMatch> matchedPolicies,
@JsonProperty("orchestrator_reachable") boolean orchestratorReachable,
@JsonProperty("processing_time_ms") long processingTimeMs) {
this.policiesEvaluated = policiesEvaluated;
this.matchedPolicies = matchedPolicies != null ? matchedPolicies : Collections.emptyList();
this.orchestratorReachable = orchestratorReachable;
this.processingTimeMs = processingTimeMs;
}

/**
* Returns the number of dynamic policies checked.
*/
public int getPoliciesEvaluated() {
return policiesEvaluated;
}

/**
* Returns details about policies that matched.
*/
public List<DynamicPolicyMatch> getMatchedPolicies() {
return matchedPolicies;
}

/**
* Returns whether the Orchestrator was reachable.
*/
public boolean isOrchestratorReachable() {
return orchestratorReachable;
}

/**
* Returns the time taken for dynamic policy evaluation in milliseconds.
*/
public long getProcessingTimeMs() {
return processingTimeMs;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DynamicPolicyInfo that = (DynamicPolicyInfo) o;
return policiesEvaluated == that.policiesEvaluated &&
orchestratorReachable == that.orchestratorReachable &&
processingTimeMs == that.processingTimeMs &&
Objects.equals(matchedPolicies, that.matchedPolicies);
}

@Override
public int hashCode() {
return Objects.hash(policiesEvaluated, matchedPolicies, orchestratorReachable, processingTimeMs);
}

@Override
public String toString() {
return "DynamicPolicyInfo{" +
"policiesEvaluated=" + policiesEvaluated +
", matchedPolicies=" + matchedPolicies +
", orchestratorReachable=" + orchestratorReachable +
", processingTimeMs=" + processingTimeMs +
'}';
}
}
122 changes: 122 additions & 0 deletions src/main/java/com/getaxonflow/sdk/types/DynamicPolicyMatch.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*
* Copyright 2026 AxonFlow
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.getaxonflow.sdk.types;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;

import java.util.Objects;

/**
* Details about a matched dynamic policy.
*
* <p>Provides information about which dynamic policy matched during
* Orchestrator evaluation, including the policy type and action taken.</p>
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public final class DynamicPolicyMatch {

@JsonProperty("policy_id")
private final String policyId;

@JsonProperty("policy_name")
private final String policyName;

@JsonProperty("policy_type")
private final String policyType;

@JsonProperty("action")
private final String action;

@JsonProperty("reason")
private final String reason;

public DynamicPolicyMatch(
@JsonProperty("policy_id") String policyId,
@JsonProperty("policy_name") String policyName,
@JsonProperty("policy_type") String policyType,
@JsonProperty("action") String action,
@JsonProperty("reason") String reason) {
this.policyId = policyId;
this.policyName = policyName;
this.policyType = policyType;
this.action = action;
this.reason = reason;
}

/**
* Returns the unique identifier of the policy.
*/
public String getPolicyId() {
return policyId;
}

/**
* Returns the human-readable name of the policy.
*/
public String getPolicyName() {
return policyName;
}

/**
* Returns the type of policy (rate-limit, budget, time-access, role-access, mcp, connector).
*/
public String getPolicyType() {
return policyType;
}

/**
* Returns the action taken (allow, block, log, etc.).
*/
public String getAction() {
return action;
}

/**
* Returns the context for the policy match.
*/
public String getReason() {
return reason;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DynamicPolicyMatch that = (DynamicPolicyMatch) o;
return Objects.equals(policyId, that.policyId) &&
Objects.equals(policyName, that.policyName) &&
Objects.equals(policyType, that.policyType) &&
Objects.equals(action, that.action) &&
Objects.equals(reason, that.reason);
}

@Override
public int hashCode() {
return Objects.hash(policyId, policyName, policyType, action, reason);
}

@Override
public String toString() {
return "DynamicPolicyMatch{" +
"policyId='" + policyId + '\'' +
", policyName='" + policyName + '\'' +
", policyType='" + policyType + '\'' +
", action='" + action + '\'' +
", reason='" + reason + '\'' +
'}';
}
}
Loading
Loading