From 33c0553cbc344004d75d0558d3838f6bca1b2be5 Mon Sep 17 00:00:00 2001 From: Saurabh Jain Date: Wed, 14 Jan 2026 14:39:46 +0100 Subject: [PATCH] feat: add exfiltration and dynamic policy fields to ConnectorPolicyInfo Add new response fields for MCP exfiltration detection and dynamic policies: - ExfiltrationCheckInfo: rows_returned, row_limit, bytes_returned, byte_limit, within_limits - DynamicPolicyInfo: policies_evaluated, matched_policies, orchestrator_reachable, processing_time_ms - DynamicPolicyMatch: policy_id, policy_name, policy_type, action, reason Update ConnectorPolicyInfo to include optional getExfiltrationCheck() and getDynamicPolicyInfo() fields. CHANGELOG.md updated for v2.4.0. --- CHANGELOG.md | 17 +++ .../sdk/types/ConnectorPolicyInfo.java | 51 +++++++- .../sdk/types/DynamicPolicyInfo.java | 110 ++++++++++++++++ .../sdk/types/DynamicPolicyMatch.java | 122 ++++++++++++++++++ .../sdk/types/ExfiltrationCheckInfo.java | 122 ++++++++++++++++++ 5 files changed, 419 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/getaxonflow/sdk/types/DynamicPolicyInfo.java create mode 100644 src/main/java/com/getaxonflow/sdk/types/DynamicPolicyMatch.java create mode 100644 src/main/java/com/getaxonflow/sdk/types/ExfiltrationCheckInfo.java diff --git a/CHANGELOG.md b/CHANGELOG.md index b12b6b7..e4ffbe6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/src/main/java/com/getaxonflow/sdk/types/ConnectorPolicyInfo.java b/src/main/java/com/getaxonflow/sdk/types/ConnectorPolicyInfo.java index 3c7e64f..37cd9b3 100644 --- a/src/main/java/com/getaxonflow/sdk/types/ConnectorPolicyInfo.java +++ b/src/main/java/com/getaxonflow/sdk/types/ConnectorPolicyInfo.java @@ -49,19 +49,43 @@ public final class ConnectorPolicyInfo { @JsonProperty("matched_policies") private final List 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 matchedPolicies) { + @JsonProperty("matched_policies") List 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 matchedPolicies) { + this(policiesEvaluated, blocked, blockReason, redactionsApplied, + processingTimeMs, matchedPolicies, null, null); } public int getPoliciesEvaluated() { @@ -88,6 +112,22 @@ public List 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; @@ -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 @@ -115,6 +158,8 @@ public String toString() { ", redactionsApplied=" + redactionsApplied + ", processingTimeMs=" + processingTimeMs + ", matchedPolicies=" + matchedPolicies + + ", exfiltrationCheck=" + exfiltrationCheck + + ", dynamicPolicyInfo=" + dynamicPolicyInfo + '}'; } } diff --git a/src/main/java/com/getaxonflow/sdk/types/DynamicPolicyInfo.java b/src/main/java/com/getaxonflow/sdk/types/DynamicPolicyInfo.java new file mode 100644 index 0000000..200091b --- /dev/null +++ b/src/main/java/com/getaxonflow/sdk/types/DynamicPolicyInfo.java @@ -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). + * + *

Dynamic policies are evaluated by the Orchestrator and can include + * rate limiting, budget controls, time-based access, and role-based access.

+ */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class DynamicPolicyInfo { + + @JsonProperty("policies_evaluated") + private final int policiesEvaluated; + + @JsonProperty("matched_policies") + private final List 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 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 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 + + '}'; + } +} diff --git a/src/main/java/com/getaxonflow/sdk/types/DynamicPolicyMatch.java b/src/main/java/com/getaxonflow/sdk/types/DynamicPolicyMatch.java new file mode 100644 index 0000000..565dccf --- /dev/null +++ b/src/main/java/com/getaxonflow/sdk/types/DynamicPolicyMatch.java @@ -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. + * + *

Provides information about which dynamic policy matched during + * Orchestrator evaluation, including the policy type and action taken.

+ */ +@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 + '\'' + + '}'; + } +} diff --git a/src/main/java/com/getaxonflow/sdk/types/ExfiltrationCheckInfo.java b/src/main/java/com/getaxonflow/sdk/types/ExfiltrationCheckInfo.java new file mode 100644 index 0000000..54d3f2b --- /dev/null +++ b/src/main/java/com/getaxonflow/sdk/types/ExfiltrationCheckInfo.java @@ -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; + +/** + * Information about exfiltration limit checks (Issue #966). + * + *

Helps prevent large-scale data extraction via MCP queries by enforcing + * row count and data volume limits on responses.

+ */ +@JsonIgnoreProperties(ignoreUnknown = true) +public final class ExfiltrationCheckInfo { + + @JsonProperty("rows_returned") + private final long rowsReturned; + + @JsonProperty("row_limit") + private final int rowLimit; + + @JsonProperty("bytes_returned") + private final long bytesReturned; + + @JsonProperty("byte_limit") + private final long byteLimit; + + @JsonProperty("within_limits") + private final boolean withinLimits; + + public ExfiltrationCheckInfo( + @JsonProperty("rows_returned") long rowsReturned, + @JsonProperty("row_limit") int rowLimit, + @JsonProperty("bytes_returned") long bytesReturned, + @JsonProperty("byte_limit") long byteLimit, + @JsonProperty("within_limits") boolean withinLimits) { + this.rowsReturned = rowsReturned; + this.rowLimit = rowLimit; + this.bytesReturned = bytesReturned; + this.byteLimit = byteLimit; + this.withinLimits = withinLimits; + } + + /** + * Returns the number of rows in the response. + */ + public long getRowsReturned() { + return rowsReturned; + } + + /** + * Returns the configured maximum rows per query. + */ + public int getRowLimit() { + return rowLimit; + } + + /** + * Returns the size of the response data in bytes. + */ + public long getBytesReturned() { + return bytesReturned; + } + + /** + * Returns the configured maximum bytes per response. + */ + public long getByteLimit() { + return byteLimit; + } + + /** + * Returns whether the response is within configured limits. + */ + public boolean isWithinLimits() { + return withinLimits; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ExfiltrationCheckInfo that = (ExfiltrationCheckInfo) o; + return rowsReturned == that.rowsReturned && + rowLimit == that.rowLimit && + bytesReturned == that.bytesReturned && + byteLimit == that.byteLimit && + withinLimits == that.withinLimits; + } + + @Override + public int hashCode() { + return Objects.hash(rowsReturned, rowLimit, bytesReturned, byteLimit, withinLimits); + } + + @Override + public String toString() { + return "ExfiltrationCheckInfo{" + + "rowsReturned=" + rowsReturned + + ", rowLimit=" + rowLimit + + ", bytesReturned=" + bytesReturned + + ", byteLimit=" + byteLimit + + ", withinLimits=" + withinLimits + + '}'; + } +}