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
32 changes: 16 additions & 16 deletions .lint_baselines/falsey_clobber.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,33 +11,33 @@
"axonflow/adapters/langchain.py:160:15",
"axonflow/adapters/langchain.py:160:64",
"axonflow/adapters/langchain.py:169:16",
"axonflow/adapters/langgraph.py:569:33",
"axonflow/adapters/langgraph.py:583:43",
"axonflow/adapters/langgraph.py:597:20",
"axonflow/adapters/langgraph.py:654:33",
"axonflow/adapters/langgraph.py:668:20",
"axonflow/adapters/langgraph.py:692:20",
"axonflow/adapters/langgraph.py:573:33",
"axonflow/adapters/langgraph.py:592:43",
"axonflow/adapters/langgraph.py:607:20",
"axonflow/adapters/langgraph.py:670:33",
"axonflow/adapters/langgraph.py:686:20",
"axonflow/adapters/langgraph.py:711:20",
"axonflow/adapters/langgraph_wrapper.py:374:19",
"axonflow/adapters/tool_wrapper.py:176:20",
"axonflow/adapters/tool_wrapper.py:190:20",
"axonflow/adapters/tool_wrapper.py:208: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"
33 changes: 26 additions & 7 deletions axonflow/adapters/langgraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,10 @@ class MCPInterceptorOptions:

Attributes:
connector_type_fn: Optional callable that maps an MCP request to a
connector type string. Defaults to ``"{server_name}.{tool_name}"``.
connector type string. Defaults to the request's ``server_name``.
The tool name is always sent separately as the ``tool`` field
(see :meth:`~axonflow.client.Client.mcp_check_input`) — it is no
longer folded into ``connector_type``.
operation: Operation type passed to ``mcp_check_input``. Defaults to
``"execute"``. Set to ``"query"`` for known read-only tool calls.
"""
Expand Down Expand Up @@ -522,11 +525,12 @@ def mcp_tool_interceptor(
... tool_interceptors=[adapter.mcp_tool_interceptor()],
... )

With custom options:
With custom options (the tool name is always sent separately as
``tool`` — ``connector_type_fn`` only controls ``connector_type``):

Example:
>>> opts = MCPInterceptorOptions(
... connector_type_fn=lambda req: req.server_name,
... connector_type_fn=lambda req: f"prod.{req.server_name}",
... operation="query",
... )
>>> tool_interceptors=[adapter.mcp_tool_interceptor(opts)]
Expand Down Expand Up @@ -564,17 +568,22 @@ def mcp_tool_interceptor(
opts = options or MCPInterceptorOptions()

def _default_connector_type(request: Any) -> str:
return f"{request.server_name}.{request.name}"
return str(request.server_name)

resolve_connector_type = opts.connector_type_fn or _default_connector_type

async def _interceptor(request: Any, handler: Callable[..., Any]) -> Any:
connector_type = resolve_connector_type(request)
# The tool name is a genuine second identity field (epic #2905 /
# #2904) — send it as `tool`, never fold it back into
# `connector_type`, even when connector_type_fn is customized.
tool = request.name
args_str = json.dumps(request.args, default=str) if request.args else "{}"
statement = f"{connector_type}({args_str})"
statement = f"{connector_type}.{tool}({args_str})"

pre_check = await self.client.mcp_check_input(
connector_type=connector_type,
tool=tool,
statement=statement,
operation=opts.operation,
parameters=request.args,
Expand All @@ -590,6 +599,7 @@ async def _interceptor(request: Any, handler: Callable[..., Any]) -> Any:
result_str = str(result)
output_check = await self.client.mcp_check_output(
connector_type=connector_type,
tool=tool,
message=result_str,
)
if not output_check.allowed:
Expand Down Expand Up @@ -620,11 +630,12 @@ def tool_output_wrapper(
>>> wrapper = adapter.tool_output_wrapper()
>>> tool_node = ToolNode(tools, awrap_tool_call=wrapper)

With custom options:
With custom options (the tool name is always sent separately as
``tool`` — ``connector_type_fn`` only controls ``connector_type``):

Example:
>>> opts = MCPInterceptorOptions(
... connector_type_fn=lambda call: f"local.{call['name']}",
... connector_type_fn=lambda call: "local",
... )
>>> tool_node = ToolNode(tools, awrap_tool_call=adapter.tool_output_wrapper(opts))

Expand All @@ -648,18 +659,25 @@ def tool_output_wrapper(
opts = options or MCPInterceptorOptions()

def _default_connector_type(call_request: dict[str, Any]) -> str:
# ToolNode's call_request carries no server/origin info to
# distinguish from the bare tool name — this is the best
# available default. The tool name itself is never dropped: it
# is always sent separately as `tool` below (epic #2905 /
# #2904), even when it also ends up equal to connector_type here.
name: str = call_request.get("name", "unknown_tool")
return name

resolve_connector_type = opts.connector_type_fn or _default_connector_type

async def _wrapper(call_request: dict[str, Any], execute: Callable[..., Any]) -> Any:
connector_type = resolve_connector_type(call_request)
tool: str = call_request.get("name", "unknown_tool")
args = call_request.get("args", {})
statement = json.dumps(args, default=str)

input_check = await self.client.mcp_check_input(
connector_type=connector_type,
tool=tool,
statement=statement,
operation=opts.operation,
)
Expand All @@ -685,6 +703,7 @@ async def _wrapper(call_request: dict[str, Any], execute: Callable[..., Any]) ->

output_check = await self.client.mcp_check_output(
connector_type=connector_type,
tool=tool,
message=serialized,
)
if not output_check.allowed:
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
Loading
Loading