From 071b50c010a4dd117a9b9e1c1e9a43198543bfc6 Mon Sep 17 00:00:00 2001 From: Saurabh Jain Date: Tue, 5 May 2026 02:58:48 +0200 Subject: [PATCH 1/3] feat(license): inject X-Axonflow-Client header on every governed request MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Per ADR-050 §4, every governed client must set `X-Axonflow-Client: /` on every request to the agent so the agent can derive request scope (sdk) and validate it against the token's aud.scope via HasScope(). Adds the header alongside the existing User-Agent in the AsyncClient constructor. Value sourced from the bundled __version__ constant; no env override (the consumer doesn't get to spoof its own client identity to the agent). Test coverage: - tests/test_client_header.py asserts the header is forwarded on proxy_llm_call requests and matches the agent-parseable "/" contract. Signed-off-by: Saurabh Jain --- axonflow/client.py | 8 +++++- tests/test_client_header.py | 57 +++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 tests/test_client_header.py diff --git a/axonflow/client.py b/axonflow/client.py index cb79050..a14e380 100644 --- a/axonflow/client.py +++ b/axonflow/client.py @@ -509,10 +509,16 @@ def __init__( # Configure SSL verification verify_ssl: bool = not insecure_skip_verify - # Build headers + # Build headers. + # ADR-050 §4: every governed request to the agent carries + # X-Axonflow-Client so the agent can derive request scope (sdk) and + # validate it against the token's aud.scope via HasScope(). Sourced + # from the bundled _SDK_VERSION constant; no env override (the + # consumer doesn't get to spoof its own client identity to the agent). headers: dict[str, str] = { "Content-Type": "application/json", "User-Agent": f"axonflow-sdk-python/{_SDK_VERSION}", + "X-Axonflow-Client": f"sdk-python/{_SDK_VERSION}", } # Always send Basic auth — server derives tenant from clientId. # Uses effective client_id ("community" default when not configured). diff --git a/tests/test_client_header.py b/tests/test_client_header.py new file mode 100644 index 0000000..ce47526 --- /dev/null +++ b/tests/test_client_header.py @@ -0,0 +1,57 @@ +"""X-Axonflow-Client header injection — ADR-050 §4. + +Asserts every governed HTTP request forwards +``X-Axonflow-Client: sdk-python/<__version__>`` so the agent can derive +request scope (sdk) and validate against the token's aud.scope via +HasScope(). + +Header is sourced from the bundled __version__ constant; the consumer +cannot spoof its own client identity through config (intentional — +honest-99% header injection per ADR-050 §4). +""" + +import pytest + +from axonflow import AxonFlow +from axonflow._version import __version__ + + +EXPECTED_CLIENT = f"sdk-python/{__version__}" + + +@pytest.mark.asyncio +async def test_client_header_present_on_proxy_call(httpx_mock): + """Every governed request includes X-Axonflow-Client.""" + httpx_mock.add_response( + url="http://localhost:8080/api/request", + json={"success": True, "data": {"answer": "ok"}, "blocked": False}, + ) + + client = AxonFlow( + endpoint="http://localhost:8080", + client_id="test-client", + client_secret="test-secret", + ) + async with client: + await client.proxy_llm_call(user_token="", query="ping", request_type="chat") + + requests = httpx_mock.get_requests() + assert len(requests) == 1 + headers = dict(requests[0].headers) + # httpx normalizes header keys to lowercase + assert headers.get("x-axonflow-client") == EXPECTED_CLIENT + + +@pytest.mark.asyncio +async def test_client_header_format_is_id_slash_semver(): + """Sanity: the value matches sdk-python/. + + The agent's deriveScopeFromClientHeader splits on '/' and maps + "sdk-*" prefixes to scope=sdk. If we ever ship a different shape + this fails loudly so we don't regress agent-side parsing. + """ + import re + + assert EXPECTED_CLIENT.startswith("sdk-python/") + assert re.match(r"^sdk-python/\d+\.\d+\.\d+", EXPECTED_CLIENT) + assert EXPECTED_CLIENT.count("/") == 1 From fdbf341aeaa211624c969749d945874d0a0b33af Mon Sep 17 00:00:00 2001 From: Saurabh Jain Date: Tue, 5 May 2026 03:10:17 +0200 Subject: [PATCH 2/3] style(lint): ruff isort on tests/test_client_header.py Signed-off-by: Saurabh Jain --- tests/test_client_header.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_client_header.py b/tests/test_client_header.py index ce47526..3ed27db 100644 --- a/tests/test_client_header.py +++ b/tests/test_client_header.py @@ -15,7 +15,6 @@ from axonflow import AxonFlow from axonflow._version import __version__ - EXPECTED_CLIENT = f"sdk-python/{__version__}" From 9f7073c85495a0975d7f2fd3818d46ddea9e7cb9 Mon Sep 17 00:00:00 2001 From: Saurabh Jain Date: Tue, 5 May 2026 03:23:47 +0200 Subject: [PATCH 3/3] chore(lint): refresh falsey_clobber baseline for line-shift after header injection Signed-off-by: Saurabh Jain --- .lint_baselines/falsey_clobber.json | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/.lint_baselines/falsey_clobber.json b/.lint_baselines/falsey_clobber.json index b734209..e08f9f7 100644 --- a/.lint_baselines/falsey_clobber.json +++ b/.lint_baselines/falsey_clobber.json @@ -22,24 +22,24 @@ "axonflow/adapters/tool_wrapper.py:190:20", "axonflow/adapters/tool_wrapper.py:208:20", "axonflow/adapters/tool_wrapper.py:220:20", - "axonflow/client.py:1059:16", - "axonflow/client.py:1136:16", - "axonflow/client.py:1608:37", - "axonflow/client.py:1649:18", - "axonflow/client.py:1707:37", - "axonflow/client.py:2225:24", - "axonflow/client.py:2246:33", - "axonflow/client.py:2247:31", - "axonflow/client.py:2259:25", - "axonflow/client.py:2320:28", - "axonflow/client.py:2361:69", + "axonflow/client.py:1065:16", + "axonflow/client.py:1142:16", + "axonflow/client.py:1614:37", + "axonflow/client.py:1655:18", + "axonflow/client.py:1713:37", + "axonflow/client.py:2231:24", + "axonflow/client.py:2252:33", + "axonflow/client.py:2253:31", + "axonflow/client.py:2265:25", + "axonflow/client.py:2326:28", + "axonflow/client.py:2367:69", "axonflow/client.py:286:14", "axonflow/client.py:291:24", "axonflow/client.py:292:20", "axonflow/client.py:488:44", - "axonflow/client.py:6068:25", - "axonflow/client.py:793:20", - "axonflow/client.py:879:20", + "axonflow/client.py:6074:25", + "axonflow/client.py:799:20", + "axonflow/client.py:885:20", "axonflow/execution.py:205:19", "axonflow/interceptors/anthropic.py:134:43", "axonflow/interceptors/anthropic.py:161:43",