dominus-sdk-python 6.1.0__tar.gz → 6.1.2__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 (68) hide show
  1. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/PKG-INFO +18 -2
  2. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/README.md +17 -1
  3. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus/__init__.py +3 -1
  4. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus/namespaces/coder.py +56 -5
  5. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus/namespaces/platform.py +27 -2
  6. dominus_sdk_python-6.1.2/dominus/namespaces/publisher.py +300 -0
  7. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus/start.py +3 -0
  8. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus_sdk_python.egg-info/PKG-INFO +18 -2
  9. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus_sdk_python.egg-info/SOURCES.txt +2 -0
  10. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/pyproject.toml +1 -1
  11. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/tests/test_platform_coder_namespaces.py +25 -1
  12. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/tests/test_public_exports.py +3 -0
  13. dominus_sdk_python-6.1.2/tests/test_publisher_namespace.py +82 -0
  14. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus/config/__init__.py +0 -0
  15. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus/config/endpoints.py +0 -0
  16. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus/errors.py +0 -0
  17. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus/helpers/__init__.py +0 -0
  18. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus/helpers/auth.py +0 -0
  19. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus/helpers/cache.py +0 -0
  20. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus/helpers/console_capture.py +0 -0
  21. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus/helpers/core.py +0 -0
  22. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus/helpers/crypto.py +0 -0
  23. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus/helpers/sse.py +0 -0
  24. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus/helpers/trace.py +0 -0
  25. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus/namespaces/__init__.py +0 -0
  26. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus/namespaces/admin.py +0 -0
  27. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus/namespaces/ai.py +0 -0
  28. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus/namespaces/artifacts.py +0 -0
  29. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus/namespaces/auth.py +0 -0
  30. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus/namespaces/authority.py +0 -0
  31. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus/namespaces/browser.py +0 -0
  32. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus/namespaces/courier.py +0 -0
  33. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus/namespaces/db.py +0 -0
  34. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus/namespaces/ddl.py +0 -0
  35. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus/namespaces/deployer.py +0 -0
  36. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus/namespaces/fastapi.py +0 -0
  37. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus/namespaces/files.py +0 -0
  38. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus/namespaces/health.py +0 -0
  39. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus/namespaces/jobs.py +0 -0
  40. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus/namespaces/logs.py +0 -0
  41. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus/namespaces/portal.py +0 -0
  42. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus/namespaces/processor.py +0 -0
  43. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus/namespaces/recipes.py +0 -0
  44. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus/namespaces/redis.py +0 -0
  45. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus/namespaces/secrets.py +0 -0
  46. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus/namespaces/secure.py +0 -0
  47. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus/namespaces/stash.py +0 -0
  48. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus/namespaces/sync.py +0 -0
  49. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus/namespaces/warden.py +0 -0
  50. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus/namespaces/workflow.py +0 -0
  51. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus/services/__init__.py +0 -0
  52. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus_sdk_python.egg-info/dependency_links.txt +0 -0
  53. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus_sdk_python.egg-info/requires.txt +0 -0
  54. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/dominus_sdk_python.egg-info/top_level.txt +0 -0
  55. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/setup.cfg +0 -0
  56. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/tests/test_auth.py +0 -0
  57. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/tests/test_authority_public_vocabulary.py +0 -0
  58. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/tests/test_browser_namespace.py +0 -0
  59. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/tests/test_control_plane_namespaces.py +0 -0
  60. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/tests/test_errors.py +0 -0
  61. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/tests/test_flat_commands.py +0 -0
  62. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/tests/test_health.py +0 -0
  63. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/tests/test_logs.py +0 -0
  64. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/tests/test_provisioning_parity.py +0 -0
  65. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/tests/test_recipes_namespace.py +0 -0
  66. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/tests/test_transport_compat.py +0 -0
  67. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/tests/test_workflow_lifecycle.py +0 -0
  68. {dominus_sdk_python-6.1.0 → dominus_sdk_python-6.1.2}/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: 6.1.0
3
+ Version: 6.1.2
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
@@ -42,7 +42,7 @@ Async Python SDK for the Dominus gateway-first service plane.
42
42
  - Gateway-scoped client mode for MCP and other user-JWT sessions
43
43
  - Transport compatibility for wrapped `{success,data}` responses and unwrapped Warden/control-plane success objects
44
44
  - Local helpers for JWT verification, trace propagation, retries, and console capture
45
- - Current package version: `6.0.0`
45
+ - Current package version: `6.1.1`
46
46
 
47
47
  ## Install
48
48
 
@@ -94,6 +94,20 @@ machine_logs = await dominus.logs.tail(
94
94
  since="2026-04-11T08:33:00Z",
95
95
  level="error",
96
96
  )
97
+ policy = await dominus.platform.ensure_policy_decision(
98
+ {
99
+ "group": "dominus",
100
+ "repository": "carebridgesystems/dominus-platform-worker",
101
+ },
102
+ actor_context={"type": "user", "id": "operator-1"},
103
+ )
104
+ coder_run = await dominus.coder.ensure_run(
105
+ policy_decision_id=policy["data"]["policy_decision"]["decision_id"],
106
+ workflow_recipe_ref="recipe://workflow-recipe-v1/coder-feature@head",
107
+ repository="carebridgesystems/dominus-platform-worker",
108
+ instructions="Fix failing tests",
109
+ actor_context={"type": "user", "id": "operator-1"},
110
+ )
97
111
 
