diff --git a/CHANGELOG.md b/CHANGELOG.md index 230a924..d66090e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,23 @@ All notable changes to the AxonFlow Python 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). +## [1.4.0] - 2026-01-14 + +### Added + +- **MCP Exfiltration Detection** (Issue #966): `ConnectorPolicyInfo` now includes `exfiltration_check` with row/volume limit information + - `ExfiltrationCheckInfo` type with `rows_returned`, `row_limit`, `bytes_returned`, `byte_limit`, `within_limits` fields + - 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 `dynamic_policy_info` for Orchestrator-evaluated policies + - `DynamicPolicyInfo` type with `policies_evaluated`, `matched_policies`, `orchestrator_reachable`, `processing_time_ms` + - `DynamicPolicyMatch` type with `policy_id`, `policy_name`, `policy_type`, `action`, `reason` + - Supports rate limiting, budget controls, time-based access, and role-based access policies + - Optional feature - enable via `MCP_DYNAMIC_POLICIES_ENABLED=true` + +--- + ## [1.3.0] - 2026-01-09 ### Added diff --git a/axonflow/types.py b/axonflow/types.py index 7b92081..08749ab 100644 --- a/axonflow/types.py +++ b/axonflow/types.py @@ -176,6 +176,53 @@ class PolicyMatchInfo(BaseModel): action: str = Field(..., description="Action taken") +class ExfiltrationCheckInfo(BaseModel): + """Information about exfiltration limit checks (Issue #966). + + Helps prevent large-scale data extraction via MCP queries. + """ + + rows_returned: int = Field(default=0, ge=0, description="Number of rows in the response") + row_limit: int = Field(default=0, ge=0, description="Configured max rows per query") + bytes_returned: int = Field(default=0, ge=0, description="Size of response data in bytes") + byte_limit: int = Field(default=0, ge=0, description="Configured max bytes per response") + within_limits: bool = Field(default=True, description="Whether response is within limits") + + +class DynamicPolicyMatch(BaseModel): + """Details about a matched dynamic policy.""" + + policy_id: str = Field(..., description="Unique policy identifier") + policy_name: str = Field(default="", description="Human-readable policy name") + policy_type: str = Field( + default="", + description="Type of policy (rate-limit, budget, time-access, role-access, mcp, connector)", + ) + action: str = Field(default="", description="Action taken (allow, block, log, etc.)") + reason: str | None = Field(default=None, description="Context for the policy match") + + +class DynamicPolicyInfo(BaseModel): + """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 policies. + """ + + policies_evaluated: int = Field( + default=0, ge=0, description="Number of dynamic policies checked" + ) + matched_policies: list[DynamicPolicyMatch] = Field( + default_factory=list, description="Policies that matched" + ) + orchestrator_reachable: bool = Field( + default=True, description="Whether the Orchestrator was reachable" + ) + processing_time_ms: int = Field( + default=0, ge=0, description="Time taken for dynamic policy evaluation" + ) + + class ConnectorPolicyInfo(BaseModel): """Policy evaluation information included in MCP responses. @@ -191,6 +238,12 @@ class ConnectorPolicyInfo(BaseModel): matched_policies: list[PolicyMatchInfo] = Field( default_factory=list, description="Policies that matched" ) + exfiltration_check: ExfiltrationCheckInfo | None = Field( + default=None, description="Exfiltration check info (Issue #966)" + ) + dynamic_policy_info: DynamicPolicyInfo | None = Field( + default=None, description="Dynamic policy evaluation info (Issue #968)" + ) class ConnectorResponse(BaseModel):