dominus-sdk-python 5.0.0__tar.gz → 6.0.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 (63) hide show
  1. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/PKG-INFO +4 -4
  2. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/README.md +3 -3
  3. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus/__init__.py +1 -7
  4. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus/namespaces/artifacts.py +10 -27
  5. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus/namespaces/authority.py +33 -12
  6. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus/namespaces/recipes.py +24 -8
  7. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus/namespaces/workflow.py +49 -26
  8. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus_sdk_python.egg-info/PKG-INFO +4 -4
  9. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/pyproject.toml +1 -1
  10. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/tests/test_authority_public_vocabulary.py +43 -3
  11. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/tests/test_recipes_namespace.py +36 -0
  12. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/tests/test_workflow_lifecycle.py +23 -6
  13. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/tests/test_workflow_refs.py +47 -9
  14. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus/config/__init__.py +0 -0
  15. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus/config/endpoints.py +0 -0
  16. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus/errors.py +0 -0
  17. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus/helpers/__init__.py +0 -0
  18. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus/helpers/auth.py +0 -0
  19. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus/helpers/cache.py +0 -0
  20. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus/helpers/console_capture.py +0 -0
  21. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus/helpers/core.py +0 -0
  22. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus/helpers/crypto.py +0 -0
  23. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus/helpers/sse.py +0 -0
  24. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus/helpers/trace.py +0 -0
  25. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus/namespaces/__init__.py +0 -0
  26. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus/namespaces/admin.py +0 -0
  27. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus/namespaces/ai.py +0 -0
  28. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus/namespaces/auth.py +0 -0
  29. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus/namespaces/browser.py +0 -0
  30. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus/namespaces/courier.py +0 -0
  31. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus/namespaces/db.py +0 -0
  32. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus/namespaces/ddl.py +0 -0
  33. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus/namespaces/deployer.py +0 -0
  34. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus/namespaces/fastapi.py +0 -0
  35. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus/namespaces/files.py +0 -0
  36. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus/namespaces/health.py +0 -0
  37. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus/namespaces/jobs.py +0 -0
  38. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus/namespaces/logs.py +0 -0
  39. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus/namespaces/portal.py +0 -0
  40. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus/namespaces/processor.py +0 -0
  41. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus/namespaces/redis.py +0 -0
  42. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus/namespaces/secrets.py +0 -0
  43. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus/namespaces/secure.py +0 -0
  44. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus/namespaces/stash.py +0 -0
  45. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus/namespaces/sync.py +0 -0
  46. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus/namespaces/warden.py +0 -0
  47. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus/services/__init__.py +0 -0
  48. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus/start.py +0 -0
  49. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus_sdk_python.egg-info/SOURCES.txt +0 -0
  50. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus_sdk_python.egg-info/dependency_links.txt +0 -0
  51. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus_sdk_python.egg-info/requires.txt +0 -0
  52. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/dominus_sdk_python.egg-info/top_level.txt +0 -0
  53. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/setup.cfg +0 -0
  54. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/tests/test_auth.py +0 -0
  55. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/tests/test_browser_namespace.py +0 -0
  56. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/tests/test_control_plane_namespaces.py +0 -0
  57. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/tests/test_errors.py +0 -0
  58. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/tests/test_flat_commands.py +0 -0
  59. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/tests/test_health.py +0 -0
  60. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/tests/test_logs.py +0 -0
  61. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/tests/test_provisioning_parity.py +0 -0
  62. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/tests/test_public_exports.py +0 -0
  63. {dominus_sdk_python-5.0.0 → dominus_sdk_python-6.0.0}/tests/test_transport_compat.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dominus-sdk-python
3
- Version: 5.0.0
3
+ Version: 6.0.0
4
4
  Summary: Python SDK for the Dominus gateway-first platform
5
5
  Author-email: CareBridge Systems <dev@carebridge.io>
6
6
  License: Proprietary
@@ -43,7 +43,7 @@ Async Python SDK for the Dominus gateway-first service plane.
43
43
  - Gateway-scoped client mode for MCP and other user-JWT sessions
44
44
  - Transport compatibility for wrapped `{success,data}` responses and unwrapped Warden/control-plane success objects
45
45
  - Local helpers for JWT verification, trace propagation, retries, and console capture
46
- - Current package version: `4.0.8`
46
+ - Current package version: `6.0.0`
47
47
 
48
48
  ## Install
49
49
 
@@ -66,7 +66,7 @@ users = await dominus.db.query("users", filters={"status": "active"})
66
66
  await dominus.redis.set("session:123", {"user": "john"}, ttl=3600)
67
67
 
68
68
  run = await dominus.workflow.ensure(
69
- "wf://carebridge/report-cycle",
69
+ workflow_recipe_ref="recipe://workflow-recipe-v1/report-cycle@v3",
70
70
  subject="PCM47474562",
71
71
  company="summit-radiology",
72
72
  )
@@ -193,7 +193,7 @@ JWT and selected scope headers directly through Gateway.
193
193
  | `health` | Gateway | Health and ping helpers |
194
194
  | `admin` | Admin Worker | Admin category reseed/reset |
195
195
  | `ai` | Agent Runtime | Agent, completion, RAG, artifacts, results, raw orchestration |
196
- | `workflow` | Workflow Manager + Authority | Saved workflow CRUD and canonical run lifecycle |
196
+ | `workflow` | Workflow Manager + Authority | Saved workflow CRUD and recipe-backed run lifecycle |
197
197
  | `artifacts` | Artifact Worker | Addressed V2 artifacts, bookmarks, watches |