98
112
  # Archive maintenance is explicit and dry-run first.
99
113
  verify = await dominus.authority.verify_timeline_archive_manifests(
@@ -206,6 +220,8 @@ JWT and selected scope headers directly through Gateway.
206
220
  | `browser` | Browser Worker | Browser run health, ensure/start/status/result/retry/nudge/cancel/timeline/dossier |
207
221
  | `deployer` | Deployer | Thin operator control-plane request surface |
208
222
  | `warden` | Warden | Thin operator control-plane request surface |
223
+ | `platform` | Platform Worker | Group/repository policy decisions with actor attribution |
224
+ | `coder` | Coder Runtime | Policy-bound Coder run lifecycle with workflow/pipeline recipe launch sources |
209
225
  | `fastapi` | Local decorators | `@jwt`, `@psk`, `@scopes(...)` |
210
226
 
211
227
  ## Root Shortcuts
@@ -9,7 +9,7 @@ Async Python SDK for the Dominus gateway-first service plane.
9
9
  - Gateway-scoped client mode for MCP and other user-JWT sessions
10
10
  - Transport compatibility for wrapped `{success,data}` responses and unwrapped Warden/control-plane success objects
11
11
  - Local helpers for JWT verification, trace propagation, retries, and console capture
12
- - Current package version: `6.0.0`
12
+ - Current package version: `6.1.1`
13
13
 
14
14
  ## Install
15
15
 
@@ -61,6 +61,20 @@ machine_logs = await dominus.logs.tail(
61
61
  since="2026-04-11T08:33:00Z",
62
62
  level="error",
63
63
  )
64
+ policy = await dominus.platform.ensure_policy_decision(
65
+ {
66
+ "group": "dominus",
67
+ "repository": "carebridgesystems/dominus-platform-worker",
68
+ },
69
+ actor_context={"type": "user", "id": "operator-1"},
70
+ )
71
+ coder_run = await dominus.coder.ensure_run(
72
+ policy_decision_id=policy["data"]["policy_decision"]["decision_id"],
73
+ workflow_recipe_ref="recipe://workflow-recipe-v1/coder-feature@head",
74
+ repository="carebridgesystems/dominus-platform-worker",
75
+ instructions="Fix failing tests",
76
+ actor_context={"type": "user", "id": "operator-1"},
77
+ )
64
78
 
65
79
  # Archive maintenance is explicit and dry-run first.
66
80
  verify = await dominus.authority.verify_timeline_archive_manifests(
@@ -173,6 +187,8 @@ JWT and selected scope headers directly through Gateway.
173
187
  | `browser` | Browser Worker | Browser run health, ensure/start/status/result/retry/nudge/cancel/timeline/dossier |
174
188
  | `deployer` | Deployer | Thin operator control-plane request surface |
175
189
  | `warden` | Warden | Thin operator control-plane request surface |
190
+ | `platform` | Platform Worker | Group/repository policy decisions with actor attribution |
191
+ | `coder` | Coder Runtime | Policy-bound Coder run lifecycle with workflow/pipeline recipe launch sources |
176
192
  | `fastapi` | Local decorators | `@jwt`, `@psk`, `@scopes(...)` |
177
193
 
178
194
  ## Root Shortcuts
@@ -114,6 +114,7 @@ from .namespaces.deployer import DeployerNamespace
114
114
  from .namespaces.warden import WardenNamespace
115
115
  from .namespaces.platform import PlatformNamespace
116
116
  from .namespaces.coder import CoderNamespace
117
+ from .namespaces.publisher import PublisherNamespace
117
118
 
118
119
  # Export AI namespace for agent-runtime operations
119
120
  from .namespaces.ai import (
@@ -164,7 +165,7 @@ from .errors import (
164
165
  TimeoutError as DominusTimeoutError,
165
166
  )
166
167
 
167
- __version__ = "6.1.0"
168
+ __version__ = "6.1.2"
168
169
  __all__ = [
169
170
  # Main SDK instance
170
171
  "dominus",
@@ -217,6 +218,7 @@ __all__ = [
217
218
  "WardenNamespace",
218
219
  "PlatformNamespace",
219
220
  "CoderNamespace",
221
+ "PublisherNamespace",
220
222
  # AI namespace for agent-runtime operations
221
223
  "AiNamespace",
222
224
  "RagSubNamespace",
@@ -54,6 +54,26 @@ def _mutation_body(
54
54
  )
55
55
 
56
56
 
57
+ def _actor_headers(actor_context: Optional[Dict[str, str]]) -> Optional[Dict[str, str]]:
58
+ if not actor_context:
59
+ return None
60
+ actor_type = str(actor_context.get("type") or actor_context.get("actor_type") or "").strip()
61
+ actor_id = str(actor_context.get("id") or actor_context.get("actor_id") or "").strip()
62
+ if not actor_type or not actor_id:
63
+ return None
64
+ return {"X-Actor-Type": actor_type, "X-Actor-Id": actor_id}
65
+
66
+
67
+ def _assert_one_launch_source(workflow_recipe_ref: Optional[str], pipeline_recipe_ref: Optional[str]) -> None:
68
+ present = [
69
+ ref
70
+ for ref in (workflow_recipe_ref, pipeline_recipe_ref)
71
+ if isinstance(ref, str) and ref.strip()
72
+ ]
73
+ if len(present) != 1:
74
+ raise ValueError("ensure_run requires exactly one of workflow_recipe_ref or pipeline_recipe_ref")
75
+
76
+
57
77
  class CoderNamespace:
58
78
  """Coder run lifecycle helpers."""
59
79
 
@@ -101,9 +121,11 @@ class CoderNamespace:
101
121
  instructions: Optional[str] = None,
102
122
  inputs: Optional[Dict[str, Any]] = None,
103
123
  metadata: Optional[Dict[str, Any]] = None,
124
+ actor_context: Optional[Dict[str, str]] = None,
104
125
  ) -> Dict[str, Any]:
105
126
  if not policy_decision_id or policy_decision_id.strip() == "":
106
127
  raise ValueError("ensure_run requires policy_decision_id")
128
+ _assert_one_launch_source(workflow_recipe_ref, pipeline_recipe_ref)
107
129
 
108
130
  body = _compact(
109
131
  {
@@ -128,6 +150,7 @@ class CoderNamespace:
128
150
  "/runs/ensure",
129
151
  method="POST",
130
152
  body=body,
153
+ headers=_actor_headers(actor_context),
131
154
  timeout=600.0,
132
155
  )
133
156
 
@@ -139,6 +162,7 @@ class CoderNamespace:
139
162
  repository: Optional[str] = None,
140
163
  limit: Optional[int] = None,
141
164
  offset: Optional[int] = None,
165
+ actor_context: Optional[Dict[str, str]] = None,
142
166
  ) -> Dict[str, Any]:
143
167
  qs = _query_string(
144
168
  {
@@ -149,10 +173,19 @@ class CoderNamespace:
149
173
  "offset": offset,
150
174
  }
151
175
  )
152
- return await self.request(f"/runs{qs}", method="GET")
176
+ return await self.request(f"/runs{qs}", method="GET", headers=_actor_headers(actor_context))
153
177
 
154
- async def get_run(self, run_id: str) -> Dict[str, Any]:
155
- return await self.request(f"/runs/{quote(run_id, safe='')}", method="GET")
178
+ async def get_run(
179
+ self,
180
+ run_id: str,
181
+ *,
182
+ actor_context: Optional[Dict[str, str]] = None,
183
+ ) -> Dict[str, Any]:
184
+ return await self.request(
185
+ f"/runs/{quote(run_id, safe='')}",
186
+ method="GET",
187
+ headers=_actor_headers(actor_context),
188
+ )
156
189
 
157
190
  async def cancel_run(
158
191
  self,
@@ -161,6 +194,7 @@ class CoderNamespace:
161
194
  reason: Optional[str] = None,
162
195
  idempotency_key: Optional[str] = None,
163
196
  metadata: Optional[Dict[str, Any]] = None,
197
+ actor_context: Optional[Dict[str, str]] = None,
164
198
  ) -> Dict[str, Any]:
165
199
  return await self.request(
166
200
  f"/runs/{quote(run_id, safe='')}/cancel",
@@ -170,6 +204,7 @@ class CoderNamespace:
170
204
  idempotency_key=idempotency_key,
171
205
  metadata=metadata,
172
206
  ),
207
+ headers=_actor_headers(actor_context),
173
208
  )
174
209
 
175
210
  async def retry_run(
@@ -179,6 +214,7 @@ class CoderNamespace:
179
214
  reason: Optional[str] = None,
180
215
  idempotency_key: Optional[str] = None,
181
216
  metadata: Optional[Dict[str, Any]] = None,
217
+ actor_context: Optional[Dict[str, str]] = None,
182
218
  ) -> Dict[str, Any]:
183
219
  return await self.request(
184
220
  f"/runs/{quote(run_id, safe='')}/retry",
@@ -188,6 +224,7 @@ class CoderNamespace:
188
224
  idempotency_key=idempotency_key,
189
225
  metadata=metadata,
190
226
  ),
227
+ headers=_actor_headers(actor_context),
191
228
  )
192
229
 
193
230
  async def nudge_run(
@@ -197,6 +234,7 @@ class CoderNamespace:
197
234
  reason: Optional[str] = None,
198
235
  idempotency_key: Optional[str] = None,
199
236
  metadata: Optional[Dict[str, Any]] = None,
237
+ actor_context: Optional[Dict[str, str]] = None,
200
238
  ) -> Dict[str, Any]:
201
239
  return await self.request(
202
240
  f"/runs/{quote(run_id, safe='')}/nudge",
@@ -206,16 +244,29 @@ class CoderNamespace:
206
244
  idempotency_key=idempotency_key,
207
245
  metadata=metadata,
208
246
  ),
247
+ headers=_actor_headers(actor_context),
209
248
  )
210
249
 
211
- async def get_run_summary(self, run_id: str) -> Dict[str, Any]:
250
+ async def get_run_summary(
251
+ self,
252
+ run_id: str,
253
+ *,
254
+ actor_context: Optional[Dict[str, str]] = None,
255
+ ) -> Dict[str, Any]:
212
256
  return await self.request(
213
257
  f"/runs/{quote(run_id, safe='')}/summary",
214
258
  method="GET",
259
+ headers=_actor_headers(actor_context),
215
260
  )
216
261
 
217
- async def get_run_artifacts(self, run_id: str) -> Dict[str, Any]:
262
+ async def get_run_artifacts(
263
+ self,
264
+ run_id: str,
265
+ *,
266
+ actor_context: Optional[Dict[str, str]] = None,
267
+ ) -> Dict[str, Any]:
218
268
  return await self.request(
219
269
  f"/runs/{quote(run_id, safe='')}/artifacts",
220
270
  method="GET",
271
+ headers=_actor_headers(actor_context),
221
272
  )
@@ -23,6 +23,16 @@ def _normalize_platform_path(path: str) -> str:
23
23
  return f"/svc/platform{normalized}"
24
24
 
25
25
 
26
+ def _actor_headers(actor_context: Optional[Dict[str, str]]) -> Optional[Dict[str, str]]:
27
+ if not actor_context:
28
+ return None
29
+ actor_type = str(actor_context.get("type") or actor_context.get("actor_type") or "").strip()
30
+ actor_id = str(actor_context.get("id") or actor_context.get("actor_id") or "").strip()
31
+ if not actor_type or not actor_id:
32
+ return None
33
+ return {"X-Actor-Type": actor_type, "X-Actor-Id": actor_id}
34
+
35
+
26
36
  class PlatformNamespace:
27
37
  """Platform group/repository policy helpers."""
28
38
 
@@ -105,26 +115,41 @@ class PlatformNamespace:
105
115
  body=repository,
106
116
  )
