dominus-sdk-python 9.0.16__tar.gz → 9.1.0__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 (92) hide show
  1. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/PKG-INFO +1 -1
  2. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/__init__.py +42 -1
  3. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/helpers/core.py +260 -0
  4. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/start.py +143 -16
  5. dominus_sdk_python-9.1.0/dominus/target.py +342 -0
  6. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus_sdk_python.egg-info/PKG-INFO +1 -1
  7. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus_sdk_python.egg-info/SOURCES.txt +2 -0
  8. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/pyproject.toml +16 -1
  9. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/tests/test_authority_public_vocabulary.py +73 -0
  10. dominus_sdk_python-9.1.0/tests/test_public_exports.py +137 -0
  11. dominus_sdk_python-9.1.0/tests/test_target_contract.py +556 -0
  12. dominus_sdk_python-9.1.0/tests/test_transport_compat.py +961 -0
  13. dominus_sdk_python-9.0.16/tests/test_public_exports.py +0 -51
  14. dominus_sdk_python-9.0.16/tests/test_transport_compat.py +0 -395
  15. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/README.md +0 -0
  16. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/config/__init__.py +0 -0
  17. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/config/endpoints.py +0 -0
  18. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/contract/__init__.py +0 -0
  19. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/contract/envelope.py +0 -0
  20. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/contract/projections.py +0 -0
  21. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/errors.py +0 -0
  22. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/helpers/__init__.py +0 -0
  23. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/helpers/auth.py +0 -0
  24. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/helpers/cache.py +0 -0
  25. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/helpers/console_capture.py +0 -0
  26. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/helpers/crypto.py +0 -0
  27. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/helpers/sse.py +0 -0
  28. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/helpers/trace.py +0 -0
  29. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/namespaces/__init__.py +0 -0
  30. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/namespaces/admin.py +0 -0
  31. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/namespaces/ai.py +0 -0
  32. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/namespaces/artifacts.py +0 -0
  33. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/namespaces/auth.py +0 -0
  34. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/namespaces/authority.py +0 -0
  35. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/namespaces/browser.py +0 -0
  36. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/namespaces/coder.py +0 -0
  37. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/namespaces/courier.py +0 -0
  38. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/namespaces/db.py +0 -0
  39. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/namespaces/ddl.py +0 -0
  40. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/namespaces/deployer.py +0 -0
  41. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/namespaces/fastapi.py +0 -0
  42. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/namespaces/files.py +0 -0
  43. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/namespaces/health.py +0 -0
  44. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/namespaces/jobs.py +0 -0
  45. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/namespaces/logs.py +0 -0
  46. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/namespaces/platform.py +0 -0
  47. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/namespaces/portal.py +0 -0
  48. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/namespaces/processor.py +0 -0
  49. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/namespaces/publisher.py +0 -0
  50. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/namespaces/recipes.py +0 -0
  51. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/namespaces/redis.py +0 -0
  52. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/namespaces/secrets.py +0 -0
  53. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/namespaces/secure.py +0 -0
  54. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/namespaces/stash.py +0 -0
  55. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/namespaces/sync.py +0 -0
  56. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/namespaces/usage.py +0 -0
  57. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/namespaces/warden.py +0 -0
  58. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/namespaces/workflow.py +0 -0
  59. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/py.typed +0 -0
  60. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus/services/__init__.py +0 -0
  61. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus_sdk_python.egg-info/dependency_links.txt +0 -0
  62. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus_sdk_python.egg-info/requires.txt +0 -0
  63. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/dominus_sdk_python.egg-info/top_level.txt +0 -0
  64. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/setup.cfg +0 -0
  65. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/tests/test_ai_namespace.py +0 -0
  66. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/tests/test_artifacts_namespace.py +0 -0
  67. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/tests/test_auth.py +0 -0
  68. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/tests/test_browser_namespace.py +0 -0
  69. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/tests/test_cache_contracts.py +0 -0
  70. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/tests/test_contract.py +0 -0
  71. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/tests/test_control_plane_namespaces.py +0 -0
  72. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/tests/test_error_recovery_action.py +0 -0
  73. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/tests/test_errors.py +0 -0
  74. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/tests/test_errors_aliases.py +0 -0
  75. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/tests/test_flat_commands.py +0 -0
  76. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/tests/test_health.py +0 -0
  77. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/tests/test_logs.py +0 -0
  78. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/tests/test_namespace_barrel_exports.py +0 -0
  79. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/tests/test_platform_coder_namespaces.py +0 -0
  80. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/tests/test_provisioning_parity.py +0 -0
  81. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/tests/test_publisher_namespace.py +0 -0
  82. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/tests/test_readme_quickstart_catalog.py +0 -0
  83. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/tests/test_recipes_namespace.py +0 -0
  84. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/tests/test_recipes_stash_routing.py +0 -0
  85. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/tests/test_secrets_presence_contract.py +0 -0
  86. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/tests/test_stash_artifact_facade.py +0 -0
  87. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/tests/test_stash_tables_namespace.py +0 -0
  88. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/tests/test_token_precedence.py +0 -0
  89. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/tests/test_usage_namespace.py +0 -0
  90. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/tests/test_version_consistency.py +0 -0
  91. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/tests/test_workflow_lifecycle.py +0 -0
  92. {dominus_sdk_python-9.0.16 → dominus_sdk_python-9.1.0}/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.16
