dominus-sdk-python 9.0.10__tar.gz → 9.0.12__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/PKG-INFO +4 -4
  2. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/README.md +3 -3
  3. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/__init__.py +3 -1
  4. dominus_sdk_python-9.0.12/dominus/helpers/auth.py +57 -0
  5. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/helpers/core.py +18 -2
  6. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/namespaces/__init__.py +2 -0
  7. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/namespaces/ai.py +30 -12
  8. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/namespaces/artifacts.py +12 -1
  9. dominus_sdk_python-9.0.12/dominus/namespaces/usage.py +214 -0
  10. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/start.py +4 -0
  11. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus_sdk_python.egg-info/PKG-INFO +4 -4
  12. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus_sdk_python.egg-info/SOURCES.txt +5 -0
  13. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/pyproject.toml +1 -1
  14. dominus_sdk_python-9.0.12/tests/test_ai_namespace.py +75 -0
  15. dominus_sdk_python-9.0.12/tests/test_artifacts_namespace.py +93 -0
  16. dominus_sdk_python-9.0.12/tests/test_token_precedence.py +174 -0
  17. dominus_sdk_python-9.0.12/tests/test_usage_namespace.py +210 -0
  18. dominus_sdk_python-9.0.10/dominus/helpers/auth.py +0 -23
  19. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/config/__init__.py +0 -0
  20. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/config/endpoints.py +0 -0
  21. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/contract/__init__.py +0 -0
  22. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/contract/envelope.py +0 -0
  23. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/contract/projections.py +0 -0
  24. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/errors.py +0 -0
  25. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/helpers/__init__.py +0 -0
  26. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/helpers/cache.py +0 -0
  27. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/helpers/console_capture.py +0 -0
  28. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/helpers/crypto.py +0 -0
  29. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/helpers/sse.py +0 -0
  30. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/helpers/trace.py +0 -0
  31. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/namespaces/admin.py +0 -0
  32. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/namespaces/auth.py +0 -0
  33. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/namespaces/authority.py +0 -0
  34. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/namespaces/browser.py +0 -0
  35. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/namespaces/coder.py +0 -0
  36. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/namespaces/courier.py +0 -0
  37. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/namespaces/db.py +0 -0
  38. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/namespaces/ddl.py +0 -0
  39. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/namespaces/deployer.py +0 -0
  40. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/namespaces/fastapi.py +0 -0
  41. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/namespaces/files.py +0 -0
  42. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/namespaces/health.py +0 -0
  43. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/namespaces/jobs.py +0 -0
  44. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/namespaces/logs.py +0 -0
  45. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/namespaces/platform.py +0 -0
  46. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/namespaces/portal.py +0 -0
  47. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/namespaces/processor.py +0 -0
  48. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/namespaces/publisher.py +0 -0
  49. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/namespaces/recipes.py +0 -0
  50. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/namespaces/redis.py +0 -0
  51. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/namespaces/secrets.py +0 -0
  52. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/namespaces/secure.py +0 -0
  53. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/namespaces/stash.py +0 -0
  54. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/namespaces/sync.py +0 -0
  55. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/namespaces/warden.py +0 -0
  56. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/namespaces/workflow.py +0 -0
  57. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/py.typed +0 -0
  58. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus/services/__init__.py +0 -0
  59. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus_sdk_python.egg-info/dependency_links.txt +0 -0
  60. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus_sdk_python.egg-info/requires.txt +0 -0
  61. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/dominus_sdk_python.egg-info/top_level.txt +0 -0
  62. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/setup.cfg +0 -0
  63. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/tests/test_auth.py +0 -0
  64. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/tests/test_authority_public_vocabulary.py +0 -0
  65. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/tests/test_browser_namespace.py +0 -0
  66. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/tests/test_contract.py +0 -0
  67. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/tests/test_control_plane_namespaces.py +0 -0
  68. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/tests/test_error_recovery_action.py +0 -0
  69. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/tests/test_errors.py +0 -0
  70. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/tests/test_errors_aliases.py +0 -0
  71. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/tests/test_flat_commands.py +0 -0
  72. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/tests/test_health.py +0 -0
  73. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/tests/test_logs.py +0 -0
  74. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/tests/test_namespace_barrel_exports.py +0 -0
  75. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/tests/test_platform_coder_namespaces.py +0 -0
  76. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/tests/test_provisioning_parity.py +0 -0
  77. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/tests/test_public_exports.py +0 -0
  78. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/tests/test_publisher_namespace.py +0 -0
  79. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/tests/test_readme_quickstart_catalog.py +0 -0
  80. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/tests/test_recipes_namespace.py +0 -0
  81. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/tests/test_recipes_stash_routing.py +0 -0
  82. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/tests/test_secrets_presence_contract.py +0 -0
  83. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/tests/test_stash_artifact_facade.py +0 -0
  84. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/tests/test_stash_tables_namespace.py +0 -0
  85. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/tests/test_transport_compat.py +0 -0
  86. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/tests/test_version_consistency.py +0 -0
  87. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/tests/test_workflow_lifecycle.py +0 -0
  88. {dominus_sdk_python-9.0.10 → dominus_sdk_python-9.0.12}/tests/test_workflow_refs.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dominus-sdk-python