107
117
 
108
- async def ensure_policy_decision(self, options: Dict[str, Any]) -> Dict[str, Any]:
118
+ async def ensure_policy_decision(
119
+ self,
120
+ options: Dict[str, Any],
121
+ *,
122
+ actor_context: Optional[Dict[str, str]] = None,
123
+ ) -> Dict[str, Any]:
109
124
  return await self.request(
110
125
  "/policy/decisions/ensure",
111
126
  method="POST",
112
127
  body=options,
128
+ headers=_actor_headers(actor_context),
113
129
  )
114
130
 
115
- async def get_policy_decision(self, decision_id: str) -> Dict[str, Any]:
131
+ async def get_policy_decision(
132
+ self,
133
+ decision_id: str,
134
+ *,
135
+ actor_context: Optional[Dict[str, str]] = None,
136
+ ) -> Dict[str, Any]:
116
137
  return await self.request(
117
138
  f"/policy/decisions/{quote(decision_id, safe='')}",
118
139
  method="GET",
140
+ headers=_actor_headers(actor_context),
119
141
  )
120
142
 
121
143
  async def rehydrate_policy_decision(
122
144
  self,
123
145
  decision_id: str,
124
146
  options: Optional[Dict[str, Any]] = None,
147
+ *,
148
+ actor_context: Optional[Dict[str, str]] = None,
125
149
  ) -> Dict[str, Any]:
