Skip to content

Commit e830f3e

Browse files
fix: use smart defaults for Gateway Mode client_id (#72)
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.
1 parent 2c44a7a commit e830f3e

3 files changed

Lines changed: 82 additions & 64 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [1.7.1] - 2026-01-25
99

10+
### Changed
11+
12+
- **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
13+
1014
### Fixed
1115

1216
- **PolicyCategory**: Added `PII_SINGAPORE = "pii-singapore"` enum value for Singapore PII detection policies (NRIC, FIN, UEN patterns)

axonflow/client.py

Lines changed: 17 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -394,25 +394,17 @@ def _has_credentials(self) -> bool:
394394
"""
395395
return bool(self._config.client_id)
396396

397-
def _require_credentials(self, feature: str) -> None:
398-
"""Require credentials for enterprise features.
397+
def _get_effective_client_id(self) -> str:
398+
"""Get the effective client_id, using smart default for community mode.
399399
400-
Raises AuthenticationError if client_id is not configured.
401-
Note: client_secret is optional for community mode.
400+
Returns the configured client_id if set, otherwise returns "community"
401+
as a smart default. This enables zero-config usage for community/self-hosted
402+
deployments while still supporting enterprise deployments with explicit credentials.
402403
403-
Args:
404-
feature: Name of the feature requiring credentials (for error message)
405-
406-
Raises:
407-
AuthenticationError: If client_id is not configured
404+
Returns:
405+
The client_id to use in requests
408406
"""
409-
if not self._has_credentials():
410-
msg = (
411-
f"{feature} requires client_id. "
412-
"Set client_id when creating the client "
413-
"(client_secret is optional for community mode)."
414-
)
415-
raise AuthenticationError(msg)
407+
return self._config.client_id if self._config.client_id else "community"
416408

417409
async def __aenter__(self) -> AxonFlow:
418410
"""Async context manager entry."""
@@ -1160,8 +1152,8 @@ async def get_policy_approved_context(
11601152
LLM call to ensure policy compliance.
11611153
11621154
Note:
1163-
This is an enterprise feature that requires credentials.
1164-
Set client_id and client_secret when creating the client.
1155+
Uses smart default "community" for client_id if not configured,
1156+
enabling zero-config usage for community/self-hosted deployments.
11651157
11661158
Args:
11671159
user_token: JWT token for the user making the request
@@ -1173,7 +1165,7 @@ async def get_policy_approved_context(
11731165
PolicyApprovalResult with context ID and approved data
11741166
11751167
Raises:
1176-
AuthenticationError: If credentials are not configured or user token is invalid
1168+
AuthenticationError: If user token is invalid
11771169
ConnectionError: If unable to reach AxonFlow Agent
11781170
TimeoutError: If request times out
11791171
@@ -1186,12 +1178,12 @@ async def get_policy_approved_context(
11861178
>>> if not result.approved:
11871179
... raise PolicyViolationError(result.block_reason)
11881180
"""
1189-
# Gateway Mode is an enterprise feature that requires credentials
1190-
self._require_credentials("Gateway Mode (get_policy_approved_context)")
1181+
# Use smart default for client_id - enables zero-config community mode
1182+
client_id = self._get_effective_client_id()
11911183

11921184
request_body = {
11931185
"user_token": user_token,
1194-
"client_id": self._config.client_id,
1186+
"client_id": client_id,
11951187
"query": query,
11961188
"data_sources": data_sources or [],
11971189
"context": context or {},
@@ -1322,12 +1314,12 @@ async def audit_llm_call(
13221314
... latency_ms=250
13231315
... )
13241316
"""
1325-
# Gateway Mode is an enterprise feature that requires credentials
1326-
self._require_credentials("Gateway Mode (audit_llm_call)")
1317+
# Use smart default for client_id - enables zero-config community mode
1318+
client_id = self._get_effective_client_id()
13271319

13281320
request_body = {
13291321
"context_id": context_id,
1330-
"client_id": self._config.client_id,
1322+
"client_id": client_id,
13311323
"response_summary": response_summary,
13321324
"provider": provider,
13331325
"model": model,

tests/test_auth_headers.py

Lines changed: 61 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -133,67 +133,89 @@ async def test_no_auth_headers_for_health_check(self, httpx_mock):
133133

134134

135135
class TestEnterpriseFeatureValidation:
136-
"""Test that enterprise features require client_id before making requests."""
136+
"""Test that Gateway Mode uses smart defaults for client_id."""
137137

138138
@pytest.mark.asyncio
139-
async def test_pre_check_fails_without_client_id(self, httpx_mock):
140-
"""get_policy_approved_context should fail before making request when no client_id."""
141-
# Don't mock the endpoint - we should fail before making the request
139+
async def test_pre_check_uses_smart_default_without_client_id(self, httpx_mock):
140+
"""get_policy_approved_context should use 'community' as default client_id."""
141+
httpx_mock.add_response(
142+
url="http://localhost:8080/api/policy/pre-check",
143+
json={
144+
"context_id": "ctx_smart_default",
145+
"approved": True,
146+
"policies": [],
147+
"expires_at": "2025-12-20T12:00:00Z",
148+
},
149+
)
150+
142151
client = AxonFlow(
143152
endpoint="http://localhost:8080",
144-
# No client_id - truly no credentials
153+
# No client_id - should use "community" smart default
145154
debug=True,
146155
)
147156

148157
async with client:
149-
with pytest.raises(AuthenticationError) as exc_info:
150-
await client.get_policy_approved_context(
151-
user_token="",
152-
query="Test query",
153-
)
158+
result = await client.get_policy_approved_context(
159+
user_token="test-user",
160+
query="Test query",
161+
)
154162

155-
assert "requires client_id" in str(exc_info.value)
156-
assert "Gateway Mode" in str(exc_info.value)
163+
assert result.approved is True
164+
assert result.context_id == "ctx_smart_default"
157165

158-
# No request should have been made
166+
# Verify request was made with smart default client_id
159167
requests = httpx_mock.get_requests()
160-
assert len(requests) == 0
168+
assert len(requests) == 1
169+
import json
170+
171+
body = json.loads(requests[0].content)
172+
assert body["client_id"] == "community"
161173

162-
print("✅ get_policy_approved_context fails without client_id (no request made)")
174+
print("✅ get_policy_approved_context uses 'community' smart default")
163175

164176
@pytest.mark.asyncio
165-
async def test_audit_fails_without_client_id(self, httpx_mock):
166-
"""audit_llm_call should fail before making request when no client_id."""
167-
# Don't mock the endpoint - we should fail before making the request
177+
async def test_audit_uses_smart_default_without_client_id(self, httpx_mock):
178+
"""audit_llm_call should use 'community' as default client_id."""
179+
httpx_mock.add_response(
180+
url="http://localhost:8080/api/audit/llm-call",
181+
json={
182+
"success": True,
183+
"audit_id": "audit_smart_default",
184+
},
185+
)
186+
168187
client = AxonFlow(
169188
endpoint="http://localhost:8080",
170-
# No client_id - truly no credentials
189+
# No client_id - should use "community" smart default
171190
debug=True,
172191
)
173192

174193
async with client:
175-
with pytest.raises(AuthenticationError) as exc_info:
176-
await client.audit_llm_call(
177-
context_id="ctx_123",
178-
response_summary="Test response",
179-
provider="openai",
180-
model="gpt-4",
181-
token_usage=TokenUsage(
182-
prompt_tokens=100,
183-
completion_tokens=50,
184-
total_tokens=150,
185-
),
186-
latency_ms=250,
187-
)
188-
189-
assert "requires client_id" in str(exc_info.value)
190-
assert "Gateway Mode" in str(exc_info.value)
191-
192-
# No request should have been made
194+
result = await client.audit_llm_call(
195+
context_id="ctx_123",
196+
response_summary="Test response",
197+
provider="openai",
198+
model="gpt-4",
199+
token_usage=TokenUsage(
200+
prompt_tokens=100,
201+
completion_tokens=50,
202+
total_tokens=150,
203+
),
204+
latency_ms=250,
205+
)
206+
207+
assert result.success is True
208+
assert result.audit_id == "audit_smart_default"
209+
210+
# Verify request was made with smart default client_id
193211
requests = httpx_mock.get_requests()
194-
assert len(requests) == 0
212+
assert len(requests) == 1
213+
import json
214+
215+
body = json.loads(requests[0].content)
216+
assert body["client_id"] == "community"
195217

196-
print("✅ audit_llm_call fails without client_id (no request made)")
218+
print("✅ audit_llm_call uses 'community' smart default")
197219

198220
@pytest.mark.asyncio
199221
async def test_pre_check_works_with_credentials(self, httpx_mock):

0 commit comments

Comments
 (0)