3
- Version: 9.0.10
3
+ Version: 9.0.12
4
4
  Summary: Python SDK for the Dominus gateway-first platform
5
5
  Author-email: CareBridge Systems <dev@carebridge.io>
6
6
  License-Expression: LicenseRef-Proprietary
@@ -38,8 +38,8 @@ Async Python SDK for the Dominus gateway-first service plane.
38
38
  ## Agent Guide
39
39
 
40
40
  Start with `docs/agent-guide/INDEX.md`. The current snapshot is
41
- `docs/agent-guide/2026-09-10-1002-sdk-python/00-reading-order.md`; the latest
42
- cleanup audit is `docs/janitor/2026-09-10-1002-sdk-python-cleanup-audit.md`.
41
+ `docs/agent-guide/2026-09-14-1300-sdk-python/00-reading-order.md`; the latest
42
+ cleanup audit is `docs/janitor/2026-09-14-1300-sdk-python-cleanup-audit.md`.
43
43
 
44
44
  ## What This Repo Ships
45
45
 
@@ -48,7 +48,7 @@ cleanup audit is `docs/janitor/2026-09-10-1002-sdk-python-cleanup-audit.md`.
48
48
  - Gateway-scoped client mode for MCP and other user-JWT sessions
49
49
  - Transport compatibility for wrapped `{success,data}` responses and unwrapped Warden/control-plane success objects
50
50
  - Local helpers for JWT verification, trace propagation, retries, and console capture
51
- - Current package version: `9.0.9`
51
+ - Current package version: `9.0.12`
52
52
 
53
53
  ## Install
54
54
 
@@ -5,8 +5,8 @@ Async Python SDK for the Dominus gateway-first service plane.
5
5
  ## Agent Guide
6
6
 
7
7
  Start with `docs/agent-guide/INDEX.md`. The current snapshot is
8
- `docs/agent-guide/2026-09-10-1002-sdk-python/00-reading-order.md`; the latest
9
- cleanup audit is `docs/janitor/2026-09-10-1002-sdk-python-cleanup-audit.md`.
8
+ `docs/agent-guide/2026-09-14-1300-sdk-python/00-reading-order.md`; the latest
9
+ cleanup audit is `docs/janitor/2026-09-14-1300-sdk-python-cleanup-audit.md`.
10
10
 
11
11
  ## What This Repo Ships
12
12
 
@@ -15,7 +15,7 @@ cleanup audit is `docs/janitor/2026-09-10-1002-sdk-python-cleanup-audit.md`.
15
15
  - Gateway-scoped client mode for MCP and other user-JWT sessions
16
16
  - Transport compatibility for wrapped `{success,data}` responses and unwrapped Warden/control-plane success objects
17
17
  - Local helpers for JWT verification, trace propagation, retries, and console capture
18
- - Current package version: `9.0.9`
18
+ - Current package version: `9.0.12`
19
19
 
20
20
  ## Install
21
21
 
@@ -121,6 +121,7 @@ from .namespaces.platform import PlatformNamespace
121
121
  from .namespaces.coder import CoderNamespace
122
122
  from .namespaces.publisher import PublisherNamespace
123
123
  from .namespaces.stash import StashNamespace, StashTablesNamespace
124
+ from .namespaces.usage import UsageNamespace
124
125
 
125
126
  # Export AI namespace for agent-runtime operations