3
+ Version: 9.1.0
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
@@ -148,6 +148,29 @@ from .helpers.core import (
148
148
  mint_selected_scope_jwt,
149
149
  )
150
150
 
151
+ # Export the exact-target surface. The shared kernel codec stays a lazy,
152
+ # out-of-band dependency (documented git-tag lane); importing this module never
153
+ # requires it, so the base install stays targetless.
154
+ from .target import (
155
+ EXACT_EXCHANGE_VERSION,
156
+ EXACT_TARGET_DEPENDENCY_LANE,
157
+ EXACT_TARGET_VERSION,
158
+ SERVICE_CORE_TAG,
159
+ TARGET_HEADER,
160
+ TOPOLOGY_TARGET_VERSION,
161
+ ExactTargetDependencyError,
162
+ ExactTargetError,
163
+ ExactTicketRecord,
164
+ assert_ticket_proves_target,
165
+ canonical_target_bytes,
166
+ selected_scope_target,
167
+ target_fingerprint,
168
+ target_header_value,
169
+ topology_target_hash,
170
+ validate_target,
171
+ validate_target_exchange_response,
172
+ )
173
+
151
174
  # Export trace utilities for distributed tracing
152
175
  from .helpers.trace import (
153
176
  generate_trace_id,
@@ -191,7 +214,7 @@ from .errors import (
191
214
  TimeoutError as DominusTimeoutError,
192
215
  )
193
216
 
194
- __version__ = "9.0.16"
217
+ __version__ = "9.1.0"
195
218
  __all__ = [
196
219
  # Main SDK instance
197
220
  "dominus",
@@ -267,6 +290,24 @@ __all__ = [
267
290
  "verify_jwt_locally",
268
291
  "is_jwt_valid",
269
292
  "mint_selected_scope_jwt",
293
+ # Exact-target surface (lazy shared kernel codec)
294
+ "EXACT_TARGET_VERSION",
295
+ "TOPOLOGY_TARGET_VERSION",
296
+ "EXACT_EXCHANGE_VERSION",
297
+ "TARGET_HEADER",
298
+ "SERVICE_CORE_TAG",
299
+ "EXACT_TARGET_DEPENDENCY_LANE",
300
+ "ExactTargetError",
301
+ "ExactTargetDependencyError",
302
+ "ExactTicketRecord",
303
+ "validate_target",
304
+ "selected_scope_target",
305
+ "validate_target_exchange_response",
306
+ "assert_ticket_proves_target",
307
+ "canonical_target_bytes",
308
+ "target_header_value",
309
+ "target_fingerprint",
310
+ "topology_target_hash",
270
311
  # Trace utilities
271
312
  "generate_trace_id",
272
313
  "get_current_trace_id",
@@ -5,6 +5,8 @@ Includes circuit breaker pattern to prevent runaway retries from exhausting
5
5
  Cloud Run CPU quota during service outages or circular dependency issues.
6
6
  """
7
7
  import asyncio
8
+ import dataclasses
9
+ import hashlib
8
10
  import httpx
9
11
  import base64
10
12
  import json
@@ -21,6 +23,21 @@ MAX_RETRIES = 3
21
23
  # When multiple concurrent coroutines need a new JWT, only one will mint
22
24
  _jwt_refresh_lock = asyncio.Lock()
23
25
 
26
+ # Exact-target ticket cache. One entry is one immutable record — a ticket is
27
+ # never paired with target bytes derived from a different request. The key is
28
+ # the credential context plus the FULL target fingerprint, so any refinement
29
+ # (branch, tenant, provisioning target, group, environment, project) or a
30
+ # different credential gets its own entry.
31
+ EXACT_TICKET_CACHE_PREFIX = "jwt:exact:"
32
+ EXACT_TICKET_REFRESH_BUFFER_SECONDS = 60
33
+ # Upper bound on the shared encrypted cache entry; the ticket's own `exp`
34
+ # (minus the refresh buffer) wins when it is sooner.
35
+ EXACT_TICKET_CACHE_TTL = 3300 # 55 minutes
36
+ # One mutex per cache entry: concurrent callers for the same credential+target
37
+ # share a single exchange, while unrelated credentials/targets never queue
38
+ # behind each other's network round-trip.
39
+ _exact_ticket_locks: dict[str, asyncio.Lock] = {}
40
+
24
41
  # Type alias
25
42
  DominusResponse = dict[str, Any]
26
43
 
@@ -642,6 +659,249 @@ async def mint_selected_scope_jwt(
642
659
  raise RuntimeError(f"Selected-scope JWT mint failed: {e}") from e
643
660
 
644
661
 
662
+ def _exact_credential_key(psk_token: str) -> str:
663
+ """Credential discriminator for the exact-target cache. The PSK never enters a key."""
664
+ digest = hashlib.sha256(psk_token.encode("utf-8")).digest()
665
+ return base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")
666
+
667
+
668
+ def _exact_refinement_key(
669
+ ttl_seconds: Optional[int],
670
+ run_id: Optional[str],
671
+ scopes: Optional[list],
672
+ ) -> str:
673
+ """Digest of the mint refinements — they land in the ticket's own claims."""
674
+ if ttl_seconds is None and run_id is None and scopes is None:
675
+ return ""
676
+ payload = {
677
+ "ttl_seconds": ttl_seconds,
678
+ "run_id": run_id,
679
+ "scopes": list(scopes) if scopes is not None else None,
680
+ }
681
+ digest = hashlib.sha256(
682
+ json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
683
+ ).digest()
684
+ return ":" + base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")
685
+
686
+
687
+ def _exact_ticket_cache_key(
688
+ psk_token: str,
689
+ fingerprint: str,
690
+ refinements: str = "",
691
+ ) -> str:
692
+ return (
693
+ f"{EXACT_TICKET_CACHE_PREFIX}{_exact_credential_key(psk_token)}"
694
+ f":{fingerprint}{refinements}"
695
+ )
696
+
697
+
698
+ def _exact_ticket_lock_for(cache_key: str) -> asyncio.Lock:
699
+ lock = _exact_ticket_locks.get(cache_key)
700
+ if lock is None:
701
+ lock = asyncio.Lock()
702
+ _exact_ticket_locks[cache_key] = lock
703
+ return lock
704
+
705
+
706
+ def _exact_ticket_expiry(ticket: str) -> float:
707
+ """Local expiry from the ticket's own ``exp``, refreshed before it lapses."""
708
+ try:
709
+ payload = _decode_jwt_payload(ticket)
710
+ exp = payload.get("exp")
711
+ if isinstance(exp, (int, float)) and not isinstance(exp, bool):
712
+ return float(exp) - EXACT_TICKET_REFRESH_BUFFER_SECONDS
713
+ except Exception:
714
+ pass
715
+ return time.time() + EXACT_TICKET_CACHE_TTL
716
+
717
+
718
+ def _cached_exact_ticket(cache_key: str):
719
+ """Read one cached record, re-validating its bytes; drop anything stale/tampered."""
720
+ from ..target import ExactTicketRecord
721
+
722
+ entry = dominus_cache.get(cache_key)
723
+ if not isinstance(entry, dict):
724
+ return None
725
+ try:
726
+ expires_at = float(entry.get("expires_at") or 0)
727
+ except (TypeError, ValueError):
728
+ expires_at = 0.0
729
+ if expires_at <= time.time():
730
+ dominus_cache.delete(cache_key)
731
+ return None
732
+ try:
733
+ return ExactTicketRecord.from_mapping(entry)
734
+ except Exception:
735
+ dominus_cache.delete(cache_key)
736
+ return None
737
+
738
+
739
+ def _target_payload(target: Any) -> dict:
740
+ if dataclasses.is_dataclass(target) and not isinstance(target, type):
741
+ return dataclasses.asdict(target)
742
+ return dict(target)
743
+
744
+
745
+ async def _exchange_exact_target(
746
+ psk_token: str,
747
+ target: Any,
748
+ *,
749
+ ttl_seconds: Optional[int] = None,
750
+ run_id: Optional[str] = None,
751
+ scopes: Optional[list] = None,
752
+ timeout: float = 30.0,
753
+ ):
754
+ """Perform one ``target-exchange.v1`` mint and validate the whole response."""
755
+ from ..config.endpoints import get_gateway_url, get_proxy_config
756
+ from ..errors import (
757
+ DominusError,
758
+ ConnectionError as DominusConnectionError,
759
+ TimeoutError as DominusTimeoutError,
760
+ raise_for_status,
761
+ )
762
+ from ..target import EXACT_EXCHANGE_VERSION, validate_target_exchange_response
763
+ from .trace import get_current_trace_id, get_current_span_id
764
+
765
+ headers = {
766
+ "Authorization": f"Bearer {psk_token}",
767
+ "Content-Type": "text/plain",
768
+ "X-Trace-Id": get_current_trace_id(),
769
+ "X-Parent-Span-Id": get_current_span_id(),
770
+ }
771
+
772
+ body: dict = {"version": EXACT_EXCHANGE_VERSION, "target": _target_payload(target)}
773
+ if ttl_seconds is not None:
774
+ body["ttl_seconds"] = ttl_seconds
775
+ if run_id is not None:
776
+ body["run_id"] = run_id
777
+ if scopes is not None:
778
+ body["scopes"] = list(scopes)
779
+
780
+ try:
781
+ async with httpx.AsyncClient(
782
+ base_url=get_gateway_url(),
783
+ headers=headers,
784
+ timeout=timeout,
785
+ proxy=get_proxy_config(),
786
+ ) as client:
787
+ response = await client.post("/jwt/mint", content=_b64_encode(body))
788
+ except httpx.TimeoutException as exc:
789
+ raise DominusTimeoutError(
790
+ "Exact-target exchange timed out", endpoint="/jwt/mint"
791
+ ) from exc
792
+ except httpx.NetworkError as exc:
793
+ raise DominusConnectionError(
794
+ f"Exact-target exchange failed: {exc}", endpoint="/jwt/mint"
795
+ ) from exc
796
+
797
+ if response.is_error:
798
+ message = f"Exact-target exchange failed: HTTP {response.status_code}"
799
+ details: dict = {}
800
+ try:
801
+ error_payload = _decode_json_or_b64_json(response.text)
802
+ except Exception:
803
+ error_payload = {}
804
+ if isinstance(error_payload, dict):
805
+ message = (
806
+ error_payload.get("message")
807
+ or error_payload.get("error")
808
+ or error_payload.get("detail")
809
+ or message
810
+ )
811
+ nested = error_payload.get("error")
812
+ error_details = error_payload.get("details")
813
+ details = error_details if isinstance(error_details, dict) else {}
814
+ for src in (error_payload, nested if isinstance(nested, dict) else {}):
815
+ for key in ("kind", "code", "category", "retryable", "trace_id", "request_id"):
816
+ if key in src and key not in details:
817
+ details[key] = src[key]
818
+ raise_for_status(response.status_code, str(message), details, "/jwt/mint")
819
+
820
+ result = _decode_json_or_b64_json(response.text)
821
+ if not isinstance(result, dict):
822
+ raise DominusError(
823
+ "Exact-target exchange returned a non-object response",
824
+ endpoint="/jwt/mint",
825
+ )
826
+ if result.get("success") is False:
827
+ error_details = result.get("details")
828
+ raise DominusError(
829
+ str(result.get("error") or result.get("message") or "Unknown exact-target exchange error"),
830
+ details=error_details if isinstance(error_details, dict) else {},
831
+ endpoint="/jwt/mint",
832
+ )
833
+
834
+ data = result.get("data", result)
835
+ return validate_target_exchange_response(data, target)
836
+
837
+
838
+ async def mint_exact_target_ticket(
839
+ psk_token: str,
840
+ target: Any,
841
+ *,
842
+ ttl_seconds: Optional[int] = None,
843
+ run_id: Optional[str] = None,
844
+ scopes: Optional[list] = None,
845
+ force_refresh: bool = False,
846
+ timeout: float = 30.0,
847
+ ):
848
+ """Exchange an exact target for one atomic ticket record, cached.
849
+
850
+ The ticket, canonical target, fingerprint, and header are one immutable
851
+ unit keyed by credential context + full target fingerprint: identical
852
+ targets under the same credential reuse the entry, and any target-field or
853
+ credential change separates it.
854
+ """
855
+ from ..target import target_fingerprint, validate_target
856
+
857
+ canonical = validate_target(target)
858
+ fingerprint = target_fingerprint(canonical)
859
+ refinements = _exact_refinement_key(ttl_seconds, run_id, scopes)
860
+ cache_key = _exact_ticket_cache_key(psk_token, fingerprint, refinements)
861
+
862
+ if refinements:
863
+ # Refinement identities are caller-controlled (run ids, TTLs) and carry
864
+ # per-run claims, so they are exchanged fresh instead of cached — the
865
+ # cache (and its lock map) stays bounded by credential × target. A
866
+ # ticket minted for one run can therefore never be served to another.
867
+ return await _exchange_exact_target(
868
+ psk_token,
869
+ canonical,
870
+ ttl_seconds=ttl_seconds,
871
+ run_id=run_id,
872
+ scopes=scopes,
873
+ timeout=timeout,
874
+ )
875
+
876
+ if force_refresh:
877
+ dominus_cache.delete(cache_key)
878
+ else:
879
+ cached = _cached_exact_ticket(cache_key)
880
+ if cached is not None:
881
+ return cached
882
+
883
+ async with _exact_ticket_lock_for(cache_key):
884
+ if not force_refresh:
885
+ cached = _cached_exact_ticket(cache_key)
886
+ if cached is not None:
887
+ return cached
888
+
889
+ record = await _exchange_exact_target(
890
+ psk_token,
891
+ canonical,
892
+ ttl_seconds=ttl_seconds,
893
+ run_id=run_id,
894
+ scopes=scopes,
895
+ timeout=timeout,
896
+ )
897
+ expires_at = _exact_ticket_expiry(record.ticket)
898
+ ttl = int(max(1, min(EXACT_TICKET_CACHE_TTL, expires_at - time.time())))
899
+ dominus_cache.set(
900
+ cache_key, {**record.to_mapping(), "expires_at": expires_at}, ttl=ttl
901
+ )
902
+ return record
903
+
904
+
645
905
  def verify_token_format(token: str) -> bool:
646
906
  """
647
907
  Verify token format is valid (basic check, not server verification).
@@ -93,12 +93,18 @@ class Dominus:
93
93
  gateway_org_id: Optional[str] = None,
94
94
  gateway_app_slug: Optional[str] = None,
95
95
  gateway_env: Optional[str] = None,
96
+ exact_target: Optional[Any] = None,
96
97
  ):
97
98
  """Initialize Dominus with flat API and minimal namespaces."""
98
99
  self._gateway_user_token = gateway_user_token
99
100
  self._gateway_org_id = gateway_org_id
100
101
  self._gateway_app_slug = gateway_app_slug
101
102
  self._gateway_env = gateway_env
103
+ # Default exact target for every project-scoped call; a per-call
104
+ # ``exact_target`` overrides it. None keeps the targetless legacy call.
105
+ self._default_exact_target: Optional[Any] = None
106
+ if exact_target is not None:
107
+ self.set_default_exact_target(exact_target)
102
108
 
103
109
  # Create internal execute function that handles validation
104
110
  async def _execute_command(command: str, **kwargs) -> Any:
@@ -238,6 +244,96 @@ class Dominus:
238
244
  verify_token_format(_TOKEN)
239
245
  _VALIDATED = True
240
246
 
247
+ # ========================================
248
+ # EXACT-TARGET TRANSPORT
249
+ # ========================================
250
+
251
+ def set_default_exact_target(self, exact_target: Optional[Any]) -> None:
252
+ """Set (or clear) the default exact target. ``None`` = targetless legacy."""
253
+ if exact_target is None:
254
+ self._default_exact_target = None
255
+ return
256
+ from .target import validate_target
257
+
258
+ self._default_exact_target = validate_target(exact_target)
259
+
260
+ async def mint_exact_target_ticket(
261
+ self,
262
+ target: Any,
263
+ *,
264
+ ttl_seconds: Optional[int] = None,
265
+ run_id: Optional[str] = None,
266
+ scopes: Optional[List[str]] = None,
267
+ force_refresh: bool = False,
268
+ ):
269
+ """Exchange one exact target for an atomic ticket record (cached)."""
270
+ await self._ensure_ready()
271
+ from .helpers.core import mint_exact_target_ticket as _mint
272
+
273
+ return await _mint(
274
+ _TOKEN,
275
+ target,
276
+ ttl_seconds=ttl_seconds,
277
+ run_id=run_id,
278
+ scopes=scopes,
279
+ force_refresh=force_refresh,
280
+ )
281
+
282
+ async def _resolve_exact_transport(
283
+ self,
284
+ exact_target: Optional[Any],
285
+ user_token: Optional[str] = None,
286
+ ) -> Optional[Any]:
287
+ """Resolve the atomic ``(ticket, target_header)`` pair, or None.
288
+
289
+ A caller-supplied ``user_token`` is already the ticket, so nothing is
290
+ exchanged — but the target must be named on this call (a client default
291
+ belongs to the service identity being exchanged, never to a caller's
292
+ bearer) and the bearer must prove it.
293
+ """
294
+ from .target import (
295
+ assert_ticket_proves_target,
296
+ target_fingerprint,
297
+ target_header_value,
298
+ validate_target,
299
+ )
300
+
301
+ if user_token:
302
+ if exact_target is None:
303
+ return None
304
+ canonical = validate_target(exact_target)
305
+ assert_ticket_proves_target(
306
+ user_token, target_fingerprint(canonical)
307
+ )
308
+ return user_token, target_header_value(canonical)
309
+
310
+ target = (
311
+ exact_target if exact_target is not None else self._default_exact_target
312
+ )
313
+ if target is None:
314
+ return None
315
+
316
+ # The exchange authenticates with the project PSK, so the exact path
317
+ # enforces the same token presence/format check as the targetless path.
318
+ await self._ensure_ready()
319
+ from .helpers.core import mint_exact_target_ticket
320
+
321
+ record = await mint_exact_target_ticket(_TOKEN, target)
322
+ return record.ticket, record.target_header
323
+
324
+ @staticmethod
325
+ def _apply_exact_pair(
326
+ headers: Dict[str, str], exact: Optional[Any]
327
+ ) -> None:
328
+ """Write the atomic pair last so no caller header can split it."""
329
+ if not exact:
330
+ return
331
+ from .target import TARGET_HEADER
332
+
333
+ ticket, target_header = exact
334
+ headers["Authorization"] = f"Bearer {ticket}"
335
+ headers[TARGET_HEADER] = target_header
336
+
241
337
  @staticmethod
242
338
  def _normalize_flat_schema(schema: Optional[str]) -> str:
243
339
  """Map legacy flat schema names onto the live gateway contract."""
@@ -351,6 +447,7 @@ class Dominus:
351
447
  timeout: float = 30.0,
352
448
  actor: Optional[Dict[str, str]] = None,
353
449
  extra_headers: Optional[Dict[str, str]] = None,
450
+ exact_target: Optional[Any] = None,
354
451
  ) -> Dict[str, Any]:
355
452
  """
356
453
  Make an HTTP request to the Dominus service plane.
@@ -380,7 +477,11 @@ class Dominus:
380
477
 
381
478
  # Determine which JWT to use (per-call user_token wins over MCP/session client defaults)
382
479
  effective_user_token = user_token or getattr(self, "_gateway_user_token", None)
383
- if effective_user_token:
480
+ exact = await self._resolve_exact_transport(exact_target, effective_user_token)
481
+ if exact:
482
+ # The exchanged ticket IS the bearer; the header travels with it.
483
+ jwt = exact[0]
484
+ elif effective_user_token:
384
485
  # Use the provided user JWT directly; downstream services validate it.
385
486
  jwt = effective_user_token
386
487
  else:
@@ -424,6 +525,8 @@ class Dominus:
424
525
  if effective_actor and effective_actor.get("type") and effective_actor.get("id"):
425
526
  headers["X-Actor-Type"] = str(effective_actor["type"])
426
527
  headers["X-Actor-Id"] = str(effective_actor["id"])
528
+ # Atomic exact pair, written last: no caller header can split ticket/bytes.
529
+ self._apply_exact_pair(headers, exact)
427
530
 
428
531
  # Prepare body
429
532
  body_b64 = _b64_encode(body or {})
@@ -527,15 +630,20 @@ class Dominus:
527
630
  body: Optional[Dict[str, Any]] = None,
528
631
  headers: Optional[Dict[str, str]] = None,
529
632
  timeout: float = 30.0,
633
+ exact_target: Optional[Any] = None,
530
634
  ) -> Any:
531
635
  """Gateway `/svc/*` fetch with mixed JSON/base64 wire handling.
532
636
 
533
637
  Mirrors the Node SDK `DominusClient.gatewayFetch()` contract so Python
534
638
  callers can use the same remaining operator control-plane surfaces.
535
639
  """
536
- await self._ensure_ready()
537
- from .helpers.core import _ensure_valid_jwt
538
- jwt = await _ensure_valid_jwt(_TOKEN, _BASE_URL)
640
+ exact = await self._resolve_exact_transport(exact_target)
641
+ if exact:
642
+ jwt = exact[0]
643
+ else:
644
+ await self._ensure_ready()
645
+ from .helpers.core import _ensure_valid_jwt
646
+ jwt = await _ensure_valid_jwt(_TOKEN, _BASE_URL)
539
647
 
540
648
  path = path.strip()
541
649
  if not path.startswith("/"):
@@ -568,6 +676,7 @@ class Dominus:
568
676
  }
569
677
  if headers:
570
678
  req_headers.update(headers)
679
+ self._apply_exact_pair(req_headers, exact)
571
680
 
572
681
  request_content = None
573
682
  request_json = None
@@ -655,7 +764,8 @@ class Dominus:
655
764
  body: Optional[Dict[str, Any]] = None,
656
765
  on_chunk: Optional[Any] = None,
657
766
  timeout: float = 300.0,
658
- use_gateway: bool = True
767
+ use_gateway: bool = True,
768
+ exact_target: Optional[Any] = None,
659
769
  ):
