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
24 changes: 12 additions & 12 deletions .lint_baselines/falsey_clobber.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"_comment": "Pre-existing falsey-clobber findings. Generated by scripts/lint_no_falsey_clobber.py --write-baseline. CI fails on any finding NOT listed here. Burn this list down via targeted PRs that fix each pattern (or add # noqa: falsey-clobber if the `or` is intentional).",
"findings": [
"axonflow/adapters/computer_use.py:184:29",
"axonflow/adapters/computer_use.py:217:29",
"axonflow/adapters/computer_use.py:183:29",
"axonflow/adapters/computer_use.py:222:29",
"axonflow/adapters/langchain.py:117:30",
"axonflow/adapters/langchain.py:117:58",
"axonflow/adapters/langchain.py:118:34",
Expand All @@ -24,20 +24,20 @@
"axonflow/adapters/tool_wrapper.py:220:20",
"axonflow/client.py:1111:16",
"axonflow/client.py:1188:16",
"axonflow/client.py:1669:37",
"axonflow/client.py:1710:18",
"axonflow/client.py:1768:37",
"axonflow/client.py:2292:24",
"axonflow/client.py:2313:33",
"axonflow/client.py:2314:31",
"axonflow/client.py:2326:25",
"axonflow/client.py:2387:28",
"axonflow/client.py:2428:69",
"axonflow/client.py:1686:37",
"axonflow/client.py:1727:18",
"axonflow/client.py:1785:37",
"axonflow/client.py:2309:24",
"axonflow/client.py:2330:33",
"axonflow/client.py:2331:31",
"axonflow/client.py:2343:25",
"axonflow/client.py:2404:28",
"axonflow/client.py:2445:69",
"axonflow/client.py:300:14",
"axonflow/client.py:305:24",
"axonflow/client.py:306:20",
"axonflow/client.py:529:44",
"axonflow/client.py:6469:25",
"axonflow/client.py:6486:25",
"axonflow/client.py:845:20",
"axonflow/client.py:931:20",
"axonflow/execution.py:205:19",
Expand Down
65 changes: 55 additions & 10 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,63 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [9.0.0] - 2026-07-18

### Changed (BREAKING)

- **The LangGraph and Computer Use adapters now report the (server, tool)
identity as two separate wire fields instead of concatenating them into
`connector_type`.** `mcp_check_input`/`mcp_check_output` (and their
`check_tool_input`/`check_tool_output` aliases) gain an optional `tool`
parameter, sent alongside `connector_type` on the wire — the platform's
two-field (server, tool) identity contract. The tool name is never folded
back into `connector_type`.

- **LangGraph** (`mcp_tool_interceptor`) now sends
`connector_type = request.server_name` and `tool = request.name` instead
of `f"{server_name}.{name}"`; the default `connector_type_fn` returns the
bare `server_name`. `connector_type_fn` is the compatibility lever — a
caller can restore any prior `connector_type` value (including the old
concatenated form, `lambda req: f"{req.server_name}.{req.name}"`) without
losing the separate `tool` field. The human-readable `statement` is now
`"{connector_type}.{tool}(args)"` built from the resolved connector type;
with a custom `connector_type_fn` its shape shifts from `"{custom}(args)"`
to `"{custom}.{tool}(args)"`. With the default resolver, a tool whose
`server_name` is empty sends `connector_type=""`, which the platform
rejects with HTTP 400 — the call raises `ConnectorError` and is blocked
(fail-closed); supply a `connector_type_fn` for server-less MCP tools.

- **Computer Use** (`ComputerUseGovernor`) now sends the constant
`connector_type = "computer_use"` and `tool =` the tool name (`computer`,
`bash`, `text_editor`). The action (e.g. `left_click`) is preserved inside
the serialized `statement`, not in `tool` (and not in `operation`, which
is constrained to `{query, execute}`). This adapter has **no**
`connector_type_fn` escape hatch, so re-scoping policies is the only
migration path.

**Migration.** Policies or per-connector settings matching the old
concatenated value — e.g. `connector_type == "filesystem.read_file"` or
`"computer_use.left_click"` — stop matching after upgrade. Re-scope them to
match `connector_type` (the bare server name, or `"computer_use"`) together
with the `tool` field (e.g. `tool == "read_file"`).

**Minimum platform.** The `tool` field is consumed on `POST
/api/v1/mcp/check-input` by **AxonFlow platform v9.10.0+**. On older
platforms it is silently dropped and identity degrades to the bare
`connector_type` — upgrade the platform to v9.10.0+ before adopting this SDK
major. Response-plane (`check-output`) `tool` scoping requires **AxonFlow
platform v9.11.0+**; until then the SDK sends it forward-compatibly and older
platforms ignore it.

### Added

- **`AuditToolCallRequest.caller_name`** (getaxonflow/axonflow-enterprise#2912,
sub-issue of epic #2905) — identifies which client made a non-LLM tool
call (e.g. `claude_code`, `codex`, `cursor`, `openclaw`). Replaces the
misleadingly-named `tool_type` field, which every real caller actually
used to identify the calling client rather than any property of the
tool. `tool_type` is kept as a deprecated input fallback (not removed):
the server resolves `caller_name` if supplied, else the legacy
`tool_type`, else a default. See
`runtime-e2e/caller_name_audit/` for the real-stack proof that
`caller_name` reaches `policy_details.caller_name` on the audit row.
- **`AuditToolCallRequest.caller_name`** — identifies which client made a
non-LLM tool call (e.g. `claude_code`, `codex`, `cursor`, `openclaw`).
Replaces the misleadingly-named `tool_type` field, which every real caller
actually used to identify the calling client rather than any property of the
tool. `tool_type` is kept as a deprecated input fallback (not removed): the
server resolves `caller_name` if supplied, else the legacy `tool_type`, else
a default.

## [8.5.1] - 2026-07-09 — Interceptor sync bridge + async-client detection + example fixes

Expand Down
2 changes: 1 addition & 1 deletion axonflow/_version.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Single source of truth for the AxonFlow SDK version."""

__version__ = "8.5.1"
__version__ = "9.0.0"
55 changes: 30 additions & 25 deletions axonflow/adapters/computer_use.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,19 +83,6 @@ class CheckResult:
"""Number of policies the server evaluated."""


def _derive_connector_type(tool_name: str, action: str | None = None) -> str:
"""Derive AxonFlow connector_type from Computer Use tool name and action.

Examples:
_derive_connector_type("computer", "left_click") -> "computer_use.left_click"
_derive_connector_type("bash") -> "computer_use.bash"
_derive_connector_type("text_editor") -> "computer_use.text_editor"
"""
if action:
return f"computer_use.{action}"
return f"computer_use.{tool_name}"


class ComputerUseGovernor:
"""Governance middleware for Anthropic Computer Use tool_use blocks.

Expand Down Expand Up @@ -162,18 +149,30 @@ async def check_tool_use(self, tool_use_block: dict[str, Any]) -> CheckResult:
policies_evaluated=0,
)

# Derive connector_type from tool name + action
action = tool_input.get("action") if isinstance(tool_input, dict) else None
connector_type = _derive_connector_type(
name,
action if isinstance(action, str) else None,
)

# Serialize input for policy evaluation
# Two-field (server, tool) identity contract (epic #2905 / #2904).
# `connector_type` is the Computer Use connector/domain marker
# ("computer_use") and `tool` is the tool NAME (e.g. "computer",
# "bash", "text_editor") — the same (server, tool) mapping the
# LangGraph adapters use, so the `tool_name` audit column means the
# tool name regardless of which adapter emitted it.
#
# The `action` (e.g. "left_click", "screenshot") is NOT a tool
# identity, so it must not go in `tool`. It stays inside `statement`
# below — `json.dumps(tool_input)` already includes the "action"
# key, and the policy engine evaluates the statement. It is
# deliberately NOT placed in the `operation` field, which the
# agent-api spec constrains to the enum {query, execute}.
#
# Previously both were folded into a single derived connector_type
# string ("computer_use.{action}"), which silently discarded the tool
# name whenever an action was present. Neither value is dropped now.

# Serialize input for policy evaluation (retains the action).
statement = json.dumps(tool_input, default=str)

check = await self._client.mcp_check_input(
connector_type=connector_type,
connector_type="computer_use",
tool=name,
statement=statement,
operation="execute",
)
Expand Down Expand Up @@ -204,10 +203,16 @@ async def check_result(
Returns:
CheckResult with allowed status and optional redacted_result.
"""
connector_type = _derive_connector_type(tool_name)

# Same (server, tool) identity as check_tool_use(): the connector is
# the "computer_use" domain marker and the tool NAME is `tool_name`
# (e.g. "computer", "bash"), so the request- and response-plane rows
# share one connector_type and one tool identity. NB: the platform's
# check-output schema does not yet consume `tool` (#2955, targeted for
# v9.11.0); it is sent now for forward compatibility and is ignored by
# every current platform version (omitted on the wire when empty).
check = await self._client.mcp_check_output(
connector_type=connector_type,
connector_type="computer_use",
tool=tool_name,
message=result,
)

Expand Down
25 changes: 25 additions & 0 deletions axonflow/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -1438,6 +1438,7 @@ async def mcp_check_input(
operation: str = "execute",
parameters: dict[str, Any] | None = None,
*,
tool: str | None = None,
client_id: str | None = None,
tenant_id: str | None = None,
user_id: str | None = None,
Expand All @@ -1455,6 +1456,10 @@ async def mcp_check_input(
statement: The SQL query or command to validate.
operation: Operation type - "query" or "execute" (default).
parameters: Optional query parameters.
tool: Optional tool name, distinct from ``connector_type``. Lets a
PEP report the (server, tool) identity as two separate values
instead of concatenating them into ``connector_type``
(epic #2905 / #2904).
client_id: Client identifier (overrides client config when set).
tenant_id: Tenant identifier for multi-tenant scoping.
user_id: End-user identifier for per-user policies.
Expand All @@ -1481,6 +1486,8 @@ async def mcp_check_input(
}
if parameters:
body["parameters"] = parameters
if tool:
body["tool"] = tool
# Wire-canonical scoping fields surfaced in the v6 sweep.
if client_id is not None:
body["client_id"] = client_id
Expand Down Expand Up @@ -1518,6 +1525,7 @@ async def check_tool_input(
operation: str = "execute",
parameters: dict[str, Any] | None = None,
*,
tool: str | None = None,
client_id: str | None = None,
tenant_id: str | None = None,
user_id: str | None = None,
Expand All @@ -1530,6 +1538,7 @@ async def check_tool_input(
statement,
operation,
parameters,
tool=tool,
client_id=client_id,
tenant_id=tenant_id,
user_id=user_id,
Expand All @@ -1545,6 +1554,7 @@ async def mcp_check_output(
metadata: dict[str, Any] | None = None,
row_count: int = 0,
*,
tool: str | None = None,
client_id: str | None = None,
tenant_id: str | None = None,
user_id: str | None = None,
Expand All @@ -1561,6 +1571,9 @@ async def mcp_check_output(
message: Execute-style response message (e.g., "5 rows affected").
metadata: Connector metadata for SQLi scanning.
row_count: Total number of rows returned.
tool: Optional tool name, distinct from ``connector_type``. Mirrors
the ``tool`` parameter on :meth:`mcp_check_input` (epic #2905 /
#2904).
client_id: Client identifier (overrides client config when set).
tenant_id: Tenant identifier for multi-tenant scoping.
user_id: End-user identifier for per-user policies.
Expand All @@ -1584,6 +1597,8 @@ async def mcp_check_output(
body["metadata"] = metadata
if row_count > 0:
body["row_count"] = row_count
if tool:
body["tool"] = tool
# Wire-canonical scoping fields surfaced in the v6 sweep.
if client_id is not None:
body["client_id"] = client_id
Expand Down Expand Up @@ -1618,6 +1633,7 @@ async def check_tool_output(
metadata: dict[str, Any] | None = None,
row_count: int = 0,
*,
tool: str | None = None,
client_id: str | None = None,
tenant_id: str | None = None,
user_id: str | None = None,
Expand All @@ -1630,6 +1646,7 @@ async def check_tool_output(
message,
metadata,
row_count,
tool=tool,
client_id=client_id,
tenant_id=tenant_id,
user_id=user_id,
Expand Down Expand Up @@ -7555,6 +7572,7 @@ def mcp_check_input(
operation: str = "execute",
parameters: dict[str, Any] | None = None,
*,
tool: str | None = None,
client_id: str | None = None,
tenant_id: str | None = None,
user_id: str | None = None,
Expand All @@ -7569,6 +7587,7 @@ def mcp_check_input(
statement,
operation,
parameters,
tool=tool,
client_id=client_id,
tenant_id=tenant_id,
user_id=user_id,
Expand Down Expand Up @@ -7618,6 +7637,7 @@ def mcp_check_output(
metadata: dict[str, Any] | None = None,
row_count: int = 0,
*,
tool: str | None = None,
client_id: str | None = None,
tenant_id: str | None = None,
user_id: str | None = None,
Expand All @@ -7631,6 +7651,7 @@ def mcp_check_output(
message,
metadata,
row_count,
tool=tool,
client_id=client_id,
tenant_id=tenant_id,
user_id=user_id,
Expand All @@ -7645,6 +7666,7 @@ def check_tool_input(
operation: str = "execute",
parameters: dict[str, Any] | None = None,
*,
tool: str | None = None,
client_id: str | None = None,
tenant_id: str | None = None,
user_id: str | None = None,
Expand All @@ -7658,6 +7680,7 @@ def check_tool_input(
statement,
operation,
parameters,
tool=tool,
client_id=client_id,
tenant_id=tenant_id,
user_id=user_id,
Expand All @@ -7674,6 +7697,7 @@ def check_tool_output(
metadata: dict[str, Any] | None = None,
row_count: int = 0,
*,
tool: str | None = None,
client_id: str | None = None,
tenant_id: str | None = None,
user_id: str | None = None,
Expand All @@ -7687,6 +7711,7 @@ def check_tool_output(
message,
metadata,
row_count,
tool=tool,
client_id=client_id,
tenant_id=tenant_id,
user_id=user_id,
Expand Down
14 changes: 14 additions & 0 deletions axonflow/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,13 @@ class MCPCheckInputRequest(BaseModel):
"""Request to validate input against MCP policies."""

connector_type: str
# Two-field (server, tool) identity contract (epic #2905 / #2904). `tool`
# carries the MCP tool name so a PEP no longer has to concatenate it into
# `connector_type` (e.g. "server.tool") to preserve tool identity.
# Source of truth: platform/agent MCPCheckInputRequest (#2904, merged to
# axonflow-enterprise as c8df2006b and first released in platform v9.10.0);
# consumed on the request-input plane. Platforms below v9.10.0 ignore it.
tool: str | None = Field(default=None)
statement: str
parameters: dict[str, Any] | None = Field(default=None)
operation: str = Field(default="execute")
Expand Down Expand Up @@ -462,6 +469,13 @@ class MCPCheckOutputRequest(BaseModel):
"""Request to validate output against MCP policies."""

connector_type: str
# Two-field (server, tool) identity contract, mirrored from
# MCPCheckInputRequest.tool (epic #2905 / #2904). Unlike the input-phase
# field, the platform's MCPCheckOutputRequest has no matching `tool` field
# on ANY released version yet (tracked by #2955); sending it is
# forward-compatible and harmless — the agent's JSON decoder ignores
# unrecognized keys — but it is not yet consumed server-side.
tool: str | None = Field(default=None)
response_data: list[dict[str, Any]] | None = Field(default=None)
message: str | None = Field(default=None)
metadata: dict[str, Any] | None = Field(default=None)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "axonflow"
version = "8.5.1"
version = "9.0.0"
description = "AxonFlow Python SDK - Enterprise AI Governance in 3 Lines of Code"
readme = "README.md"
license = {text = "MIT"}
Expand Down
Loading
Loading