126
127
  from .namespaces.ai import (
@@ -190,7 +191,7 @@ from .errors import (
190
191
  TimeoutError as DominusTimeoutError,
191
192
  )
192
193
 
193
- __version__ = "9.0.10"
194
+ __version__ = "9.0.12"
194
195
  __all__ = [
195
196
  # Main SDK instance
196
197
  "dominus",
@@ -249,6 +250,7 @@ __all__ = [
249
250
  "PublisherNamespace",
250
251
  "StashNamespace",
251
252
  "StashTablesNamespace",
253
+ "UsageNamespace",
252
254
  # AI namespace for agent-runtime operations
253
255
  "AiNamespace",
254
256
  "RagSubNamespace",
@@ -0,0 +1,57 @@
1
+ """
2
+ Token resolution helper.
3
+
4
+ Handles resolution of authentication token from environment variables.
5
+ No hardcoded tokens supported - environment variable only.
6
+ """
7
+ import os
8
+ from typing import Optional
9
+
10
+
11
+ def _resolve_token() -> Optional[str]:
12
+ """
13
+ Resolve auth token from environment variable.
14
+
15
+ Precedence: DOMINUS_USER_TOKEN outranks DOMINUS_TOKEN. This is intentional
16
+ (the README quick start is a gateway user-token flow), but it means an
17
+ ambient operator token silently shadows a project PSK. Callers that intend
18
+ project-PSK auth must scrub DOMINUS_USER_TOKEN (and DOMINUS_MCP_USER_TOKEN)
19
+ from the environment before importing this package.
20
+
21
+ Note: When fetching from Infisical via Sovereign, the secret is stored as
22
+ PROVISION_DOMINUS_TOKEN but should be set as DOMINUS_TOKEN in environment.
23
+ The PROVISION_ prefix is dropped when setting environment variables.
24
+
25
+ Returns:
26
+ Token string or None if not set
27
+ """
28
+ return os.getenv("DOMINUS_USER_TOKEN") or os.getenv("DOMINUS_TOKEN")
29
+
30
+
31
+ def _shadowed_token_hint(token: Optional[str]) -> Optional[str]:
32
+ """
33
+ Diagnose a token that may have been shadowed by environment precedence.
34
+
35
+ Returns a message naming BOTH variables when, and only when, the ambiguity
36
+ is real: DOMINUS_USER_TOKEN and DOMINUS_TOKEN are both set AND ``token`` is
37
+ the shadowing value that precedence selected. A caller-supplied token that
38
+ is not the environment's chosen value was never ambiguous, so it returns
39
+ None, as does any environment where only one variable is set.
40
+
41
+ Args:
42
+ token: The token an auth attempt actually used
43
+
44
+ Returns:
45
+ Diagnostic message, or None when there is nothing ambiguous to report
46
+ """
47
+ user_token = os.getenv("DOMINUS_USER_TOKEN")
48
+ psk_token = os.getenv("DOMINUS_TOKEN")
49
+ if not user_token or not psk_token or token != user_token:
50
+ return None
51
+ return (
52
+ "DOMINUS_USER_TOKEN outranks DOMINUS_TOKEN (dominus/helpers/auth.py) and "
53
+ "both are set in this environment, so this attempt authenticated with "
54
+ "DOMINUS_USER_TOKEN. If you intended project-PSK auth, unset "
55
+ "DOMINUS_USER_TOKEN (and DOMINUS_MCP_USER_TOKEN) before importing the "
56
+ "SDK and retry."
57
+ )
@@ -11,6 +11,7 @@ import json
11
11
  import time
12
12
  from typing import Any, Optional
13
13
 
14
+ from .auth import _shadowed_token_hint
14
15
  from .cache import dominus_cache, gateway_circuit_breaker, exponential_backoff_with_jitter
15
16
 
16
17
  # Max retries for HTTP requests (reduced from implicit to explicit)
@@ -349,6 +350,13 @@ def is_jwt_valid(jwt_token: str) -> bool:
349
350
  return False
350
351
 
351
352
 
353
+ def _mint_failure_message(message: str, shadow_hint: Optional[str]) -> str:
354
+ """Append the token-shadowing diagnostic to a failed-mint message."""
355
+ if not shadow_hint:
356
+ return message
357
+ return f"{message}\n{shadow_hint}"
358
+
359
+
352
360
  async def _get_service_jwt(psk_token: str, base_url: str) -> str:
353
361
  """
354
362
  Get service JWT by calling gateway /jwt/mint with PSK.
@@ -392,6 +400,10 @@ async def _get_service_jwt(psk_token: str, base_url: str) -> str:
392
400
  body_json = {"method": "auth.self", "params": {}}
393
401
  body_b64 = _b64_encode(body_json)
394
402
 
403
+ # A rejected mint is indistinguishable from a bad PSK when an ambient
404
+ # DOMINUS_USER_TOKEN shadowed the intended project PSK at import.
405
+ shadow_hint = _shadowed_token_hint(psk_token)
406
+
395
407
  # Retry loop for JWT minting
396
408
  JWT_MINT_RETRIES = 3
397
409
  last_error = None
@@ -419,7 +431,9 @@ async def _get_service_jwt(psk_token: str, base_url: str) -> str:
419
431
  if not result.get("success"):
420
432
  error_msg = result.get("error", "Unknown auth error")
421
433
  gateway_circuit_breaker.record_failure()
422
- raise RuntimeError(f"Auth error: {error_msg}")
434
+ raise RuntimeError(
435
+ _mint_failure_message(f"Auth error: {error_msg}", shadow_hint)
436
+ )
423
437
 
424
438
  data = result.get("data", {})
425
439
  jwt = data.get("access_token") or data.get("token")
@@ -465,7 +479,9 @@ async def _get_service_jwt(psk_token: str, base_url: str) -> str:
465
479
  raise RuntimeError(f"Failed to get JWT: {e}") from e
466
480
  # For other errors, we've already handled retries in the status check above
467
481
  gateway_circuit_breaker.record_failure()
468
- raise RuntimeError(f"Failed to get JWT: {e}") from e
482
+ raise RuntimeError(
483
+ _mint_failure_message(f"Failed to get JWT: {e}", shadow_hint)
484
+ ) from e
469
485
 
470
486
  # Should not reach here, but just in case
471
487
  gateway_circuit_breaker.record_failure()
@@ -28,6 +28,7 @@ from .platform import PlatformNamespace
28
28
  from .coder import CoderNamespace
29
29
  from .publisher import PublisherNamespace
30
30
  from .stash import StashNamespace, StashTablesNamespace
31
+ from .usage import UsageNamespace
31
32
  from .ai import (
32
33
  AiNamespace,
33
34
  RagSubNamespace,
@@ -65,6 +66,7 @@ __all__ = [
65
66
  "PublisherNamespace",
66
67
  "StashNamespace",
67
68
  "StashTablesNamespace",
69
+ "UsageNamespace",
68
70
  "AiNamespace",
69
71
  "RagSubNamespace",
70
72
  "ArtifactsSubNamespace",
@@ -969,9 +969,10 @@ class AiNamespace(GatewayMixin):
969
969
  tool_allowlist: Optional[List[str]] = None,
970
970
  guardrails: Optional[Dict[str, Any]] = None,
971
971
  output_schema: Optional[Dict[str, Any]] = None,
972
- model: str = "claude-sonnet-4-5",
972
+ model: Optional[str] = None,
973
973
  tool_endpoint: Optional[str] = None,
974
- timeout: float = 300.0
974
+ timeout: float = 300.0,
975
+ provider: str = "claude"
975
976
  ) -> Dict[str, Any]:
976
977
  """
977
978
  Execute agent with blocking wait.
@@ -987,9 +988,10 @@ class AiNamespace(GatewayMixin):
987
988
  tool_allowlist: Optional list of tool names to enable
988
989
  guardrails: Optional guardrails config (max_steps, max_tool_calls, etc.)
989
990
  output_schema: Optional JSON Schema for structured output
990
- model: Model to use (default: claude-sonnet-4-5)
991
+ model: Model to use (provider default when omitted)
991
992
  tool_endpoint: Optional custom tool endpoint URL
992
993
  timeout: Request timeout in seconds (default: 300s)
994
+ provider: Agent provider to use (default: claude)
993
995
 
994
996
  Returns:
995
997
  Dict with "conversation_id", "final_response", "machine_output",
@@ -1000,8 +1002,12 @@ class AiNamespace(GatewayMixin):
1000
1002
  "system_prompt": system_prompt,
1001
1003
  "user_prompt": user_prompt,
1002
1004
  "history_source": history_source,
1003
- "model": model
1005
+ "provider": provider
1004
1006
  }
1007
+ if model:
1008
+ body["model"] = model
1009
+ elif provider == "claude":
1010
+ body["model"] = "claude-sonnet-4-5"
1005
1011
  if inline_history:
1006
1012
  body["inline_history"] = inline_history
1007
1013
  if preloaded_context:
@@ -1034,10 +1040,11 @@ class AiNamespace(GatewayMixin):
1034
1040
  tool_allowlist: Optional[List[str]] = None,
1035
1041
  guardrails: Optional[Dict[str, Any]] = None,
1036
1042
  output_schema: Optional[Dict[str, Any]] = None,
1037
- model: str = "claude-sonnet-4-5",
1043
+ model: Optional[str] = None,
1038
1044
  tool_endpoint: Optional[str] = None,
1039
1045
  on_chunk: Optional[Callable[[Dict[str, Any]], None]] = None,
1040
- timeout: float = 300.0
1046
+ timeout: float = 300.0,
1047
+ provider: str = "claude"
1041
1048
  ) -> AsyncGenerator[Dict[str, Any], None]:
1042
1049
  """
1043
1050
  Execute agent with SSE streaming.
@@ -1053,10 +1060,11 @@ class AiNamespace(GatewayMixin):
1053
1060
  tool_allowlist: Optional list of tool names
1054
1061
  guardrails: Optional guardrails config
1055
1062
  output_schema: Optional JSON Schema for structured output
1056
- model: Model to use
1063
+ model: Model to use (provider default when omitted)
1057
1064
  tool_endpoint: Optional custom tool endpoint URL
1058
1065
  on_chunk: Optional callback for each chunk
1059
1066
  timeout: Request timeout in seconds
1067
+ provider: Agent provider to use (default: claude)
1060
1068
 
1061
1069
  Yields:
1062
1070
  Streaming chunks with "type" and content
@@ -1066,8 +1074,12 @@ class AiNamespace(GatewayMixin):
1066
1074
  "system_prompt": system_prompt,
1067
1075
  "user_prompt": user_prompt,
1068
1076
  "history_source": history_source,
1069
- "model": model
1077
+ "provider": provider
1070
1078
  }
1079
+ if model:
1080
+ body["model"] = model
1081
+ elif provider == "claude":
1082
+ body["model"] = "claude-sonnet-4-5"
1071
1083
  if inline_history:
1072
1084
  body["inline_history"] = inline_history
1073
1085
  if preloaded_context:
@@ -1104,9 +1116,10 @@ class AiNamespace(GatewayMixin):
1104
1116
  tool_allowlist: Optional[List[str]] = None,
1105
1117
  guardrails: Optional[Dict[str, Any]] = None,
1106
1118
  output_schema: Optional[Dict[str, Any]] = None,
1107
- model: str = "claude-sonnet-4-5",
1119
+ model: Optional[str] = None,
1108
1120
  tool_endpoint: Optional[str] = None,
1109
- webhook_url: Optional[str] = None
1121
+ webhook_url: Optional[str] = None,
1122
+ provider: str = "claude"
1110
1123
  ) -> Dict[str, Any]:
1111
1124
  """
1112
1125
  Fire-and-forget async execution.
@@ -1125,9 +1138,10 @@ class AiNamespace(GatewayMixin):
1125
1138
  tool_allowlist: Optional list of tool names
1126
1139
  guardrails: Optional guardrails config
1127
1140
  output_schema: Optional JSON Schema
1128
- model: Model to use
1141
+ model: Model to use (provider default when omitted)
1129
1142
  tool_endpoint: Optional custom tool endpoint URL
1130
1143
  webhook_url: Optional webhook for completion notification
1144
+ provider: Agent provider to use (default: claude)
1131
1145
 
1132
1146
  Returns:
1133
1147
  Dict with "result_key" for polling
@@ -1138,8 +1152,12 @@ class AiNamespace(GatewayMixin):
1138
1152
  "user_prompt": user_prompt,
1139
1153
  "result_key": result_key,
1140
1154
  "history_source": history_source,
1141
- "model": model
1155
+ "provider": provider
1142
1156
  }