660
770
  """
661
771
  Make an SSE streaming request to the Dominus service plane.
@@ -679,9 +789,13 @@ class Dominus:
679
789
  endpoint = "/svc/" + endpoint[5:]
680
790
 
681
791
  # Validate and get JWT
682
- await self._ensure_ready()
683
- from .helpers.core import _ensure_valid_jwt
684
- jwt = await _ensure_valid_jwt(_TOKEN, _BASE_URL)
792
+ exact = await self._resolve_exact_transport(exact_target)
793
+ if exact:
794
+ jwt = exact[0]
795
+ else:
796
+ await self._ensure_ready()
797
+ from .helpers.core import _ensure_valid_jwt
798
+ jwt = await _ensure_valid_jwt(_TOKEN, _BASE_URL)
685
799
 
686
800
  from .helpers.trace import get_current_trace_id, get_current_span_id
687
801
  headers = {
@@ -691,6 +805,7 @@ class Dominus:
691
805
  "X-Trace-Id": get_current_trace_id(),
692
806
  "X-Parent-Span-Id": get_current_span_id(),
693
807
  }
808
+ self._apply_exact_pair(headers, exact)
694
809
 
695
810
  body_b64 = _b64_encode(body or {})
696
811
 
@@ -711,7 +826,8 @@ class Dominus:
711
826
  content_type: str = "application/octet-stream",
712
827
  additional_fields: Optional[Dict[str, str]] = None,
713
828
  timeout: float = 60.0,
714
- use_gateway: bool = True
829
+ use_gateway: bool = True,
830
+ exact_target: Optional[Any] = None,
715
831
  ) -> Dict[str, Any]:
716
832
  """