198
198
  | `jobs` | Job Worker | Enqueue, poll, dead-letter management |
199
199
  | `processor` | Processor | Batch and single-job processing |
@@ -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: `4.0.8`
12
+ - Current package version: `6.0.0`
13
13
 
14
14
  ## Install
15
15
 
@@ -32,7 +32,7 @@ users = await dominus.db.query("users", filters={"status": "active"})
32
32
  await dominus.redis.set("session:123", {"user": "john"}, ttl=3600)
33
33
 
34
34
  run = await dominus.workflow.ensure(
35
- "wf://carebridge/report-cycle",
35
+ workflow_recipe_ref="recipe://workflow-recipe-v1/report-cycle@v3",
36
36
  subject="PCM47474562",
37
37
  company="summit-radiology",
38
38
  )
@@ -159,7 +159,7 @@ JWT and selected scope headers directly through Gateway.
159
159
  | `health` | Gateway | Health and ping helpers |
160
160
  | `admin` | Admin Worker | Admin category reseed/reset |
161
161
  | `ai` | Agent Runtime | Agent, completion, RAG, artifacts, results, raw orchestration |
162
- | `workflow` | Workflow Manager + Authority | Saved workflow CRUD and canonical run lifecycle |
162
+ | `workflow` | Workflow Manager + Authority | Saved workflow CRUD and recipe-backed run lifecycle |
163
163
  | `artifacts` | Artifact Worker | Addressed V2 artifacts, bookmarks, watches |
164
164
  | `jobs` | Job Worker | Enqueue, poll, dead-letter management |
165
165
  | `processor` | Processor | Batch and single-job processing |
@@ -90,14 +90,12 @@ from .namespaces.health import HealthNamespace
90
90
  # Export new namespaces (Node.js SDK parity)
