From 5a999cea03df81139522d049f51a813752612964 Mon Sep 17 00:00:00 2001 From: Saurabh Jain Date: Sun, 25 Jan 2026 22:06:54 +0100 Subject: [PATCH] fix: use smart defaults for Gateway Mode client_id Replace _require_credentials() with _get_effective_client_id() that returns "community" as default when client_id is not configured. This enables zero-config usage for community/self-hosted deployments while still supporting enterprise deployments with explicit credentials. Gateway Mode methods (get_policy_approved_context, audit_llm_call) now work without requiring credentials to be configured. --- CHANGELOG.md | 4 ++ axonflow/client.py | 42 +++++++--------- tests/test_auth_headers.py | 100 ++++++++++++++++++++++--------------- 3 files changed, 82 insertions(+), 64 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c81a92..ea34d01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.7.1] - 2026-01-25 +### Changed + +- **Gateway Mode smart defaults**: `get_policy_approved_context()` and `audit_llm_call()` now use `"community"` as default client_id when not configured, enabling zero-config usage for community/self-hosted deployments + ### Fixed - **PolicyCategory**: Added `PII_SINGAPORE = "pii-singapore"` enum value for Singapore PII detection policies (NRIC, FIN, UEN patterns) diff --git a/axonflow/client.py b/axonflow/client.py index 3260074..30db665 100644 --- a/axonflow/client.py +++ b/axonflow/client.py @@ -394,25 +394,17 @@ def _has_credentials(self) -> bool: """ return bool(self._config.client_id) - def _require_credentials(self, feature: str) -> None: - """Require credentials for enterprise features. + def _get_effective_client_id(self) -> str: + """Get the effective client_id, using smart default for community mode. - Raises AuthenticationError if client_id is not configured. - Note: client_secret is optional for community mode. + Returns the configured client_id if set, otherwise returns "community" + as a smart default. This enables zero-config usage for community/self-hosted + deployments while still supporting enterprise deployments with explicit credentials. - Args: - feature: Name of the feature requiring credentials (for error message) - - Raises: - AuthenticationError: If client_id is not configured + Returns: + The client_id to use in requests """ - if not self._has_credentials(): - msg = ( - f"{feature} requires client_id. " - "Set client_id when creating the client " - "(client_secret is optional for community mode)." - ) - raise AuthenticationError(msg) + return self._config.client_id if self._config.client_id else "community" async def __aenter__(self) -> AxonFlow: """Async context manager entry.""" @@ -1160,8 +1152,8 @@ async def get_policy_approved_context( LLM call to ensure policy compliance. Note: - This is an enterprise feature that requires credentials. - Set client_id and client_secret when creating the client. + Uses smart default "community" for client_id if not configured, + enabling zero-config usage for community/self-hosted deployments. Args: user_token: JWT token for the user making the request @@ -1173,7 +1165,7 @@ async def get_policy_approved_context( PolicyApprovalResult with context ID and approved data Raises: - AuthenticationError: If credentials are not configured or user token is invalid + AuthenticationError: If user token is invalid ConnectionError: If unable to reach AxonFlow Agent TimeoutError: If request times out @@ -1186,12 +1178,12 @@ async def get_policy_approved_context( >>> if not result.approved: ... raise PolicyViolationError(result.block_reason) """ - # Gateway Mode is an enterprise feature that requires credentials - self._require_credentials("Gateway Mode (get_policy_approved_context)") + # Use smart default for client_id - enables zero-config community mode + client_id = self._get_effective_client_id() request_body = { "user_token": user_token, - "client_id": self._config.client_id, + "client_id": client_id, "query": query, "data_sources": data_sources or [], "context": context or {}, @@ -1322,12 +1314,12 @@ async def audit_llm_call( ... latency_ms=250 ... ) """ - # Gateway Mode is an enterprise feature that requires credentials - self._require_credentials("Gateway Mode (audit_llm_call)") + # Use smart default for client_id - enables zero-config community mode + client_id = self._get_effective_client_id() request_body = { "context_id": context_id, - "client_id": self._config.client_id, + "client_id": client_id, "response_summary": response_summary, "provider": provider, "model": model, diff --git a/tests/test_auth_headers.py b/tests/test_auth_headers.py index 435997f..6d03e20 100644 --- a/tests/test_auth_headers.py +++ b/tests/test_auth_headers.py @@ -133,67 +133,89 @@ async def test_no_auth_headers_for_health_check(self, httpx_mock): class TestEnterpriseFeatureValidation: - """Test that enterprise features require client_id before making requests.""" + """Test that Gateway Mode uses smart defaults for client_id.""" @pytest.mark.asyncio - async def test_pre_check_fails_without_client_id(self, httpx_mock): - """get_policy_approved_context should fail before making request when no client_id.""" - # Don't mock the endpoint - we should fail before making the request + async def test_pre_check_uses_smart_default_without_client_id(self, httpx_mock): + """get_policy_approved_context should use 'community' as default client_id.""" + httpx_mock.add_response( + url="http://localhost:8080/api/policy/pre-check", + json={ + "context_id": "ctx_smart_default", + "approved": True, + "policies": [], + "expires_at": "2025-12-20T12:00:00Z", + }, + ) + client = AxonFlow( endpoint="http://localhost:8080", - # No client_id - truly no credentials + # No client_id - should use "community" smart default debug=True, ) async with client: - with pytest.raises(AuthenticationError) as exc_info: - await client.get_policy_approved_context( - user_token="", - query="Test query", - ) + result = await client.get_policy_approved_context( + user_token="test-user", + query="Test query", + ) - assert "requires client_id" in str(exc_info.value) - assert "Gateway Mode" in str(exc_info.value) + assert result.approved is True + assert result.context_id == "ctx_smart_default" - # No request should have been made + # Verify request was made with smart default client_id requests = httpx_mock.get_requests() - assert len(requests) == 0 + assert len(requests) == 1 + import json + + body = json.loads(requests[0].content) + assert body["client_id"] == "community" - print("✅ get_policy_approved_context fails without client_id (no request made)") + print("✅ get_policy_approved_context uses 'community' smart default") @pytest.mark.asyncio - async def test_audit_fails_without_client_id(self, httpx_mock): - """audit_llm_call should fail before making request when no client_id.""" - # Don't mock the endpoint - we should fail before making the request + async def test_audit_uses_smart_default_without_client_id(self, httpx_mock): + """audit_llm_call should use 'community' as default client_id.""" + httpx_mock.add_response( + url="http://localhost:8080/api/audit/llm-call", + json={ + "success": True, + "audit_id": "audit_smart_default", + }, + ) + client = AxonFlow( endpoint="http://localhost:8080", - # No client_id - truly no credentials + # No client_id - should use "community" smart default debug=True, ) async with client: - with pytest.raises(AuthenticationError) as exc_info: - await client.audit_llm_call( - context_id="ctx_123", - response_summary="Test response", - provider="openai", - model="gpt-4", - token_usage=TokenUsage( - prompt_tokens=100, - completion_tokens=50, - total_tokens=150, - ), - latency_ms=250, - ) - - assert "requires client_id" in str(exc_info.value) - assert "Gateway Mode" in str(exc_info.value) - - # No request should have been made + result = await client.audit_llm_call( + context_id="ctx_123", + response_summary="Test response", + provider="openai", + model="gpt-4", + token_usage=TokenUsage( + prompt_tokens=100, + completion_tokens=50, + total_tokens=150, + ), + latency_ms=250, + ) + + assert result.success is True + assert result.audit_id == "audit_smart_default" + + # Verify request was made with smart default client_id requests = httpx_mock.get_requests() - assert len(requests) == 0 + assert len(requests) == 1 + import json + + body = json.loads(requests[0].content) + assert body["client_id"] == "community" - print("✅ audit_llm_call fails without client_id (no request made)") + print("✅ audit_llm_call uses 'community' smart default") @pytest.mark.asyncio async def test_pre_check_works_with_credentials(self, httpx_mock):