717
833
  Upload binary file via multipart/form-data (no base64 encoding).
@@ -734,9 +850,13 @@ class Dominus:
734
850
  if use_gateway and endpoint.startswith("/api/"):
735
851
  endpoint = "/svc/" + endpoint[5:]
736
852
 
737
- await self._ensure_ready()
738
- from .helpers.core import _ensure_valid_jwt
739
- jwt = await _ensure_valid_jwt(_TOKEN, _BASE_URL)
853
+ exact = await self._resolve_exact_transport(exact_target)
854
+ if exact:
855
+ jwt = exact[0]
856
+ else:
857
+ await self._ensure_ready()
858
+ from .helpers.core import _ensure_valid_jwt
859
+ jwt = await _ensure_valid_jwt(_TOKEN, _BASE_URL)
740
860
 
741
861
  from .helpers.trace import get_current_trace_id, get_current_span_id
742
862
  headers = {
@@ -744,6 +864,7 @@ class Dominus:
744
864
  "X-Trace-Id": get_current_trace_id(),
745
865
  "X-Parent-Span-Id": get_current_span_id(),
746
866
  }
867
+ self._apply_exact_pair(headers, exact)
747
868
 
748
869
  # Build multipart form data
749
870
  files = {"file": (filename, file_bytes, content_type)}