91
91
  from .namespaces.artifacts import (
92
92
  ARTIFACT_REF_PREFIX,
93
- DISPLAY_REF_PREFIX,
94
93
  ARTIFACT_ENVIRONMENTS,
95
94
  ArtifactsNamespace,
96
95
  build_artifact_ref,
97
96
  build_v2_artifact_ref,
98
97
  build_pinned_artifact_ref,
99
98
  build_legacy_artifact_ref,
100
- build_display_artifact_ref,
101
99
  parse_artifact_ref,
102
100
  try_parse_artifact_ref,
103
101
  validate_artifact_address,
@@ -105,7 +103,6 @@ from .namespaces.artifacts import (
105
103
  is_pinned_artifact_ref,
106
104
  is_head_artifact_ref,
107
105
  is_legacy_artifact_ref,
108
- is_display_artifact_ref,
109
106
  )
110
107
  from .namespaces.jobs import JobsNamespace
111
108
  from .namespaces.processor import ProcessorNamespace
@@ -165,7 +162,7 @@ from .errors import (
165
162
  TimeoutError as DominusTimeoutError,
166
163
  )
167
164
 
168
- __version__ = "4.6.2"
165
+ __version__ = "6.0.0"
169
166
  __all__ = [
170
167
  # Main SDK instance
171
168
  "dominus",
@@ -195,14 +192,12 @@ __all__ = [
195
192
  "HealthNamespace",
196
193
  # New namespaces (Node.js SDK parity)
197
194
  "ARTIFACT_REF_PREFIX",
198
- "DISPLAY_REF_PREFIX",
199
195
  "ARTIFACT_ENVIRONMENTS",
200
196
  "ArtifactsNamespace",
201
197
  "build_artifact_ref",
202
198
  "build_v2_artifact_ref",
203
199
  "build_pinned_artifact_ref",
204
200
  "build_legacy_artifact_ref",
205
- "build_display_artifact_ref",
206
201
  "parse_artifact_ref",
207
202
  "try_parse_artifact_ref",
208
203
  "validate_artifact_address",
@@ -210,7 +205,6 @@ __all__ = [
210
205
  "is_pinned_artifact_ref",
211
206
  "is_head_artifact_ref",
212
207
  "is_legacy_artifact_ref",
213
- "is_display_artifact_ref",
214
208
  "JobsNamespace",
215
209
  "ProcessorNamespace",
216
210
  "SyncNamespace",
@@ -35,10 +35,13 @@ def _safe_string(value: Any) -> str:
35
35
 
36
36
 
37
37
  ARTIFACT_REF_PREFIX = "ar://"
38
- DISPLAY_REF_PREFIX = "art:r:"
39
38
  ARTIFACT_ENVIRONMENTS = ("development", "staging", "production")
40
39
 
41
40
 
41
+ def _legacy_project_head_ref(project_id: str, environment: str, key: str) -> str:
42
+ return f"{ARTIFACT_REF_PREFIX}dominus/project:{project_id}/{environment}/legacy/{quote(key, safe='')}"
43
+
44
+
42
45
  def build_artifact_ref(
43
46
  *,
44
47
  project_slug: Optional[str] = None,
@@ -109,13 +112,6 @@ def build_pinned_artifact_ref(
109
112
  return f"{ref}@v{version}"
110
113
 
111
114
 
112
- def build_display_artifact_ref(artifact_key: str) -> Optional[str]:
113
- normalized_key = _safe_string(artifact_key)
114
- if not normalized_key:
115
- return None
116
- return f"{DISPLAY_REF_PREFIX}{normalized_key}"
117
-
118
-
119
115
  def build_legacy_artifact_ref(
120
116
  *,
121
117
  project_slug: str,
@@ -146,15 +142,6 @@ def parse_artifact_ref(artifact_ref: str) -> Optional[Dict[str, Any]]:
146
142
  normalized = _safe_string(artifact_ref)
147
143
  if not normalized:
148
144
  return None
149
- if normalized.startswith(DISPLAY_REF_PREFIX):
150
- artifact_key = normalized[len(DISPLAY_REF_PREFIX):]
151
- if not artifact_key:
152
- return None
153
- return {
154
- "format": "display",
155
- "raw": normalized,
156
- "artifact_key": artifact_key,
157
- }
158
145
  if not normalized.startswith(ARTIFACT_REF_PREFIX):
159
146
  return None
160
147
 
@@ -245,10 +232,6 @@ def is_legacy_artifact_ref(artifact_ref: Dict[str, Any]) -> bool:
245
232
  return artifact_ref.get("format") == "v1"
246
233
 
247
234
 
248
- def is_display_artifact_ref(artifact_ref: Dict[str, Any]) -> bool:
249
- return artifact_ref.get("format") == "display"
250
-
251
-
252
235
  def _split_version_suffix(value: str) -> tuple[str, Optional[str]]:
253
236
  if "@" not in value:
254
237
  return value, None
@@ -366,10 +349,10 @@ class ArtifactsNamespace:
366
349
  category=category,
367
350
  content_type=content_type,
368
351
  )
369
- disp = build_display_artifact_ref(use_key)
352
+ fallback_ref = _legacy_project_head_ref(project_id, environment, use_key)
370
353
  return {
371
354
  "key": use_key,
372
- "ref": v2.get("compatibility_ref") or v2.get("head_ref") or disp,
355
+ "ref": v2.get("head_ref") or fallback_ref,
373
356
  "storage_type": v2.get("storage_type") or "redis",
374
357
  "size_bytes": v2.get("size_bytes", 0),
375
358
  "expires_at": v2.get("expires_at") or "",
@@ -377,14 +360,14 @@ class ArtifactsNamespace:
377
360
 
378
361
  async def retrieve(self, key: str) -> Dict[str, Any]:
379
362
  """
380
- Retrieve by key via V2 display ref + target_project_id (compat with legacy data).
363
+ Retrieve by key via the addressed V2 compatibility head.
381
364
  """
382
- project_id, _environment = await self._legacy_project_context()
365
+ project_id, environment = await self._legacy_project_context()
366
+ ref = _legacy_project_head_ref(project_id, environment, key)
383
367
  return await self._api(
384
368
  "/api/artifact/v2/retrieve",
385
369
  body={
386
- "ref": f"{DISPLAY_REF_PREFIX}{key}",
387
- "target_project_id": project_id,
370
+ "ref": ref,
388
371
  },
389
372
  )
390
373
 
@@ -12,9 +12,9 @@ All methods route through the gateway (``use_gateway=True``) which maps
12
12
 
13
13
  Run launch is also exposed as ``dominus.workflow.ensure(...)``. Both call
14
14
  ``/api/authority/runs/ensure``. Use ``dominus.workflow.ensure`` for the
15
- canonical "launch a saved workflow" path; use ``dominus.authority.ensure_run``
16
- when you want to think in Authority terms (run lifecycle, run dossiers,
17
- retries, cancels).
15
+ canonical recipe-backed launch path; use ``dominus.authority.ensure_run`` when
16
+ you want to think in Authority terms (run lifecycle, run dossiers, retries,
17
+ cancels).
18
18
  """
19
19
  from __future__ import annotations
20
20
 
@@ -48,6 +48,14 @@ def _is_provisioning_bootstrap_run_kind(run_kind: Optional[str]) -> bool:
48
48
  return _normalize_run_kind_token(run_kind) == "provisioning_bootstrap"
49
49
 
50
50
 
51
+ def _is_workflow_recipe_ref(value: Optional[str]) -> bool:
52
+ return str(value or "").strip().startswith("recipe://workflow-recipe-v1/")
53
+
54
+
55
+ def _is_pipeline_recipe_ref(value: Optional[str]) -> bool:
56
+ return str(value or "").strip().startswith("recipe://pipeline-recipe-v1/")
57
+
58
+
51
59
  def _query_string(params: Mapping[str, Any]) -> str:
52
60
  cleaned = {k: v for k, v in params.items() if v is not None and v != ""}
53
61
  if not cleaned:
@@ -61,7 +69,10 @@ class AuthorityNamespace:
61
69
 
62
70
  Usage::
63
71
 
64
- await dominus.authority.ensure_run(workflow_id="...", subject="PCM47474562")
72
+ await dominus.authority.ensure_run(
73
+ workflow_recipe_ref="recipe://workflow-recipe-v1/report-cycle@head",
74
+ subject="PCM47474562",
75
+ )
65
76
  await dominus.authority.list_provisioning_targets()
66
77
  await dominus.authority.get_run_dossier(run_id)
67
78
  """
@@ -167,8 +178,8 @@ class AuthorityNamespace:
167
178
  async def ensure_run(
168
179
  self,
169
180
  *,
170
- workflow_id: Optional[str] = None,
171
- workflow_ref: Optional[str] = None,
181
+ workflow_recipe_ref: Optional[str] = None,
182
+ pipeline_recipe_ref: Optional[str] = None,
172
183
  run_kind: Optional[str] = None,
173
184
  bootstrap_profile_ref: Optional[str] = None,
174
185
  provisioning_target_slug: Optional[str] = None,
@@ -213,11 +224,23 @@ class AuthorityNamespace:
213
224
  ``bootstrap_provisioning_target`` (``execution_mode``, ``region``, ``bootstrap_profile_ref``, ...).
214
225
  """
215
226
  bootstrap = _is_provisioning_bootstrap_run_kind(run_kind)
216
- if not bootstrap and not workflow_id and not workflow_ref:
227
+ recipe_sources = [
228
+ value
229
+ for value in (
230
+ str(workflow_recipe_ref or "").strip(),
231
+ str(pipeline_recipe_ref or "").strip(),
232
+ )
233
+ if value
234
+ ]
235
+ if not bootstrap and len(recipe_sources) != 1:
217
236
  raise ValueError(
218
- "ensure_run requires workflow_id or workflow_ref unless run_kind is "
237
+ "ensure_run requires exactly one of workflow_recipe_ref or pipeline_recipe_ref unless run_kind is "
219
238
  "provisioning.bootstrap"
220
239
  )
240
+ if workflow_recipe_ref and not _is_workflow_recipe_ref(workflow_recipe_ref):
241
+ raise ValueError("workflow_recipe_ref must start with recipe://workflow-recipe-v1/")
242
+ if pipeline_recipe_ref and not _is_pipeline_recipe_ref(pipeline_recipe_ref):
243
+ raise ValueError("pipeline_recipe_ref must start with recipe://pipeline-recipe-v1/")
221
244
  if bootstrap:
222
245
  target_slug = (provisioning_target_slug or "").strip()
223
246
  if not target_slug:
@@ -236,8 +259,6 @@ class AuthorityNamespace:
236
259
  "run_kind": run_kind,
237
260
  "bootstrap_profile_ref": bootstrap_profile_ref,
238
261
  "provisioning_target_slug": provisioning_target_slug,
239
- "workflow_id": workflow_id,
240
- "workflow_ref": workflow_ref,
241
262
  "instance_id": instance_id,
242
263
  "instance_key": instance_key,
243
264
  "group": group,
@@ -262,8 +283,8 @@ class AuthorityNamespace:
262
283
  })
263
284
  else:
264
285
  body = _compact({
265
- "workflow_id": workflow_id,
266
- "workflow_ref": workflow_ref,
286
+ "workflow_recipe_ref": workflow_recipe_ref,
287
+ "pipeline_recipe_ref": pipeline_recipe_ref,
267
288
  "instance_id": instance_id,
268
289
  "instance_key": instance_key,
269
290
  "group": group,
@@ -41,14 +41,24 @@ class RecipesNamespace:
41
41
  def __init__(self, client: "Dominus"):
42
42
  self._client = client
43
43
 
44
- async def _post(self, endpoint: str, body: Optional[Dict[str, Any]] = None, *, timeout: float = 30.0) -> Dict[str, Any]:
45
- return await self._client._request(
46
- endpoint=endpoint,
47
- method="POST",
48
- body=body or {},
49
- use_gateway=True,
50
- timeout=timeout,
51
- )
44
+ async def _post(
45
+ self,
46
+ endpoint: str,
47
+ body: Optional[Dict[str, Any]] = None,
48
+ *,
49
+ timeout: float = 30.0,
50
+ actor_context: Optional[Dict[str, str]] = None,
51
+ ) -> Dict[str, Any]:
52
+ kwargs: Dict[str, Any] = {
53
+ "endpoint": endpoint,
54
+ "method": "POST",
55
+ "body": body or {},
56
+ "use_gateway": True,
57
+ "timeout": timeout,
58
+ }
59
+ if actor_context is not None:
60
+ kwargs["actor"] = actor_context
61
+ return await self._client._request(**kwargs)
52
62
 
53
63
  async def _get(self, endpoint: str, *, timeout: float = 30.0) -> Dict[str, Any]:
54
64
  return await self._client._request(
@@ -75,6 +85,7 @@ class RecipesNamespace:
75
85
  name: str,
76
86
  body: str,
77
87
  description: Optional[str] = None,
88
+ actor_context: Optional[Dict[str, str]] = None,
78
89
  timeout: float = 30.0,
79
90
  ) -> Dict[str, Any]:
80
91
  """Publish a recipe. ``POST /api/recipe/recipes/publish``."""
@@ -87,6 +98,7 @@ class RecipesNamespace:
87
98
  "body": body,
88
99
  "description": description,
89
100
  }),
101
+ actor_context=actor_context,
90
102
  timeout=timeout,
91
103
  )
92
104
 
@@ -112,6 +124,7 @@ class RecipesNamespace:
112
124
  name: str,
113
125
  version: int,
114
126
  reason: Optional[str] = None,
127
+ actor_context: Optional[Dict[str, str]] = None,
115
128
  timeout: float = 30.0,
116
129
  ) -> Dict[str, Any]:
117
130
  """Mark the current head recipe version deprecated. ``POST /api/recipe/recipes/deprecate``."""
@@ -124,6 +137,7 @@ class RecipesNamespace:
124
137
  "version": version,
125
138
  "reason": reason,
126
139
  }),
140
+ actor_context=actor_context,
127
141
  timeout=timeout,
128
142
  )
129
143
 
@@ -170,6 +184,7 @@ class RecipesNamespace:
170
184
  owning_worker: str,
171
185
  schema_version: int,
172
186
  json_schema: Dict[str, Any],
187
+ actor_context: Optional[Dict[str, str]] = None,
173
188
  timeout: float = 15.0,
174
189
  ) -> Dict[str, Any]:
175
190
  """Register a recipe type. ``POST /api/recipe/types/register``.
@@ -184,5 +199,6 @@ class RecipesNamespace:
184
199
  "schema_version": schema_version,
185
200
  "json_schema": json_schema,
186
201
  },
202
+ actor_context=actor_context,
187
203
  timeout=timeout,
188
204
  )
@@ -33,9 +33,9 @@ Usage:
33
33
  tools = await dominus.workflow.list_tools(limit=50)
34
34
  await dominus.workflow.register_tool({"slug": "my-tool", "name": "My Tool", "tool_type": "http"})
35
35
 
36
- # Canonical saved-workflow launch (Authority POST /api/authority/runs/ensure)
36
+ # Canonical recipe-backed launch (Authority POST /api/authority/runs/ensure)
37
37
  execution = await dominus.workflow.ensure(
38
- "my-workflow-id",
38
+ workflow_recipe_ref="recipe://workflow-recipe-v1/my-workflow@head",
39
39
  subject="PCM47474562",
40
40
  company="acme",
41
41
  inputs={"key": "value"},
@@ -58,7 +58,7 @@ class WorkflowNamespace:
58
58
  Workflow management namespace.
59
59
 
60
60
  Provides operations for workflow CRUD, pipelines, templates,
61
- and the saved-workflow run lifecycle via the dominus-workflow-manager service.
61
+ and the recipe-backed run lifecycle via Authority.
62
62
  """
63
63
 
64
64
  def __init__(self, client: "Dominus"):
@@ -92,17 +92,12 @@ class WorkflowNamespace:
92
92
  return endpoint
93
93
 
94
94
  @staticmethod
95
- def _workflow_identifier_body(
96
- workflow_id: Optional[str] = None,
97
- workflow_ref: Optional[str] = None,
98
- ) -> Dict[str, Any]:
99
- if workflow_ref and str(workflow_ref).strip():
100
- return {"workflow_ref": str(workflow_ref).strip()}
101
- if WorkflowNamespace._is_workflow_ref(workflow_id):
102
- return {"workflow_ref": str(workflow_id).strip()}
103
- if workflow_id and str(workflow_id).strip():
104
- return {"workflow_id": str(workflow_id).strip()}
105
- raise ValueError("workflow_id or workflow_ref is required")
95
+ def _is_workflow_recipe_ref(value: Optional[str]) -> bool:
96
+ return str(value or "").strip().startswith("recipe://workflow-recipe-v1/")
97
+
98
+ @staticmethod
99
+ def _is_pipeline_recipe_ref(value: Optional[str]) -> bool:
100
+ return str(value or "").strip().startswith("recipe://pipeline-recipe-v1/")
106
101
 
107
102
  @staticmethod
108
103
  def _instance_base_endpoint(workflow_id: str) -> str:
@@ -210,11 +205,14 @@ class WorkflowNamespace:
210
205
 
211
206
  def _build_authority_ensure_body(
212
207
  self,
213
- workflow_id: str,
208
+ recipe_ref: Optional[str] = None,
214
209
  *,
215
- workflow_ref: Optional[str] = None,
210
+ workflow_recipe_ref: Optional[str] = None,
211
+ pipeline_recipe_ref: Optional[str] = None,
216
212
  subject: Optional[str] = None,
217
213
  company: Optional[str] = None,
214
+ group: Optional[str] = None,
215
+ owner: Optional[str] = None,
218
216
  inputs: Optional[Dict[str, Any]] = None,
219
217
  context: Optional[Dict[str, Any]] = None,
220
218
  bindings: Optional[Dict[str, Any]] = None,
@@ -227,16 +225,37 @@ class WorkflowNamespace:
227
225
  idempotency_key: Optional[str] = None,
228
226
  ) -> Dict[str, Any]:
229
227
  body: Dict[str, Any] = {"mode": mode}
230
- if workflow_ref:
231
- body["workflow_ref"] = workflow_ref
232
- elif self._is_workflow_ref(workflow_id):
233
- body["workflow_ref"] = workflow_id
234
- else:
235
- body["workflow_id"] = workflow_id
228
+ launch_sources: List[tuple[str, str]] = []
229
+ normalized_recipe_ref = str(recipe_ref or "").strip()
230
+ if workflow_recipe_ref and str(workflow_recipe_ref).strip():
231
+ launch_sources.append(("workflow_recipe_ref", str(workflow_recipe_ref).strip()))
232
+ if pipeline_recipe_ref and str(pipeline_recipe_ref).strip():
233
+ launch_sources.append(("pipeline_recipe_ref", str(pipeline_recipe_ref).strip()))
234
+ if normalized_recipe_ref:
235
+ if self._is_workflow_recipe_ref(normalized_recipe_ref):
236
+ launch_sources.append(("workflow_recipe_ref", normalized_recipe_ref))
237
+ elif self._is_pipeline_recipe_ref(normalized_recipe_ref):
238
+ launch_sources.append(("pipeline_recipe_ref", normalized_recipe_ref))
239
+ else:
240
+ raise ValueError(
241
+ "recipe_ref must start with recipe://workflow-recipe-v1/ "
242
+ "or recipe://pipeline-recipe-v1/"
243
+ )
244
+ if len(launch_sources) != 1:
245
+ raise ValueError(
246
+ "ensure requires exactly one of recipe_ref, workflow_recipe_ref, "
247
+ "or pipeline_recipe_ref"
248
+ )
249
+ field, value = launch_sources[0]
250
+ body[field] = value
236
251
  if subject is not None:
237
252
  body["subject"] = subject
238
253
  if company is not None:
239
254
  body["company"] = company
255
+ if group is not None:
256
+ body["group"] = group
257
+ if owner is not None:
258
+ body["owner"] = owner
240
259
  if inputs:
241
260
  body["inputs"] = inputs
242
261
  if context:
@@ -855,7 +874,7 @@ class WorkflowNamespace:
855
874
 
856
875
  async def ensure(
857
876
  self,
858
- workflow_id: str,
877
+ recipe_ref: Optional[str] = None,
859
878
  *,
860
879
  instance_id: Optional[str] = None,
861
880
  group: Optional[str] = None,
@@ -865,7 +884,8 @@ class WorkflowNamespace:
865
884
  bindings: Optional[Dict[str, Any]] = None,
866
885
  mode: str = "blocking",
867
886
  context: Optional[Dict[str, Any]] = None,
868
- workflow_ref: Optional[str] = None,
887
+ workflow_recipe_ref: Optional[str] = None,
888
+ pipeline_recipe_ref: Optional[str] = None,
869
889
  subject: Optional[str] = None,
870
890
  company: Optional[str] = None,
871
891
  inputs: Optional[Dict[str, Any]] = None,
@@ -884,10 +904,13 @@ class WorkflowNamespace:
884
904
  return await self._api(
885
905
  endpoint="/api/authority/runs/ensure",
886
906
  body=self._build_authority_ensure_body(
887
- workflow_id,
888
- workflow_ref=workflow_ref,
907
+ recipe_ref,
908
+ workflow_recipe_ref=workflow_recipe_ref,
909
+ pipeline_recipe_ref=pipeline_recipe_ref,
889
910
  subject=subject,
890
911
  company=company,
912
+ group=group,
913
+ owner=owner,
891
914
  inputs=inputs,
892
915
  context=context,
893
916
  bindings=bindings,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dominus-sdk-python
3
- Version: 5.0.0
3
+ Version: 6.0.0
4
4
  Summary: Python SDK for the Dominus gateway-first platform
5
5
  Author-email: CareBridge Systems <dev@carebridge.io>
6
6
  License: Proprietary
@@ -43,7 +43,7 @@ Async Python SDK for the Dominus gateway-first service plane.
43
43
  - Gateway-scoped client mode for MCP and other user-JWT sessions
44
44
  - Transport compatibility for wrapped `{success,data}` responses and unwrapped Warden/control-plane success objects
45
45
  - Local helpers for JWT verification, trace propagation, retries, and console capture
46
- - Current package version: `4.0.8`
46
+ - Current package version: `6.0.0`
47
47
 
48
48
  ## Install
49
49
 
@@ -66,7 +66,7 @@ users = await dominus.db.query("users", filters={"status": "active"})
66
66
  await dominus.redis.set("session:123", {"user": "john"}, ttl=3600)
67
67
 
68
68
  run = await dominus.workflow.ensure(
69
- "wf://carebridge/report-cycle",
69
+ workflow_recipe_ref="recipe://workflow-recipe-v1/report-cycle@v3",
70
70
  subject="PCM47474562",
71
71
  company="summit-radiology",
72
72
  )
@@ -193,7 +193,7 @@ JWT and selected scope headers directly through Gateway.
193
193
  | `health` | Gateway | Health and ping helpers |
194
194
  | `admin` | Admin Worker | Admin category reseed/reset |
195
195
  | `ai` | Agent Runtime | Agent, completion, RAG, artifacts, results, raw orchestration |
196
- | `workflow` | Workflow Manager + Authority | Saved workflow CRUD and canonical run lifecycle |
196
+ | `workflow` | Workflow Manager + Authority | Saved workflow CRUD and recipe-backed run lifecycle |
197
197
  | `artifacts` | Artifact Worker | Addressed V2 artifacts, bookmarks, watches |
198
198
  | `jobs` | Job Worker | Enqueue, poll, dead-letter management |
199
199
  | `processor` | Processor | Batch and single-job processing |
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dominus-sdk-python"
7
- version = "5.0.0"
7
+ version = "6.0.0"
8
8
  description = "Python SDK for the Dominus gateway-first platform"
9
9
  readme = "README.md"
10
10
  license = {text = "Proprietary"}
@@ -86,10 +86,29 @@ def test_authority_scope_signatures_use_new_public_vocabulary():
86
86
  schedule_create_sig = inspect.signature(AuthorityNamespace.create_schedule)
87
87
  mint_sig = inspect.signature(core_helpers.mint_selected_scope_jwt)
88
88
 
89
- for name in ("app_slug", "env", "target_org_id", "target_env", "run_kind", "target_app_slug", "bootstrap_profile_ref", "provisioning_target_slug"):
89
+ for name in (
90
+ "app_slug",
91
+ "env",
92
+ "target_org_id",
93
+ "target_env",
94
+ "run_kind",
95
+ "target_app_slug",
96
+ "bootstrap_profile_ref",
97
+ "provisioning_target_slug",
98
+ "workflow_recipe_ref",
99
+ "pipeline_recipe_ref",
100
+ ):
90
101
  assert name in ensure_sig.parameters
91
102
  old_profile_key = "bootstrap" + "_profile"
92
- for legacy in ("project_slug", "environment", "target_project_id", "target_environment", old_profile_key):
103
+ for legacy in (
104
+ "project_slug",
105
+ "environment",
106
+ "target_project_id",
107
+ "target_environment",
108
+ "workflow_id",
109
+ "workflow_ref",
110
+ old_profile_key,
111
+ ):
93
112
  assert legacy not in ensure_sig.parameters
94
113
 
95
114
  for name in ("shared_app_slug", "target_app_slug", "target_env", "bootstrap_profile_ref"):
@@ -135,7 +154,7 @@ async def test_authority_scope_methods_use_canonical_context_wire_names():
135
154
  namespace = AuthorityNamespace(client)
136
155
 
137
156
  await namespace.ensure_run(
138
- workflow_id="wf-123",
157
+ workflow_recipe_ref="recipe://workflow-recipe-v1/report-cycle@head",
139
158
  app_slug="carebridge",
140
159
  env="production",
141
160
  target_org_id="org-123",
@@ -255,7 +274,10 @@ async def test_authority_scope_methods_use_canonical_context_wire_names():
255
274
  assert ensure_body["env"] == "production"
256
275
  assert ensure_body["target_org_id"] == "org-123"
257
276
  assert ensure_body["target_env"] == "production"
277
+ assert ensure_body["workflow_recipe_ref"] == "recipe://workflow-recipe-v1/report-cycle@head"
258
278
  assert "bootstrap_profile_ref" not in ensure_body
279
+ assert "workflow_id" not in ensure_body
280
+ assert "workflow_ref" not in ensure_body
259
281
 
260
282
  bootstrap_body = client.calls[1]["body"]
261
283
  assert bootstrap_body["shared_app_slug"] == "shared-core"
@@ -478,6 +500,24 @@ async def test_authority_ensure_run_provisioning_bootstrap_omits_workflow_mode()
478
500
  assert "mode" not in body
479
501
 
480
502
 
503
+ @pytest.mark.asyncio
504
+ async def test_authority_ensure_run_requires_recipe_launch_ref():
505
+ client = FakeClient()
506
+ namespace = AuthorityNamespace(client)
507
+
508
+ with pytest.raises(ValueError, match="workflow_recipe_ref or pipeline_recipe_ref"):
509
+ await namespace.ensure_run(app_slug="carebridge", env="production")
510
+
511
+ with pytest.raises(ValueError, match="recipe://workflow-recipe-v1/"):
512
+ await namespace.ensure_run(
513
+ workflow_recipe_ref="wf://carebridge/report-cycle",
514
+ app_slug="carebridge",
515
+ env="production",
516
+ )
517
+
518
+ assert client.calls == []
519
+
520
+
481
521
  @pytest.mark.asyncio
482
522
  async def test_authority_ensure_run_bootstrap_requires_provisioning_target_slug():
483
523
  client = FakeClient()
@@ -47,3 +47,39 @@ async def test_recipes_namespace_maps_deprecate_to_gateway_route(monkeypatch, sd
47
47
  "timeout": 30.0,
48
48
  }
49
49
  ]
50
+
51
+
52
+ @pytest.mark.asyncio
53
+ async def test_recipes_namespace_passes_actor_context_on_publish(monkeypatch, sdk):
54
+ calls = []
55
+
56
+ async def fake_request(**kwargs):
57
+ calls.append(kwargs)
58
+ return {"ok": True, "recipe": {"version": 1}}
59
+
60
+ monkeypatch.setattr(sdk, "_request", fake_request)
61
+
62
+ actor = {"type": "user", "id": "user-1"}
63
+ await sdk.recipes.publish(
64
+ type="browser-recipe",
65
+ tier="project",
66
+ name="cms-login",
67
+ body="version: browser-recipe-v1\nsteps: []\n",
68
+ actor_context=actor,
69
+ )
70
+
71
+ assert calls == [
72
+ {
73
+ "endpoint": "/api/recipe/recipes/publish",
74
+ "method": "POST",
75
+ "body": {
76
+ "type": "browser-recipe",
77
+ "tier": "project",
78
+ "name": "cms-login",
79
+ "body": "version: browser-recipe-v1\nsteps: []\n",
80
+ },
81
+ "use_gateway": True,
82
+ "timeout": 30.0,
83
+ "actor": actor,
84
+ }
85
+ ]
@@ -255,7 +255,7 @@ async def test_workflow_ensure_supports_authority_one_call_lifecycle():
255
255
  namespace = WorkflowNamespace(client)
256
256
 
257
257
  await namespace.ensure(
258
- "wf://carebridge/report-cycle",
258
+ workflow_recipe_ref="recipe://workflow-recipe-v1/report-cycle@head",
259
259
  subject="PCM47474562",
260
260
  company="summit-radiology",
261
261
  inputs={"report_snapshot": "ar://artifact"},
@@ -265,7 +265,7 @@ async def test_workflow_ensure_supports_authority_one_call_lifecycle():
265
265
 
266
266
  assert client.calls[0]["endpoint"] == "/api/authority/runs/ensure"
267
267
  body = client.calls[0]["body"]
268
- assert body["workflow_ref"] == "wf://carebridge/report-cycle"
268
+ assert body["workflow_recipe_ref"] == "recipe://workflow-recipe-v1/report-cycle@head"
269
269
  assert body["subject"] == "PCM47474562"
270
270
  assert body["company"] == "summit-radiology"
271
271
  assert body["inputs"] == {"report_snapshot": "ar://artifact"}
@@ -274,6 +274,23 @@ async def test_workflow_ensure_supports_authority_one_call_lifecycle():
274
274
  assert isinstance(body["idempotency_key"], str) and body["idempotency_key"]
275
275
 
276
276
 
277
+ @pytest.mark.asyncio
278
+ async def test_workflow_ensure_can_infer_recipe_ref_from_first_argument():
279
+ client = FakeClient()
280
+ namespace = WorkflowNamespace(client)
281
+
282
+ await namespace.ensure(
283
+ "recipe://workflow-recipe-v1/report-cycle@v3",
284
+ company="summit-radiology",
285
+ mode="async",
286
+ )
287
+
288
+ body = client.calls[0]["body"]
289
+ assert body["workflow_recipe_ref"] == "recipe://workflow-recipe-v1/report-cycle@v3"
290
+ assert body["company"] == "summit-radiology"
291
+ assert "workflow_id" not in body
292
+
293
+
277
294
  @pytest.mark.asyncio
278
295
  async def test_workflow_get_detects_authority_run_ids():
279
296
  client = FakeClient()
@@ -416,24 +433,24 @@ async def test_saved_workflow_instance_ensure_contract_matches_workflow_manager(
416
433
 
417
434
 
418
435
  @pytest.mark.asyncio
419
- async def test_saved_workflow_ensure_is_authority_only_and_stream_guard():
436
+ async def test_recipe_workflow_ensure_is_authority_only_and_stream_guard():
420
437
  client = FakeClient()
421
438
  namespace = WorkflowNamespace(client)
422
439
 
423
440
  await namespace.ensure(
424
- "wf-instance",
441
+ workflow_recipe_ref="recipe://workflow-recipe-v1/report-cycle@head",
425
442
  mode="ensure_only",
426
443
  )
427
444
 
428
445
  call = client.calls[0]
429
446
  assert call["endpoint"] == "/api/authority/runs/ensure"
430
- assert call["body"]["workflow_id"] == "wf-instance"
447
+ assert call["body"]["workflow_recipe_ref"] == "recipe://workflow-recipe-v1/report-cycle@head"
431
448
  assert call["body"]["mode"] == "ensure_only"
432
449
  assert isinstance(call["body"]["idempotency_key"], str)
433
450
 
434
451
  with pytest.raises(ValueError, match="Authority-only and supports blocking, async, or ensure_only"):
435
452
  await namespace.ensure(
436
- "wf-instance",
453
+ workflow_recipe_ref="recipe://workflow-recipe-v1/report-cycle@head",
437
454
  mode="streaming",
438
455
  )
439
456
 
@@ -56,24 +56,62 @@ async def test_workflow_get_accepts_workflow_ref():
56
56
 
57
57
 
58
58
  @pytest.mark.asyncio
59
- async def test_workflow_ensure_accepts_workflow_ref():
59
+ async def test_workflow_ensure_rejects_legacy_workflow_ref():
60
+ client = FakeClient()
61
+ namespace = WorkflowNamespace(client)
62
+
63
+ with pytest.raises(ValueError, match="recipe_ref"):
64
+ await namespace.ensure(
65
+ "wf://carebridge-platform/production/shared/patient-intake",
66
+ subject="sub-1",
67
+ company="co-1",
68
+ instance_id="run-123",
69
+ context={"report_ref": {"report_id": "r-1"}},
70
+ mode="async",
71
+ )
72
+
73
+ assert client.calls == []
74
+
75
+
76
+ @pytest.mark.asyncio
77
+ async def test_workflow_ensure_accepts_recipe_refs():
60
78
  client = FakeClient()
61
79
  namespace = WorkflowNamespace(client)
62
80
 
63
81
  await namespace.ensure(
64
- "wf://carebridge-platform/production/shared/patient-intake",
82
+ workflow_recipe_ref="recipe://workflow-recipe-v1/patient-intake@v4",
65
83
  subject="sub-1",
66
84
  company="co-1",
67
- instance_id="run-123",
85
+ mode="async",
86
+ )
87
+ await namespace.ensure(
88
+ pipeline_recipe_ref="recipe://pipeline-recipe-v1/intake-pipeline@v2",
68
89
  context={"report_ref": {"report_id": "r-1"}},
69
90
  mode="async",
70
91
  )
71
92
 
72
- assert client.calls[0]["endpoint"] == "/api/authority/runs/ensure"
73
- body = client.calls[0]["body"]
74
- assert body["workflow_ref"] == "wf://carebridge-platform/production/shared/patient-intake"
75
- assert body["instance_id"] == "run-123"
76
- assert body["context"] == {"report_ref": {"report_id": "r-1"}}
93
+ first = client.calls[0]["body"]
94
+ assert first["workflow_recipe_ref"] == "recipe://workflow-recipe-v1/patient-intake@v4"
95
+ assert first["subject"] == "sub-1"
96
+ assert "workflow_id" not in first
97
+ second = client.calls[1]["body"]
98
+ assert second["pipeline_recipe_ref"] == "recipe://pipeline-recipe-v1/intake-pipeline@v2"
99
+ assert second["context"] == {"report_ref": {"report_id": "r-1"}}
100
+ assert "workflow_ref" not in second
101
+
102
+
103
+ @pytest.mark.asyncio
104
+ async def test_workflow_ensure_rejects_mixed_recipe_refs():
105
+ client = FakeClient()
106
+ namespace = WorkflowNamespace(client)
107
+
108
+ with pytest.raises(ValueError, match="exactly one"):
109
+ await namespace.ensure(
110
+ "recipe://pipeline-recipe-v1/intake-pipeline@v2",
111
+ workflow_recipe_ref="recipe://workflow-recipe-v1/patient-intake@v4",
112
+ )
113
+
114
+ assert client.calls == []
77
115
 
78
116
 
79
117
  @pytest.mark.asyncio
@@ -125,7 +163,7 @@ def test_artifact_ref_helpers_round_trip():
125
163
  }
126
164
 
127
165
 
128
- def test_artifact_ref_helpers_support_v2_and_display_refs():
166
+ def test_artifact_ref_helpers_support_v2_refs():
129
167
  v2_ref = build_artifact_ref(
130
168
  group="carebridge",
131
169
  owner="carebridge-summit",