126
150
  return await self.request(
127
151
  f"/policy/decisions/{quote(decision_id, safe='')}/rehydrate",
128
152
  method="POST",
129
153
  body=options or {},
154
+ headers=_actor_headers(actor_context),
130
155
  )
@@ -0,0 +1,300 @@
1
+ """Publisher namespace.
2
+
3
+ Build-artifact lifecycle, signing, release records, channel pins, and resolver
4
+ reads for dominus-publisher. This surface intentionally does not expose raw
5
+ runner credentials or direct byte upload shortcuts.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from typing import Any, Dict, Optional, TYPE_CHECKING
10
+ from urllib.parse import quote, urlencode
11
+
12
+ if TYPE_CHECKING:
13
+ from ..start import Dominus
14
+
15
+
16
+ def _compact(payload: Dict[str, Any]) -> Dict[str, Any]:
17
+ out: Dict[str, Any] = {}
18
+ for key, value in payload.items():
19
+ if value is None:
20
+ continue
21
+ if isinstance(value, str) and value.strip() == "":
22
+ continue
23
+ out[key] = value
24
+ return out
25
+
26
+
27
+ def _query_string(params: Dict[str, Any]) -> str:
28
+ cleaned = _compact(params)
29
+ return f"?{urlencode(cleaned)}" if cleaned else ""
30
+
31
+
32
+ def _normalize_publisher_path(path: str) -> str:
33
+ trimmed = path.strip()
34
+ normalized = trimmed if trimmed.startswith("/") else f"/{trimmed}"
35
+ if normalized == "/svc/publisher" or normalized.startswith("/svc/publisher/"):
36
+ return normalized
37
+ if normalized == "/api/publisher" or normalized.startswith("/api/publisher/"):
38
+ return normalized.replace("/api/", "/svc/", 1)
39
+ return f"/svc/publisher{normalized}"
40
+
41
+
42
+ def _actor_headers(actor_context: Optional[Dict[str, str]]) -> Optional[Dict[str, str]]:
43
+ if not actor_context:
44
+ return None
45
+ actor_type = str(actor_context.get("type") or actor_context.get("actor_type") or "").strip()
46
+ actor_id = str(actor_context.get("id") or actor_context.get("actor_id") or "").strip()
47
+ if not actor_type or not actor_id:
48
+ return None
49
+ return {"X-Actor-Type": actor_type, "X-Actor-Id": actor_id}
50
+
51
+
52
+ def _mutation_body(
53
+ *,
54
+ reason: Optional[str] = None,
55
+ idempotency_key: Optional[str] = None,
56
+ metadata: Optional[Dict[str, Any]] = None,
57
+ ) -> Dict[str, Any]:
58
+ return _compact(
59
+ {
60
+ "reason": reason,
61
+ "idempotency_key": idempotency_key,
62
+ "metadata": metadata,
63
+ }
64
+ )
65
+
66
+
67
+ class PublisherNamespace:
68
+ """Publisher build-artifact lifecycle helpers."""
69
+
70
+ def __init__(self, client: "Dominus"):
71
+ self._client = client
72
+
73
+ async def request(
74
+ self,
75
+ path: str,
76
+ *,
77
+ method: str = "GET",
78
+ body: Optional[Dict[str, Any]] = None,
79
+ headers: Optional[Dict[str, str]] = None,
80
+ timeout: float = 30.0,
81
+ ) -> Any:
82
+ return await self._client.gateway_fetch(
83
+ _normalize_publisher_path(path),
84
+ method=method,
85
+ body=body,
86
+ headers=headers,
87
+ timeout=timeout,
88
+ )
89
+
90
+ async def health(self) -> Dict[str, Any]:
91
+ return await self.request("/health", method="GET")
92
+
93
+ async def ready(self) -> Dict[str, Any]:
94
+ return await self.request("/ready", method="GET")
95
+
96
+ async def list_builds(
97
+ self,
98
+ *,
99
+ actor_context: Optional[Dict[str, str]] = None,
100
+ ) -> Dict[str, Any]:
101
+ return await self.request("/builds", method="GET", headers=_actor_headers(actor_context))
102
+
103
+ async def ensure_build(
104
+ self,
105
+ payload: Dict[str, Any],
106
+ *,
107
+ actor_context: Optional[Dict[str, str]] = None,
108
+ ) -> Dict[str, Any]:
109
+ return await self.request(
110
+ "/builds/ensure",
111
+ method="POST",
112
+ body=payload,
113
+ headers=_actor_headers(actor_context),
114
+ timeout=600.0,
115
+ )
116
+
117
+ async def get_build(
118
+ self,
119
+ build_id: str,
120
+ *,
121
+ actor_context: Optional[Dict[str, str]] = None,
122
+ ) -> Dict[str, Any]:
123
+ return await self.request(
124
+ f"/builds/{quote(build_id, safe='')}",
125
+ method="GET",
126
+ headers=_actor_headers(actor_context),
127
+ )
128
+
129
+ async def retry_build(
130
+ self,
131
+ build_id: str,
132
+ *,
133
+ reason: Optional[str] = None,
134
+ idempotency_key: Optional[str] = None,
135
+ metadata: Optional[Dict[str, Any]] = None,
136
+ actor_context: Optional[Dict[str, str]] = None,
137
+ ) -> Dict[str, Any]:
138
+ return await self.request(
139
+ f"/builds/{quote(build_id, safe='')}/retry",
140
+ method="POST",
141
+ body=_mutation_body(reason=reason, idempotency_key=idempotency_key, metadata=metadata),
142
+ headers=_actor_headers(actor_context),
143
+ )
144
+
145
+ async def cancel_build(
146
+ self,
147
+ build_id: str,
148
+ *,
149
+ reason: Optional[str] = None,
150
+ idempotency_key: Optional[str] = None,
151
+ metadata: Optional[Dict[str, Any]] = None,
152
+ actor_context: Optional[Dict[str, str]] = None,
153
+ ) -> Dict[str, Any]:
154
+ return await self.request(
155
+ f"/builds/{quote(build_id, safe='')}/cancel",
156
+ method="POST",
157
+ body=_mutation_body(reason=reason, idempotency_key=idempotency_key, metadata=metadata),
158
+ headers=_actor_headers(actor_context),
159
+ )
160
+
161
+ async def evaluate_signing(
162
+ self,
163
+ payload: Dict[str, Any],
164
+ *,
165
+ actor_context: Optional[Dict[str, str]] = None,
166
+ ) -> Dict[str, Any]:
167
+ return await self.request(
168
+ "/builds/signing/evaluate",
169
+ method="POST",
170
+ body=payload,
171
+ headers=_actor_headers(actor_context),
172
+ )
173
+
174
+ async def record_signing_evidence(
175
+ self,
176
+ payload: Dict[str, Any],
177
+ *,
178
+ actor_context: Optional[Dict[str, str]] = None,
179
+ ) -> Dict[str, Any]:
180
+ return await self.request(
181
+ "/builds/signing/evidence",
182
+ method="POST",
183
+ body=payload,
184
+ headers=_actor_headers(actor_context),
185
+ )
186
+
187
+ async def list_artifacts(
188
+ self,
189
+ *,
190
+ actor_context: Optional[Dict[str, str]] = None,
191
+ ) -> Dict[str, Any]:
192
+ return await self.request("/artifacts", method="GET", headers=_actor_headers(actor_context))
193
+
194
+ async def record_release(
195
+ self,
196
+ payload: Dict[str, Any],
197
+ *,
198
+ actor_context: Optional[Dict[str, str]] = None,
199
+ ) -> Dict[str, Any]:
200
+ return await self.request(
201
+ "/artifacts/releases/record",
202
+ method="POST",
203
+ body=payload,
204
+ headers=_actor_headers(actor_context),
205
+ timeout=600.0,
206
+ )
207
+
208
+ async def resolve_artifact(
209
+ self,
210
+ artifact_key: str,
211
+ *,
212
+ selector: Optional[str] = None,
213
+ environment: Optional[str] = None,
214
+ target_app: Optional[str] = None,
215
+ architecture: Optional[str] = None,
216
+ runtime_family: Optional[str] = None,
217
+ actor_context: Optional[Dict[str, str]] = None,
218
+ ) -> Dict[str, Any]:
219
+ qs = _query_string(
220
+ {
221
+ "selector": selector,
222
+ "environment": environment,
223
+ "target_app": target_app,
224
+ "architecture": architecture,
225
+ "runtime_family": runtime_family,
226
+ }
227
+ )
228
+ return await self.request(
229
+ f"/artifacts/{quote(artifact_key, safe='')}{qs}",
230
+ method="GET",
231
+ headers=_actor_headers(actor_context),
232
+ )
233
+
234
+ async def list_channels(
235
+ self,
236
+ *,
237
+ actor_context: Optional[Dict[str, str]] = None,
238
+ ) -> Dict[str, Any]:
239
+ return await self.request("/channels", method="GET", headers=_actor_headers(actor_context))
240
+
241
+ async def get_channel(
242
+ self,
243
+ artifact_key: str,
244
+ selector: str,
245
+ *,
246
+ environment: Optional[str] = None,
247
+ target_app: Optional[str] = None,
248
+ architecture: Optional[str] = None,
249
+ runtime_family: Optional[str] = None,
250
+ actor_context: Optional[Dict[str, str]] = None,
251
+ ) -> Dict[str, Any]:
252
+ qs = _query_string(
253
+ {
254
+ "environment": environment,
255
+ "target_app": target_app,
256
+ "architecture": architecture,
257
+ "runtime_family": runtime_family,
258
+ }
259
+ )
260
+ return await self.request(
261
+ f"/channels/{quote(artifact_key, safe='')}/{quote(selector, safe='')}{qs}",
262
+ method="GET",
263
+ headers=_actor_headers(actor_context),
264
+ )
265
+
266
+ async def pin_channel(
267
+ self,
268
+ artifact_key: str,
269
+ selector: str,
270
+ payload: Dict[str, Any],
271
+ *,
272
+ actor_context: Optional[Dict[str, str]] = None,
273
+ ) -> Dict[str, Any]:
274
+ return await self.request(
275
+ f"/channels/{quote(artifact_key, safe='')}/{quote(selector, safe='')}/pin",
276
+ method="POST",
277
+ body=payload,
278
+ headers=_actor_headers(actor_context),
279
+ )
280
+
281
+ async def list_prestage(
282
+ self,
283
+ *,
284
+ actor_context: Optional[Dict[str, str]] = None,
285
+ ) -> Dict[str, Any]:
286
+ return await self.request("/prestage", method="GET", headers=_actor_headers(actor_context))
287
+
288
+ async def ensure_prestage(
289
+ self,
290
+ payload: Dict[str, Any],
291
+ *,
292
+ actor_context: Optional[Dict[str, str]] = None,
293
+ ) -> Dict[str, Any]:
294
+ return await self.request(
295
+ "/prestage/ensure",
296
+ method="POST",
297
+ body=payload,
298
+ headers=_actor_headers(actor_context),
299
+ timeout=600.0,
300
+ )
@@ -181,10 +181,12 @@ class Dominus:
181
181
  from .namespaces.warden import WardenNamespace
182
182
  from .namespaces.platform import PlatformNamespace
183
183
  from .namespaces.coder import CoderNamespace
184
+ from .namespaces.publisher import PublisherNamespace
184
185
  self.deployer = DeployerNamespace(self)
185
186
  self.warden = WardenNamespace(self)
186
187
  self.platform = PlatformNamespace(self)
187
188
  self.coder = CoderNamespace(self)
189
+ self.publisher = PublisherNamespace(self)
188
190
 
189
191
  # Stash worker (per-scope durable items: credentials + configs)
190
192
  from .namespaces.stash import StashNamespace
@@ -539,6 +541,7 @@ class Dominus:
539
541
  or "/svc/admin/" in path
540
542
  or "/svc/platform/" in path
541
543
  or "/svc/coder/" in path
544
+ or "/svc/publisher/" in path
542
545
  )
543
546
  decode_base64 = (
544
547
  "/svc/warden/secrets" in path
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dominus-sdk-python
3
- Version: 6.1.0
3
+ Version: 6.1.2
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
@@ -42,7 +42,7 @@ Async Python SDK for the Dominus gateway-first service plane.
42
42
  - Gateway-scoped client mode for MCP and other user-JWT sessions
43
43
  - Transport compatibility for wrapped `{success,data}` responses and unwrapped Warden/control-plane success objects
44
44
  - Local helpers for JWT verification, trace propagation, retries, and console capture
45
- - Current package version: `6.0.0`
45
+ - Current package version: `6.1.1`
46
46
 
47
47
  ## Install
48
48
 
@@ -94,6 +94,20 @@ machine_logs = await dominus.logs.tail(
94
94
  since="2026-04-11T08:33:00Z",
95
95
  level="error",
96
96
  )
97
+ policy = await dominus.platform.ensure_policy_decision(
98
+ {
99
+ "group": "dominus",
100
+ "repository": "carebridgesystems/dominus-platform-worker",
101
+ },
102
+ actor_context={"type": "user", "id": "operator-1"},
103
+ )
104
+ coder_run = await dominus.coder.ensure_run(
105
+ policy_decision_id=policy["data"]["policy_decision"]["decision_id"],
106
+ workflow_recipe_ref="recipe://workflow-recipe-v1/coder-feature@head",
107
+ repository="carebridgesystems/dominus-platform-worker",
108
+ instructions="Fix failing tests",
109
+ actor_context={"type": "user", "id": "operator-1"},
110
+ )
97
111
 
98
112
  # Archive maintenance is explicit and dry-run first.
99
113
  verify = await dominus.authority.verify_timeline_archive_manifests(
@@ -206,6 +220,8 @@ JWT and selected scope headers directly through Gateway.
206
220
  | `browser` | Browser Worker | Browser run health, ensure/start/status/result/retry/nudge/cancel/timeline/dossier |
207
221
  | `deployer` | Deployer | Thin operator control-plane request surface |
208
222
  | `warden` | Warden | Thin operator control-plane request surface |
223
+ | `platform` | Platform Worker | Group/repository policy decisions with actor attribution |
224
+ | `coder` | Coder Runtime | Policy-bound Coder run lifecycle with workflow/pipeline recipe launch sources |
209
225
  | `fastapi` | Local decorators | `@jwt`, `@psk`, `@scopes(...)` |
210
226
 
211
227
  ## Root Shortcuts
@@ -33,6 +33,7 @@ dominus/namespaces/logs.py
33
33
  dominus/namespaces/platform.py
34
34
  dominus/namespaces/portal.py
35
35
  dominus/namespaces/processor.py
36
+ dominus/namespaces/publisher.py
36
37
  dominus/namespaces/recipes.py
37
38
  dominus/namespaces/redis.py
38
39
  dominus/namespaces/secrets.py
@@ -58,6 +59,7 @@ tests/test_logs.py
58
59
  tests/test_platform_coder_namespaces.py
59
60
  tests/test_provisioning_parity.py
60
61
  tests/test_public_exports.py
62
+ tests/test_publisher_namespace.py
61
63
  tests/test_recipes_namespace.py
62
64
  tests/test_transport_compat.py
63
65
  tests/test_workflow_lifecycle.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dominus-sdk-python"
7
- version = "6.1.0"
7
+ version = "6.1.2"
8
8
  description = "Python SDK for the Dominus gateway-first platform"
9
9
  readme = "README.md"
10
10
  license = "LicenseRef-Proprietary"
@@ -31,7 +31,8 @@ async def test_platform_namespace_normalizes_helpers_onto_service_routes():
31
31
  "group": "dominus",
32
32
  "repository": "carebridgesystems/dominus-platform-worker",
33
33
  "metadata": {"safe": "ok"},
34
- }
34
+ },
35
+ actor_context={"type": "user", "id": "user-1"},
35
36
  )
36
37
  await platform.rehydrate_policy_decision("pd_123", {"run_id": "run_123"})
37
38
 
@@ -51,6 +52,7 @@ async def test_platform_namespace_normalizes_helpers_onto_service_routes():
51
52
  "repository": "carebridgesystems/dominus-platform-worker",
52
53
  "metadata": {"safe": "ok"},
53
54
  }
55
+ assert client.calls[5][1]["headers"] == {"X-Actor-Type": "user", "X-Actor-Id": "user-1"}
54
56
 
55
57
 
56
58
  @pytest.mark.asyncio
@@ -76,8 +78,10 @@ async def test_coder_namespace_exposes_lifecycle_helpers_but_no_raw_shell():
76
78
  policy_decision_id="pd_123",
77
79
  mode="async",
78
80
  task_recipe_ref="recipe://coder-task-recipe-v1/fix-tests@head",
81
+ workflow_recipe_ref="recipe://workflow-recipe-v1/coder-feature@head",
79
82
  repository="carebridgesystems/dominus-platform-worker",
80
83
  instructions="Fix failing tests",
84
+ actor_context={"type": "user", "id": "user-1"},
81
85
  )
82
86
  await coder.list_runs(status="running", limit=10)
83
87
  await coder.cancel_run("run_123", reason="operator requested")
@@ -99,9 +103,11 @@ async def test_coder_namespace_exposes_lifecycle_helpers_but_no_raw_shell():
99
103
  "policy_decision_id": "pd_123",
100
104
  "mode": "async",
101
105
  "task_recipe_ref": "recipe://coder-task-recipe-v1/fix-tests@head",
106
+ "workflow_recipe_ref": "recipe://workflow-recipe-v1/coder-feature@head",
102
107
  "repository": "carebridgesystems/dominus-platform-worker",
103
108
  "instructions": "Fix failing tests",
104
109
  }
110
+ assert client.calls[0][1]["headers"] == {"X-Actor-Type": "user", "X-Actor-Id": "user-1"}
105
111
  assert not hasattr(coder, "shell")
106
112
  assert not hasattr(coder, "exec")
107
113
  assert not hasattr(coder, "command")
@@ -116,3 +122,21 @@ async def test_coder_ensure_run_requires_policy_decision_id():
116
122
  await coder.ensure_run(policy_decision_id="")
117
123
 
118
124
  assert client.calls == []
125
+
126
+
127
+ @pytest.mark.asyncio
128
+ async def test_coder_ensure_run_requires_exactly_one_authority_launch_source():
129
+ client = MockGatewayClient()
130
+ coder = CoderNamespace(client)
131
+
132
+ with pytest.raises(ValueError, match="exactly one"):
133
+ await coder.ensure_run(policy_decision_id="pd_123")
134
+
135
+ with pytest.raises(ValueError, match="exactly one"):
136
+ await coder.ensure_run(
137
+ policy_decision_id="pd_123",
138
+ workflow_recipe_ref="recipe://workflow-recipe-v1/coder@head",
139
+ pipeline_recipe_ref="recipe://pipeline-recipe-v1/coder@head",
140
+ )
141
+
142
+ assert client.calls == []
@@ -3,6 +3,7 @@ from dominus import (
3
3
  CoderNamespace,
4
4
  DeployerNamespace,
5
5
  PlatformNamespace,
6
+ PublisherNamespace,
6
7
  RecipesNamespace,
7
8
  WardenNamespace,
8
9
  gateway_circuit_breaker,
@@ -37,6 +38,7 @@ def test_top_level_exports_drop_delegate_alias():
37
38
  assert RecipesNamespace is not None
38
39
  assert PlatformNamespace is not None
39
40
  assert CoderNamespace is not None
41
+ assert PublisherNamespace is not None
40
42
 
41
43
 
42
44
  def test_singleton_exposes_browser_namespace():
@@ -46,3 +48,4 @@ def test_singleton_exposes_browser_namespace():
46
48
  assert callable(dominus.recipes.list_types)
47
49
  assert callable(dominus.platform.ensure_policy_decision)
48
50
  assert callable(dominus.coder.ensure_run)
51
+ assert callable(dominus.publisher.resolve_artifact)
@@ -0,0 +1,82 @@
1
+ import pytest
2
+
3
+ from dominus.namespaces.publisher import PublisherNamespace
4
+
5
+
6
+ class MockGatewayClient:
7
+ def __init__(self):
8
+ self.calls = []
9
+
10
+ async def gateway_fetch(self, path, **kwargs):
11
+ self.calls.append((path, kwargs))
12
+ return {"ok": True}
13
+
14
+
15
+ @pytest.mark.asyncio
16
+ async def test_publisher_namespace_normalizes_lifecycle_and_resolver_helpers():
17
+ client = MockGatewayClient()
18
+ publisher = PublisherNamespace(client)
19
+
20
+ await publisher.health()
21
+ await publisher.ready()
22
+ await publisher.list_builds()
23
+ await publisher.ensure_build(
24
+ {"artifact_key": "envoy_binary"},
25
+ actor_context={"type": "service", "id": "dominus-deployer"},
26
+ )
27
+ await publisher.get_build("pub_123")
28
+ await publisher.retry_build("pub_123", reason="rerun", idempotency_key="retry-1")
29
+ await publisher.cancel_build("pub_123", metadata={"requested_by": "operator"})
30
+ await publisher.evaluate_signing({"environment": "production"})
31
+ await publisher.record_signing_evidence({"artifact_key": "envoy_binary"})
32
+ await publisher.list_artifacts()
33
+ await publisher.record_release({"artifact_key": "envoy_binary"})
34
+ await publisher.resolve_artifact(
35
+ "envoy_binary",
36
+ selector="stable",
37
+ environment="production",
38
+ target_app="powerscribe",
39
+ architecture="x64",
40
+ runtime_family="rust-tauri",
41
+ )
42
+ await publisher.list_channels()
43
+ await publisher.get_channel("envoy_binary", "stable", environment="production")
44
+ await publisher.pin_channel("envoy_binary", "stable", {"release_ref": "ar://..."})
45
+ await publisher.list_prestage()
46
+
47
+ assert [(path, kwargs["method"]) for path, kwargs in client.calls] == [
48
+ ("/svc/publisher/health", "GET"),
49
+ ("/svc/publisher/ready", "GET"),
50
+ ("/svc/publisher/builds", "GET"),
51
+ ("/svc/publisher/builds/ensure", "POST"),
52
+ ("/svc/publisher/builds/pub_123", "GET"),
53
+ ("/svc/publisher/builds/pub_123/retry", "POST"),
54
+ ("/svc/publisher/builds/pub_123/cancel", "POST"),
55
+ ("/svc/publisher/builds/signing/evaluate", "POST"),
56
+ ("/svc/publisher/builds/signing/evidence", "POST"),
57
+ ("/svc/publisher/artifacts", "GET"),
58
+ ("/svc/publisher/artifacts/releases/record", "POST"),
59
+ ("/svc/publisher/artifacts/envoy_binary?selector=stable&environment=production&target_app=powerscribe&architecture=x64&runtime_family=rust-tauri", "GET"),
60
+ ("/svc/publisher/channels", "GET"),
61
+ ("/svc/publisher/channels/envoy_binary/stable?environment=production", "GET"),
62
+ ("/svc/publisher/channels/envoy_binary/stable/pin", "POST"),
63
+ ("/svc/publisher/prestage", "GET"),
64
+ ]
65
+ assert client.calls[3][1]["headers"] == {"X-Actor-Type": "service", "X-Actor-Id": "dominus-deployer"}
66
+ assert client.calls[5][1]["body"] == {"reason": "rerun", "idempotency_key": "retry-1"}
67
+ assert not hasattr(publisher, "shell")
68
+ assert not hasattr(publisher, "command")
69
+
70
+
71
+ @pytest.mark.asyncio
72
+ async def test_publisher_request_rewrites_api_paths_to_svc_paths():
73
+ client = MockGatewayClient()
74
+ publisher = PublisherNamespace(client)
75
+
76
+ await publisher.request("/api/publisher/builds", method="GET")
77
+ await publisher.request("/svc/publisher/channels", method="GET")
78
+
79
+ assert [path for path, _ in client.calls] == [
80
+ "/svc/publisher/builds",
81
+ "/svc/publisher/channels",
82
+ ]