1157
+ if model:
1158
+ body["model"] = model
1159
+ elif provider == "claude":
1160
+ body["model"] = "claude-sonnet-4-5"
1143
1161
  if inline_history:
1144
1162
  body["inline_history"] = inline_history
1145
1163
  if preloaded_context:
@@ -624,11 +624,22 @@ class ArtifactsNamespace:
624
624
  async def unwatch(self, *, watcher_name: str, **selector: Any) -> Dict[str, Any]:
625
625
  return await self.unwatch_v2(watcher_name=watcher_name, **selector)
626
626
 
627
- async def list_v2(self, limit: int = 100, include_tombstoned: bool = False, **selector: Any) -> Dict[str, Any]:
627
+ async def list_v2(
628
+ self,
629
+ limit: int = 100,
630
+ include_tombstoned: bool = False,
631
+ cursor: Optional[str] = None,
632
+ include_versions: bool = False,
633
+ **selector: Any,
634
+ ) -> Dict[str, Any]:
628
635
  body = self._selector_body(selector)
629
636
  body["limit"] = limit
630
637
  if include_tombstoned:
631
638
  body["include_tombstoned"] = True
639
+ if cursor:
640
+ body["cursor"] = cursor
641
+ if include_versions:
642
+ body["include_versions"] = True
632
643
  return await self._api("/api/artifact/v2/list", body=body)