@@ -770,7 +891,8 @@ class Dominus:
770
891
  endpoint: str,
771
892
  body: Optional[Dict[str, Any]] = None,
772
893
  timeout: float = 60.0,
773
- use_gateway: bool = True
894
+ use_gateway: bool = True,
895
+ exact_target: Optional[Any] = None,
774
896
  ) -> bytes:
775
897
  """
776
898
  Download binary content (no base64 encoding in response).
@@ -793,9 +915,13 @@ class Dominus:
793
915
  if use_gateway and endpoint.startswith("/api/"):
794
916
  endpoint = "/svc/" + endpoint[5:]
795
917
 
796
- await self._ensure_ready()
797
- from .helpers.core import _ensure_valid_jwt
798
- jwt = await _ensure_valid_jwt(_TOKEN, _BASE_URL)
918
+ exact = await self._resolve_exact_transport(exact_target)
919
+ if exact:
920
+ jwt = exact[0]
921
+ else:
922
+ await self._ensure_ready()
923
+ from .helpers.core import _ensure_valid_jwt
924
+ jwt = await _ensure_valid_jwt(_TOKEN, _BASE_URL)
799
925
 
800
926
  from .helpers.trace import get_current_trace_id, get_current_span_id
801
927
  headers = {
@@ -805,6 +931,7 @@ class Dominus:
805
931
  "X-Trace-Id": get_current_trace_id(),
806
932
  "X-Parent-Span-Id": get_current_span_id(),
807
933
  }
934
+ self._apply_exact_pair(headers, exact)
808
935
 
809
936
  body_b64 = _b64_encode(body or {})
810
937