633
644
 
634
645
  async def delete_v2(self, *, tombstone: bool = False, delete_bookmarks: bool = False, actor_context: Optional[Dict[str, str]] = None, **selector: Any) -> Dict[str, Any]:
@@ -0,0 +1,214 @@
1
+ """
2
+ Usage Namespace - Dominus consumption and provider-cost reporting.
3
+
4
+ Canonical SDK entry point for the dominus-usage service's authorized read
5
+ surface: summary, evidence, coverage, and explain. All four route through
6
+ the gateway (``use_gateway=True``), which maps ``/api/usage/*`` ->
7
+ ``/svc/usage/*`` upstream to dominus-usage.
8
+
9
+ This namespace is a thin pass-through: it never converts quantities/amounts
10
+ to float, never infers a default window or grouping (the service applies
11
+ last-7-days / meter+project+environment+logical_service when omitted), and
12
+ never adds a token of its own. An explicitly minted human bearer reaches
13
+ Usage unchanged via the existing ``Dominus(gateway_user_token=...)``
14
+ constructor behavior, exactly like ``dominus.authority.*``.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ from typing import Any, Dict, Optional, Sequence, TYPE_CHECKING, Union
19
+ from urllib.parse import urlencode
20
+
21
+ if TYPE_CHECKING:
22
+ from ..start import Dominus
23
+
24
+
25
+ def _query_string(params: Dict[str, Any]) -> str:
26
+ """Build a ``?...`` query string, repeating the key for list/tuple values."""
27
+ pairs: list[tuple[str, str]] = []
28
+ for key, value in params.items():
29
+ if value is None or value == "":
30
+ continue
31
+ if isinstance(value, (list, tuple)):
32
+ for item in value:
33
+ if item is None or item == "":
34
+ continue
35
+ pairs.append((key, str(item)))
36
+ else:
37
+ pairs.append((key, str(value)))
38
+ if not pairs:
39
+ return ""
40
+ return "?" + urlencode(pairs)
41
+
42
+
43
+ class UsageNamespace:
44
+ """
45
+ Usage service namespace: typed consumption and provider-cost reads.
46
+
47
+ Usage::
48
+
49
+ await dominus.usage.summary(since="2026-09-01T00:00:00Z", meter="ai.tokens.input")
50
+ await dominus.usage.evidence(project_id="proj_123", limit=100)
51
+ await dominus.usage.coverage()
52
+ await dominus.usage.explain(observation_id="obs_123")
53
+ """
54
+
55
+ def __init__(self, client: "Dominus"):
56
+ self._client = client
57
+
58
+ async def _get(self, endpoint: str, *, timeout: float = 30.0) -> Dict[str, Any]:
59
+ return await self._client._request(
60
+ endpoint=endpoint,
61
+ method="GET",
62
+ use_gateway=True,
63
+ timeout=timeout,
64
+ )
65
+
66
+ async def summary(
67
+ self,
68
+ *,
69
+ since: Optional[str] = None,
70
+ until: Optional[str] = None,
71
+ group_by: Optional[Union[str, Sequence[str]]] = None,
72
+ project_id: Optional[str] = None,
73
+ environment: Optional[str] = None,
74
+ logical_service: Optional[str] = None,
75
+ family_root_id: Optional[str] = None,
76
+ meter: Optional[str] = None,
77
+ provider: Optional[str] = None,
78
+ timeout: float = 30.0,
79
+ ) -> Dict[str, Any]:
80
+ """Unit-aware usage rows plus distinct provider-reported/estimated cost.
81
+
82
+ ``GET /api/usage/summary``. Omitting ``since``/``until`` uses the
83
+ service's last-7-days default; omitting ``group_by`` uses its default
84
+ grouping. Quantities/amounts are returned as decimal strings, never
85
+ converted to float.
86
+ """
87
+ qs = _query_string(
88
+ {
89
+ "since": since,
90
+ "until": until,
91
+ "group_by": group_by,
92
+ "project_id": project_id,
93
+ "environment": environment,
94
+ "logical_service": logical_service,
95
+ "family_root_id": family_root_id,
96
+ "meter": meter,
97
+ "provider": provider,
98
+ }
99
+ )
100
+ return await self._get(f"/api/usage/summary{qs}", timeout=timeout)
101
+
102
+ async def evidence(
103
+ self,
104
+ *,
105
+ since: Optional[str] = None,
106
+ until: Optional[str] = None,
107
+ project_id: Optional[str] = None,
108
+ environment: Optional[str] = None,
109
+ logical_service: Optional[str] = None,
110
+ family_root_id: Optional[str] = None,
111
+ meter: Optional[str] = None,
112
+ provider: Optional[str] = None,
113
+ limit: Optional[int] = None,
114
+ cursor: Optional[str] = None,
115
+ timeout: float = 30.0,
116
+ ) -> Dict[str, Any]:
117
+ """Cursor-paginated observation/provider-source evidence.
118
+
119
+ ``GET /api/usage/evidence``. ``limit`` defaults to 100 and is bounded
120
+ to 500 server-side; pass the response's ``next_cursor`` back in to
121
+ page forward.
122
+ """
123
+ qs = _query_string(
124
+ {
125
+ "since": since,
126
+ "until": until,
127
+ "project_id": project_id,
128
+ "environment": environment,
129
+ "logical_service": logical_service,
130
+ "family_root_id": family_root_id,
131
+ "meter": meter,
132
+ "provider": provider,
133
+ "limit": limit,
134
+ "cursor": cursor,
135
+ }
136
+ )
137
+ return await self._get(f"/api/usage/evidence{qs}", timeout=timeout)
138
+
139
+ async def coverage(
140
+ self,
141
+ *,
142
+ since: Optional[str] = None,
143
+ until: Optional[str] = None,
144
+ project_id: Optional[str] = None,
145
+ environment: Optional[str] = None,
146
+ logical_service: Optional[str] = None,
147
+ family_root_id: Optional[str] = None,
148
+ meter: Optional[str] = None,
149
+ provider: Optional[str] = None,
150
+ timeout: float = 30.0,
151
+ ) -> Dict[str, Any]:
152
+ """Registered meter/source coverage, freshness, and gap reasons.
153
+
154
+ ``GET /api/usage/coverage``. Never reports unsupported capture as
155
+ complete; ``gaps`` names each unavailable reason explicitly.
156
+ """
157
+ qs = _query_string(
158
+ {
159
+ "since": since,
160
+ "until": until,
161
+ "project_id": project_id,
162
+ "environment": environment,
163
+ "logical_service": logical_service,
164
+ "family_root_id": family_root_id,
165
+ "meter": meter,
166
+ "provider": provider,
167
+ }
168
+ )
169
+ return await self._get(f"/api/usage/coverage{qs}", timeout=timeout)
170
+
171
+ async def explain(
172
+ self,
173
+ *,
174
+ observation_id: Optional[str] = None,
175
+ operation_id: Optional[str] = None,
176
+ since: Optional[str] = None,
177
+ until: Optional[str] = None,
178
+ project_id: Optional[str] = None,
179
+ environment: Optional[str] = None,
180
+ logical_service: Optional[str] = None,
181
+ family_root_id: Optional[str] = None,
182
+ meter: Optional[str] = None,
183
+ timeout: float = 30.0,
184
+ ) -> Dict[str, Any]:
185
+ """Explanation linking permitted observations, attempts, and cost basis.
186
+
187
+ ``GET /api/usage/explain``. Exactly one of ``observation_id`` or
188
+ ``operation_id`` must be supplied; supplying both (or neither) is
189
+ rejected locally before the request is sent, matching the service's
190
+ own ambiguous-selector rejection. The remaining filters narrow which
191
+ observations can match an ``operation_id`` lookup; they are inert for
192
+ an ``observation_id`` lookup, which the service resolves unfiltered.
193
+ """
194
+ if bool(observation_id) == bool(operation_id):
195
+ raise ValueError(
196
+ "usage.explain requires exactly one of observation_id or operation_id"
197
+ )
198
+ qs = _query_string(
199
+ {
200
+ "observation_id": observation_id,
201
+ "operation_id": operation_id,
202
+ "since": since,
203
+ "until": until,
204
+ "project_id": project_id,
205
+ "environment": environment,
206
+ "logical_service": logical_service,
207
+ "family_root_id": family_root_id,
208
+ "meter": meter,
209
+ }
210
+ )
211
+ return await self._get(f"/api/usage/explain{qs}", timeout=timeout)
212
+
213
+
214
+ __all__ = ["UsageNamespace"]
@@ -196,6 +196,10 @@ class Dominus:
196
196
  from .namespaces.recipes import RecipesNamespace
197
197
  self.recipes = RecipesNamespace(self)
198
198
 
199
+ # Usage service (dominus-usage; consumption and provider-cost reports)
200
+ from .namespaces.usage import UsageNamespace
201
+ self.usage = UsageNamespace(self)
202
+
199
203
  # Cache for JWT public key
200
204
  self._public_key_cache = None
201
205
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dominus-sdk-python
3
- Version: 9.0.10
3
+ Version: 9.0.12
4
4
  Summary: Python SDK for the Dominus gateway-first platform
5
5
  Author-email: CareBridge Systems <dev@carebridge.io>
6
6
  License-Expression: LicenseRef-Proprietary
@@ -38,8 +38,8 @@ Async Python SDK for the Dominus gateway-first service plane.
38
38
  ## Agent Guide
39
39
 
40
40
  Start with `docs/agent-guide/INDEX.md`. The current snapshot is
41
- `docs/agent-guide/2026-09-10-1002-sdk-python/00-reading-order.md`; the latest
42
- cleanup audit is `docs/janitor/2026-09-10-1002-sdk-python-cleanup-audit.md`.
41
+ `docs/agent-guide/2026-09-14-1300-sdk-python/00-reading-order.md`; the latest
42
+ cleanup audit is `docs/janitor/2026-09-14-1300-sdk-python-cleanup-audit.md`.
43
43
 
44
44
  ## What This Repo Ships
45
45
 
@@ -48,7 +48,7 @@ cleanup audit is `docs/janitor/2026-09-10-1002-sdk-python-cleanup-audit.md`.
48
48
  - Gateway-scoped client mode for MCP and other user-JWT sessions
49
49
  - Transport compatibility for wrapped `{success,data}` responses and unwrapped Warden/control-plane success objects
50
50
  - Local helpers for JWT verification, trace propagation, retries, and console capture
51
- - Current package version: `9.0.9`
51
+ - Current package version: `9.0.12`
52
52
 
53
53
  ## Install
54
54
 
@@ -44,6 +44,7 @@ dominus/namespaces/secrets.py
44
44
  dominus/namespaces/secure.py
45
45
  dominus/namespaces/stash.py
46
46
  dominus/namespaces/sync.py
47
+ dominus/namespaces/usage.py
47
48
  dominus/namespaces/warden.py
48
49
  dominus/namespaces/workflow.py
49
50
  dominus/services/__init__.py
@@ -52,6 +53,8 @@ dominus_sdk_python.egg-info/SOURCES.txt
52
53
  dominus_sdk_python.egg-info/dependency_links.txt
53
54
  dominus_sdk_python.egg-info/requires.txt
54
55
  dominus_sdk_python.egg-info/top_level.txt
56
+ tests/test_ai_namespace.py
57
+ tests/test_artifacts_namespace.py
55
58
  tests/test_auth.py
56
59
  tests/test_authority_public_vocabulary.py
57
60
  tests/test_browser_namespace.py
@@ -74,7 +77,9 @@ tests/test_recipes_stash_routing.py
74
77
  tests/test_secrets_presence_contract.py
75
78
  tests/test_stash_artifact_facade.py
76
79
  tests/test_stash_tables_namespace.py
80
+ tests/test_token_precedence.py
77
81
  tests/test_transport_compat.py
82
+ tests/test_usage_namespace.py
78
83
  tests/test_version_consistency.py
79
84
  tests/test_workflow_lifecycle.py
80
85
  tests/test_workflow_refs.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dominus-sdk-python"
7
- version = "9.0.10"
7
+ version = "9.0.12"
8
8
  description = "Python SDK for the Dominus gateway-first platform"
9
9
  readme = "README.md"
10
10
  license = "LicenseRef-Proprietary"