dominus-sdk-python 9.0.5__tar.gz → 9.0.7__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 (81) hide show
  1. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/PKG-INFO +4 -4
  2. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/README.md +3 -3
  3. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/__init__.py +28 -5
  4. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/namespaces/artifacts.py +7 -1
  5. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/namespaces/authority.py +54 -1
  6. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/namespaces/browser.py +6 -1
  7. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/namespaces/stash.py +20 -1
  8. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/start.py +2 -2
  9. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus_sdk_python.egg-info/PKG-INFO +4 -4
  10. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus_sdk_python.egg-info/SOURCES.txt +1 -0
  11. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/pyproject.toml +1 -1
  12. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/tests/test_authority_public_vocabulary.py +94 -0
  13. dominus_sdk_python-9.0.7/tests/test_errors_aliases.py +277 -0
  14. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/tests/test_stash_artifact_facade.py +69 -0
  15. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/config/__init__.py +0 -0
  16. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/config/endpoints.py +0 -0
  17. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/contract/__init__.py +0 -0
  18. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/contract/envelope.py +0 -0
  19. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/contract/projections.py +0 -0
  20. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/errors.py +0 -0
  21. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/helpers/__init__.py +0 -0
  22. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/helpers/auth.py +0 -0
  23. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/helpers/cache.py +0 -0
  24. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/helpers/console_capture.py +0 -0
  25. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/helpers/core.py +0 -0
  26. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/helpers/crypto.py +0 -0
  27. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/helpers/sse.py +0 -0
  28. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/helpers/trace.py +0 -0
  29. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/namespaces/__init__.py +0 -0
  30. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/namespaces/admin.py +0 -0
  31. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/namespaces/ai.py +0 -0
  32. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/namespaces/auth.py +0 -0
  33. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/namespaces/coder.py +0 -0
  34. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/namespaces/courier.py +0 -0
  35. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/namespaces/db.py +0 -0
  36. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/namespaces/ddl.py +0 -0
  37. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/namespaces/deployer.py +0 -0
  38. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/namespaces/fastapi.py +0 -0
  39. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/namespaces/files.py +0 -0
  40. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/namespaces/health.py +0 -0
  41. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/namespaces/jobs.py +0 -0
  42. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/namespaces/logs.py +0 -0
  43. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/namespaces/platform.py +0 -0
  44. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/namespaces/portal.py +0 -0
  45. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/namespaces/processor.py +0 -0
  46. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/namespaces/publisher.py +0 -0
  47. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/namespaces/recipes.py +0 -0
  48. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/namespaces/redis.py +0 -0
  49. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/namespaces/secrets.py +0 -0
  50. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/namespaces/secure.py +0 -0
  51. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/namespaces/sync.py +0 -0
  52. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/namespaces/warden.py +0 -0
  53. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/namespaces/workflow.py +0 -0
  54. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/py.typed +0 -0
  55. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus/services/__init__.py +0 -0
  56. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus_sdk_python.egg-info/dependency_links.txt +0 -0
  57. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus_sdk_python.egg-info/requires.txt +0 -0
  58. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/dominus_sdk_python.egg-info/top_level.txt +0 -0
  59. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/setup.cfg +0 -0
  60. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/tests/test_auth.py +0 -0
  61. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/tests/test_browser_namespace.py +0 -0
  62. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/tests/test_contract.py +0 -0
  63. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/tests/test_control_plane_namespaces.py +0 -0
  64. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/tests/test_error_recovery_action.py +0 -0
  65. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/tests/test_errors.py +0 -0
  66. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/tests/test_flat_commands.py +0 -0
  67. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/tests/test_health.py +0 -0
  68. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/tests/test_logs.py +0 -0
  69. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/tests/test_namespace_barrel_exports.py +0 -0
  70. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/tests/test_platform_coder_namespaces.py +0 -0
  71. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/tests/test_provisioning_parity.py +0 -0
  72. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/tests/test_public_exports.py +0 -0
  73. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/tests/test_publisher_namespace.py +0 -0
  74. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/tests/test_readme_quickstart_catalog.py +0 -0
  75. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/tests/test_recipes_namespace.py +0 -0
  76. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/tests/test_recipes_stash_routing.py +0 -0
  77. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/tests/test_secrets_presence_contract.py +0 -0
  78. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/tests/test_stash_tables_namespace.py +0 -0
  79. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/tests/test_transport_compat.py +0 -0
  80. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/tests/test_workflow_lifecycle.py +0 -0
  81. {dominus_sdk_python-9.0.5 → dominus_sdk_python-9.0.7}/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.5
3
+ Version: 9.0.7
4
4
  Summary: Python SDK for the Dominus gateway-first platform
5
5
  Author-email: CareBridge Systems <dev@carebridge.io>
6
6
  License-Expression: LicenseRef-Proprietary
@@ -38,8 +38,8 @@ Async Python SDK for the Dominus gateway-first service plane.
38
38
  ## Agent Guide
39
39
 
40
40
  Start with `docs/agent-guide/INDEX.md`. The current snapshot is
41
- `docs/agent-guide/2026-08-04-sdk-python/00-reading-order.md`; the latest
42
- cleanup audit is `docs/janitor/2026-08-04-sdk-python-cleanup-audit.md`.
41
+ `docs/agent-guide/2026-09-03-0942-sdk-python/00-reading-order.md`; the latest
42
+ cleanup audit is `docs/janitor/2026-09-03-0942-sdk-python-cleanup-audit.md`.
43
43
 
44
44
  ## What This Repo Ships
45
45
 
@@ -48,7 +48,7 @@ cleanup audit is `docs/janitor/2026-08-04-sdk-python-cleanup-audit.md`.
48
48
  - Gateway-scoped client mode for MCP and other user-JWT sessions
49
49
  - Transport compatibility for wrapped `{success,data}` responses and unwrapped Warden/control-plane success objects
50
50
  - Local helpers for JWT verification, trace propagation, retries, and console capture
51
- - Current package version: `9.0.5`
51
+ - Current package version: `9.0.6`
52
52
 
53
53
  ## Install
54
54
 
@@ -5,8 +5,8 @@ Async Python SDK for the Dominus gateway-first service plane.
5
5
  ## Agent Guide
6
6
 
7
7
  Start with `docs/agent-guide/INDEX.md`. The current snapshot is
8
- `docs/agent-guide/2026-08-04-sdk-python/00-reading-order.md`; the latest
9
- cleanup audit is `docs/janitor/2026-08-04-sdk-python-cleanup-audit.md`.
8
+ `docs/agent-guide/2026-09-03-0942-sdk-python/00-reading-order.md`; the latest
9
+ cleanup audit is `docs/janitor/2026-09-03-0942-sdk-python-cleanup-audit.md`.
10
10
 
11
11
  ## What This Repo Ships
12
12
 
@@ -15,7 +15,7 @@ cleanup audit is `docs/janitor/2026-08-04-sdk-python-cleanup-audit.md`.
15
15
  - Gateway-scoped client mode for MCP and other user-JWT sessions
16
16
  - Transport compatibility for wrapped `{success,data}` responses and unwrapped Warden/control-plane success objects
17
17
  - Local helpers for JWT verification, trace propagation, retries, and console capture
18
- - Current package version: `9.0.5`
18
+ - Current package version: `9.0.6`
19
19
 
20
20
  ## Install
21
21
 
@@ -160,12 +160,24 @@ from .helpers.trace import (
160
160
  # Export error classes
161
161
  from .errors import (
162
162
  DominusError,
163
+ # v1 aliases (preserved — deprecated in v2, removed in v3 per ADR 0010)
163
164
  AuthenticationError,
164
165
  AuthorizationError,
165
- NotFoundError,
166
166
  ValidationError,
167
- ConflictError,
168
167
  ServiceError,
168
+ # Per-kind canonical subclasses (Seams fork 6, ADR 0010)
169
+ UnauthorizedError,
170
+ ForbiddenError,
171
+ NotFoundError,
172
+ ValidationFailedError,
173
+ ConflictError,
174
+ RateLimitedError,
175
+ UpstreamUnavailableError,
176
+ InternalError,
177
+ TimeoutError,
178
+ CanceledError,
179
+ UnsupportedVersionError,
180
+ # Stash subclasses
169
181
  StashBookmarkArtifactNotFound,
170
182
  StashBookmarkInvalidVersionRef,
171
183
  StashBookmarkDuplicateName,
@@ -178,7 +190,7 @@ from .errors import (
178
190
  TimeoutError as DominusTimeoutError,
179
191
  )
180
192
 
181
- __version__ = "9.0.5"
193
+ __version__ = "9.0.6"
182
194
  __all__ = [
183
195
  # Main SDK instance
184
196
  "dominus",
@@ -262,12 +274,23 @@ __all__ = [
262
274
  "with_trace",
263
275
  # Error classes
264
276
  "DominusError",
277
+ # v1 aliases (preserved)
265
278
  "AuthenticationError",
266
279
  "AuthorizationError",
267
- "NotFoundError",
268
280
  "ValidationError",
269
- "ConflictError",
270
281
  "ServiceError",
282
+ # Per-kind canonical subclasses
283
+ "UnauthorizedError",
284
+ "ForbiddenError",
285
+ "NotFoundError",
286
+ "ValidationFailedError",
287
+ "ConflictError",
288
+ "RateLimitedError",
289
+ "UpstreamUnavailableError",
290
+ "InternalError",
291
+ "TimeoutError",
292
+ "CanceledError",
293
+ "UnsupportedVersionError",
271
294
  "StashBookmarkArtifactNotFound",
272
295
  "StashBookmarkInvalidVersionRef",
273
296
  "StashBookmarkDuplicateName",
@@ -1,5 +1,11 @@
1
1
  """
2
- Artifacts Namespace - Temporary artifact storage.
2
+ Artifacts Namespace - temporary and addressed artifact storage.
3
+
4
+ Positioning: this is the **escape hatch** for direct ``ar://`` addressed
5
+ artifact workflows — use it when you already hold a canonical ``ar://`` ref.
6
+ For new code, prefer a Stash ``artifact-v2``-backed kind (e.g. ``artifact-v1``)
7
+ via ``dominus.stash.*``, the primary storage surface (the Two-Layer Storage
8
+ Rule). These addressed helpers are a building block under Stash, not deprecated.
3
9
 
4
10
  Provides auto-tiered artifact storage (Redis < 1MB, B2 >= 1MB)
5
11
  with TTL-based expiration. Routes through gateway to artifact-worker.
@@ -48,6 +48,30 @@ 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
+ #: The only run kind ``GET /api/authority/runs`` surfaces. Authority rejects any
52
+ #: other value; ``bootstrap``/``deploy_rollout`` runs are read by run id.
53
+ LIST_RUN_KIND = "workflow"
54
+
55
+ #: Canonical control-result view. Authority still defaults to the legacy
56
+ #: top-level projection on the wire; this SDK never relies on that default.
57
+ RESULT_VIEW_BOUND = "bound"
58
+
59
+
60
+ def remote_command_outcome(payload: Mapping[str, Any]) -> Dict[str, Any]:
61
+ """Unwrap a canonical ``RemoteCommandResult`` to the remote outcome it carries.
62
+
63
+ The envelope's ``owner``/``bound_subject``/``provenance`` state who actually
64
+ performed the command; callers that only need the run summary use this, and
65
+ callers that need attribution read the envelope directly. A payload that is
66
+ not an envelope is returned unchanged so an upstream error body is never
67
+ replaced with an empty dict.
68
+ """
69
+ if isinstance(payload, Mapping) and "outcome" in payload and "bound_subject" in payload:
70
+ outcome = payload.get("outcome")
71
+ return dict(outcome) if isinstance(outcome, Mapping) else {}
72
+ return dict(payload)
73
+
74
+
51
75
  def _is_workflow_recipe_ref(value: Optional[str]) -> bool:
52
76
  return str(value or "").strip().startswith("recipe://workflow-recipe-v1/")
53
77
 
@@ -313,9 +337,22 @@ class AuthorityNamespace:
313
337
  timeout=self._http_timeout(timeout, 30.0),
314
338
  )
315
339
 
340
+ async def run_kinds(self, *, timeout: Optional[float] = None) -> Dict[str, Any]:
341
+ """Read the run-kind/capability matrix. ``GET /api/authority/run-kinds``.
342
+
343
+ Explicit capability discovery: callers ask which actions a kind supports
344
+ (and which kinds the run list surfaces) instead of guessing from run-id
345
+ prefixes.
346
+ """
347
+ return await self._get(
348
+ "/api/authority/run-kinds",
349
+ timeout=self._http_timeout(timeout, 30.0),
350
+ )
351
+
316
352
  async def list_runs(
317
353
  self,
318
354
  *,
355
+ kind: str = LIST_RUN_KIND,
319
356
  workflow_id: Optional[str] = None,
320
357
  status: Optional[str] = None,
321
358
  group: Optional[str] = None,
@@ -328,9 +365,20 @@ class AuthorityNamespace:
328
365
  offset: int = 0,
329
366
  timeout: Optional[float] = None,
330
367
  ) -> Dict[str, Any]:
331
- """List Authority-backed runs. ``GET /api/authority/runs``."""
368
+ """List Authority-backed workflow runs. ``GET /api/authority/runs``.
369
+
370
+ This list is workflow-kind only and says so: ``kind`` is sent explicitly
371
+ rather than inheriting a server default that reads like a universal
372
+ inventory. Use :meth:`run_kinds` to see which kinds are listable.
373
+ """
374
+ if str(kind).strip().lower() != LIST_RUN_KIND:
375
+ raise ValueError(
376
+ f"Authority lists only kind={LIST_RUN_KIND!r} runs; "
377
+ f"{kind!r} runs are read by run id via get_run()"
378
+ )
332
379
  tid, tev = self._target_query_params(target_org_id, target_env)
333
380
  qs = _query_string({
381
+ "kind": LIST_RUN_KIND,
334
382
  "workflow_id": workflow_id,
335
383
  "status": status,
336
384
  "group": group,
@@ -378,6 +426,7 @@ class AuthorityNamespace:
378
426
  "metadata": metadata,
379
427
  **self._initiator(initiator_type, initiator_id, idempotency_key),
380
428
  **self._scope(app_slug, env, target_org_id, target_env),
429
+ "result_view": RESULT_VIEW_BOUND,
381
430
  })
382
431
  return await self._post(
383
432
  f"/api/authority/runs/{quote(run_id, safe='')}/cancel",
@@ -422,6 +471,7 @@ class AuthorityNamespace:
422
471
  "metadata": metadata,
423
472
  **self._initiator(initiator_type, initiator_id, idempotency_key),
424
473
  **self._scope(app_slug, env, target_org_id, target_env),
474
+ "result_view": RESULT_VIEW_BOUND,
425
475
  })
426
476
  return await self._post(
427
477
  f"/api/authority/runs/{quote(run_id, safe='')}/retry",
@@ -459,6 +509,7 @@ class AuthorityNamespace:
459
509
  "metadata": metadata,
460
510
  **self._initiator(initiator_type, initiator_id, idempotency_key),
461
511
  **self._scope(app_slug, env, target_org_id, target_env),
512
+ "result_view": RESULT_VIEW_BOUND,
462
513
  })
463
514
  return await self._post(
464
515
  f"/api/authority/runs/{quote(run_id, safe='')}/nudge",
@@ -898,6 +949,7 @@ class AuthorityNamespace:
898
949
  region: Optional[str] = None,
899
950
  system: Optional[str] = None,
900
951
  github_mode: Optional[str] = None,
952
+ repo_template: Optional[str] = None,
901
953
  overwrite_existing: Optional[bool] = None,
902
954
  owner_email: Optional[str] = None,
903
955
  execution_mode: Optional[str] = None,
@@ -928,6 +980,7 @@ class AuthorityNamespace:
928
980
  "region": region,
929
981
  "system": system,
930
982
  "github_mode": github_mode,
983
+ "repo_template": repo_template,
931
984
  "overwrite_existing": overwrite_existing,
932
985
  "owner_email": owner_email,
933
986
  "execution_mode": execution_mode,
@@ -162,7 +162,12 @@ class BrowserNamespace:
162
162
  )
163
163
 
164
164
  async def get_run_status(self, run_id: str, *, timeout: float = 30.0) -> Dict[str, Any]:
165
- """Read browser run runtime state. ``GET /api/browser/runs/{run_id}/status``."""
165
+ """Read browser run runtime state. ``GET /api/browser/runs/{run_id}/status``.
166
+
167
+ The envelope may carry an additive ``result_stash_ref`` (Stash palette
168
+ ``artifact`` selector) alongside the legacy ``result_artifact_ref``; it is
169
+ present only when a best-effort Stash dual-write succeeded.
170
+ """
166
171
  if not run_id:
167
172
  raise ValueError("run_id is required")
168
173
  return await self._get(
@@ -1,5 +1,9 @@
1
1
  """
2
- Stash Namespace - per-scope durable items (credentials + configs).
2
+ Stash Namespace - the primary kind-addressed storage surface.
3
+
4
+ Store named-kind data here and let the kind registry pick the backend — you
5
+ address items by (kind, scope, key), not by physical storage. ``ar://``
6
+ addressed artifact workflows remain the escape hatch for foreign refs.
3
7
 
4
8
  Routes through ``/svc/stash/*`` on the Dominus gateway. The stash worker
5
9
  stores items in each project's ``stash.*`` schema and transparently falls
@@ -53,6 +57,18 @@ ManagedTableOperator = Literal[
53
57
  "after",
54
58
  ]
55
59
 
60
+ # Live stash kind backends. The kind registry dispatches each registered kind
61
+ # to exactly one of these; `artifact-v2` and `journal` are the newer additions
62
+ # that previously had no typed surface in this SDK.
63
+ StashBackend = Literal[
64
+ "db-row",
65
+ "b2-versioned-bytes",
66
+ "b2-pointer-text",
67
+ "redis-ttl",
68
+ "artifact-v2",
69
+ "journal",
70
+ ]
71
+
56
72
 
57
73
  class _ManagedTableFieldOptional(TypedDict, total=False):
58
74
  required: bool
@@ -536,6 +552,7 @@ class StashNamespace:
536
552
  self,
537
553
  *,
538
554
  kind: str,
555
+ backend: Optional[StashBackend] = None,
539
556
  is_sensitive: Optional[bool] = None,
540
557
  value_schema: Optional[Dict[str, Any]] = None,
541
558
  default_rotation_days: Optional[int] = None,
@@ -544,6 +561,8 @@ class StashNamespace:
544
561
  ) -> Dict[str, Any]:
545
562
  """Register or update a kind in stash.kind_registry. Admin-scoped."""
546
563
  body: Dict[str, Any] = {"kind": kind}
564
+ if backend is not None:
565
+ body["backend"] = backend
547
566
  if is_sensitive is not None:
548
567
  body["is_sensitive"] = is_sensitive
549
568
  if value_schema is not None:
@@ -472,7 +472,7 @@ class Dominus:
472
472
  if not isinstance(error_details, dict):
473
473
  error_details = {}
474
474
  for src in (error_payload, nested_err):
475
- for key in ("code", "category", "retryable", "trace_id", "request_id"):
475
+ for key in ("kind", "code", "category", "retryable", "trace_id", "request_id", "correlation_id", "cause"):
476
476
  if key in src and key not in error_details:
477
477
  error_details[key] = src[key]
478
478
  raise_for_status(
@@ -502,7 +502,7 @@ class Dominus:
502
502
  error_details = result.get("details")
503
503
  if not isinstance(error_details, dict):
504
504
  error_details = {}
505
- for key in ("code", "category", "retryable", "trace_id", "request_id"):
505
+ for key in ("kind", "code", "category", "retryable", "trace_id", "request_id", "correlation_id", "cause"):
506
506
  if key in result:
507
507
  error_details[key] = result[key]
508
508
  raise DominusError(str(error_msg), details=error_details, endpoint=endpoint)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dominus-sdk-python
3
- Version: 9.0.5
3
+ Version: 9.0.7
4
4
  Summary: Python SDK for the Dominus gateway-first platform
5
5
  Author-email: CareBridge Systems <dev@carebridge.io>
6
6
  License-Expression: LicenseRef-Proprietary
@@ -38,8 +38,8 @@ Async Python SDK for the Dominus gateway-first service plane.
38
38
  ## Agent Guide
39
39
 
40
40
  Start with `docs/agent-guide/INDEX.md`. The current snapshot is
41
- `docs/agent-guide/2026-08-04-sdk-python/00-reading-order.md`; the latest
42
- cleanup audit is `docs/janitor/2026-08-04-sdk-python-cleanup-audit.md`.
41
+ `docs/agent-guide/2026-09-03-0942-sdk-python/00-reading-order.md`; the latest
42
+ cleanup audit is `docs/janitor/2026-09-03-0942-sdk-python-cleanup-audit.md`.
43
43
 
44
44
  ## What This Repo Ships
45
45
 
@@ -48,7 +48,7 @@ cleanup audit is `docs/janitor/2026-08-04-sdk-python-cleanup-audit.md`.
48
48
  - Gateway-scoped client mode for MCP and other user-JWT sessions
49
49
  - Transport compatibility for wrapped `{success,data}` responses and unwrapped Warden/control-plane success objects
50
50
  - Local helpers for JWT verification, trace propagation, retries, and console capture
51
- - Current package version: `9.0.5`
51
+ - Current package version: `9.0.6`
52
52
 
53
53
  ## Install
54
54
 
@@ -59,6 +59,7 @@ tests/test_contract.py
59
59
  tests/test_control_plane_namespaces.py
60
60
  tests/test_error_recovery_action.py
61
61
  tests/test_errors.py
62
+ tests/test_errors_aliases.py
62
63
  tests/test_flat_commands.py
63
64
  tests/test_health.py
64
65
  tests/test_logs.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dominus-sdk-python"
7
- version = "9.0.5"
7
+ version = "9.0.7"
8
8
  description = "Python SDK for the Dominus gateway-first platform"
9
9
  readme = "README.md"
10
10
  license = "LicenseRef-Proprietary"
@@ -515,3 +515,97 @@ async def test_authority_ensure_run_bootstrap_requires_provisioning_target_slug(
515
515
  env="production",
516
516
  )
517
517
  assert client.calls == []
518
+
519
+
520
+ # --- Task 21: canonical run-kind discovery, honest list, bound control results --
521
+
522
+
523
+ @pytest.mark.asyncio
524
+ async def test_run_kinds_reads_the_explicit_capability_surface():
525
+ client = FakeClient()
526
+ ns = AuthorityNamespace(client)
527
+
528
+ await ns.run_kinds()
529
+
530
+ assert client.calls[0]["endpoint"] == "/api/authority/run-kinds"
531
+ assert client.calls[0]["method"] == "GET"
532
+
533
+
534
+ @pytest.mark.asyncio
535
+ async def test_list_runs_states_the_workflow_kind_it_actually_gets():
536
+ client = FakeClient()
537
+ ns = AuthorityNamespace(client)
538
+
539
+ await ns.list_runs(limit=5)
540
+
541
+ assert "kind=workflow" in client.calls[0]["endpoint"]
542
+
543
+
544
+ @pytest.mark.asyncio
545
+ async def test_list_runs_rejects_a_kind_authority_does_not_list():
546
+ client = FakeClient()
547
+ ns = AuthorityNamespace(client)
548
+
549
+ with pytest.raises(ValueError):
550
+ await ns.list_runs(kind="bootstrap")
551
+
552
+ assert client.calls == []
553
+
554
+
555
+ @pytest.mark.asyncio
556
+ async def test_run_control_calls_request_the_canonical_bound_result():
557
+ client = FakeClient()
558
+ ns = AuthorityNamespace(client)
559
+
560
+ await ns.cancel_run("run-1", reason="operator")
561
+ await ns.retry_run("run-1", reason="operator")
562
+ await ns.nudge_run("run-1", reason="operator")
563
+
564
+ assert [call["body"]["result_view"] for call in client.calls] == ["bound", "bound", "bound"]
565
+
566
+
567
+ def test_run_control_outcome_helper_preserves_owner_and_provenance():
568
+ from dominus.namespaces.authority import remote_command_outcome
569
+
570
+ envelope = {
571
+ "owner": "workflow-manager",
572
+ "bound_subject": {"kind": "workflow", "canonical_id": "wf-1"},
573
+ "operation": "cancel",
574
+ "outcome": {"run_id": "run-1", "status": "cancelled"},
575
+ "observed_at": "2026-08-24T00:00:00Z",
576
+ "provenance": {"request_id": "req_1", "trace_id": "tr_1"},
577
+ }
578
+
579
+ assert remote_command_outcome(envelope) == {"run_id": "run-1", "status": "cancelled"}
580
+ # A non-envelope payload is returned untouched rather than silently emptied.
581
+ assert remote_command_outcome({"run_id": "run-1"}) == {"run_id": "run-1"}
582
+
583
+
584
+ def test_bootstrap_signature_carries_repo_template():
585
+ bootstrap_sig = inspect.signature(AuthorityNamespace.bootstrap_provisioning_target)
586
+ assert "repo_template" in bootstrap_sig.parameters
587
+
588
+
589
+ @pytest.mark.asyncio
590
+ async def test_bootstrap_provisioning_target_carries_repo_template():
591
+ client = FakeClient()
592
+ namespace = AuthorityNamespace(client)
593
+ await namespace.bootstrap_provisioning_target(
594
+ "summit-radiology",
595
+ shared_app_slug="shared-core",
596
+ app_slug="carebridge",
597
+ env="production",
598
+ repo_template="dominus-fastapi-template",
599
+ )
600
+ assert client.calls[-1]["body"]["repo_template"] == "dominus-fastapi-template"
601
+
602
+ absent_client = FakeClient()
603
+ absent_namespace = AuthorityNamespace(absent_client)
604
+ await absent_namespace.bootstrap_provisioning_target(
605
+ "summit-radiology",
606
+ shared_app_slug="shared-core",
607
+ app_slug="carebridge",
608
+ env="production",
609
+ )
610
+ # Absent field keeps the legacy wire body byte-identical.
611
+ assert "repo_template" not in absent_client.calls[-1]["body"]
@@ -0,0 +1,277 @@
1
+ """
2
+ Test per-kind canonical error subclasses and v1 alias compatibility.
3
+
4
+ Seams fork 6 (ADR 0010): DominusError base + 11 per-kind subclasses;
5
+ existing class names preserved as aliases in v1.
6
+ """
7
+ import pytest
8
+ from dominus.errors import (
9
+ DominusError,
10
+ # v1 aliases (preserved)
11
+ AuthenticationError,
12
+ AuthorizationError,
13
+ ValidationError,
14
+ ServiceError,
15
+ ConnectionError,
16
+ SecureTableError,
17
+ # Per-kind canonical subclasses
18
+ UnauthorizedError,
19
+ ForbiddenError,
20
+ NotFoundError,
21
+ ValidationFailedError,
22
+ ConflictError,
23
+ RateLimitedError,
24
+ UpstreamUnavailableError,
25
+ InternalError,
26
+ TimeoutError,
27
+ CanceledError,
28
+ UnsupportedVersionError,
29
+ # Stash subclasses
30
+ StashBookmarkArtifactNotFound,
31
+ StashBookmarkInvalidVersionRef,
32
+ StashBookmarkDuplicateName,
33
+ StashBookmarkUpstreamRejected,
34
+ StashWatchInvalidWebhookUrl,
35
+ StashWatchUpstreamRejected,
36
+ StashWatchNotFound,
37
+ raise_for_status,
38
+ )
39
+
40
+ CANONICAL_KINDS = [
41
+ "unauthorized",
42
+ "forbidden",
43
+ "not_found",
44
+ "validation_failed",
45
+ "conflict",
46
+ "rate_limited",
47
+ "upstream_unavailable",
48
+ "internal",
49
+ "timeout",
50
+ "canceled",
51
+ "unsupported_version",
52
+ ]
53
+
54
+ PER_KIND_CLASSES = [
55
+ UnauthorizedError,
56
+ ForbiddenError,
57
+ NotFoundError,
58
+ ValidationFailedError,
59
+ ConflictError,
60
+ RateLimitedError,
61
+ UpstreamUnavailableError,
62
+ InternalError,
63
+ TimeoutError,
64
+ CanceledError,
65
+ UnsupportedVersionError,
66
+ ]
67
+
68
+
69
+ class TestPerKindSubclasses:
70
+ """Each canonical kind has a DominusError subclass with the correct kind."""
71
+
72
+ def test_all_kinds_have_subclass(self):
73
+ kind_map = {cls.kind: cls for cls in PER_KIND_CLASSES}
74
+ for kind in CANONICAL_KINDS:
75
+ assert kind in kind_map, f"Missing subclass for kind '{kind}'"
76
+
77
+ def test_each_subclass_has_kind_attribute(self):
78
+ for cls in PER_KIND_CLASSES:
79
+ assert cls.kind in CANONICAL_KINDS, f"{cls.__name__}.kind = {cls.kind!r} not in canonical kinds"
80
+
81
+ def test_each_subclass_is_dominus_error(self):
82
+ for cls in PER_KIND_CLASSES:
83
+ assert issubclass(cls, DominusError), f"{cls.__name__} is not a DominusError subclass"
84
+
85
+ def test_instance_kind_matches_class_kind(self):
86
+ for cls in PER_KIND_CLASSES:
87
+ err = cls("test")
88
+ assert err.kind == cls.kind, f"{cls.__name__} instance kind mismatch: {err.kind!r} != {cls.kind!r}"
89
+
90
+
91
+ class TestV1Aliases:
92
+ """v1 alias class names resolve to the canonical per-kind subclass."""
93
+
94
+ def test_authentication_error_is_unauthorized(self):
95
+ assert issubclass(AuthenticationError, UnauthorizedError)
96
+ err = AuthenticationError()
97
+ assert isinstance(err, UnauthorizedError)
98
+ assert isinstance(err, DominusError)
99
+ assert err.kind == "unauthorized"
100
+
101
+ def test_authorization_error_is_forbidden(self):
102
+ assert issubclass(AuthorizationError, ForbiddenError)
103
+ err = AuthorizationError()
104
+ assert isinstance(err, ForbiddenError)
105
+ assert isinstance(err, DominusError)
106
+ assert err.kind == "forbidden"
107
+
108
+ def test_validation_error_is_validation_failed(self):
109
+ assert issubclass(ValidationError, ValidationFailedError)
110
+ err = ValidationError()
111
+ assert isinstance(err, ValidationFailedError)
112
+ assert isinstance(err, DominusError)
113
+ assert err.kind == "validation_failed"
114
+
115
+ def test_service_error_is_upstream_unavailable(self):
116
+ assert issubclass(ServiceError, UpstreamUnavailableError)
117
+ err = ServiceError()
118
+ assert isinstance(err, UpstreamUnavailableError)
119
+ assert isinstance(err, DominusError)
120
+ assert err.kind == "upstream_unavailable"
121
+
122
+ def test_connection_error_is_upstream_unavailable(self):
123
+ assert issubclass(ConnectionError, UpstreamUnavailableError)
124
+ err = ConnectionError()
125
+ assert isinstance(err, UpstreamUnavailableError)
126
+ assert isinstance(err, DominusError)
127
+ assert err.kind == "upstream_unavailable"
128
+
129
+ def test_secure_table_error_is_forbidden(self):
130
+ assert issubclass(SecureTableError, ForbiddenError)
131
+ err = SecureTableError()
132
+ assert isinstance(err, ForbiddenError)
133
+ assert isinstance(err, DominusError)
134
+ assert err.kind == "forbidden"
135
+
136
+ def test_existing_catch_sites_still_work(self):
137
+ """Catching by old name still catches the canonical subclass instance."""
138
+ err = UnauthorizedError("bad token")
139
+ # Old catch pattern
140
+ try:
141
+ raise err
142
+ except AuthenticationError as e:
143
+ assert e.kind == "unauthorized"
144
+ assert e.message == "bad token"
145
+
146
+
147
+ class TestStashSubclasses:
148
+ """Stash subclasses map to canonical per-kind bases."""
149
+
150
+ def test_stash_bookmark_not_found(self):
151
+ assert issubclass(StashBookmarkArtifactNotFound, NotFoundError)
152
+ assert StashBookmarkArtifactNotFound().kind == "not_found"
153
+
154
+ def test_stash_bookmark_invalid_ref(self):
155
+ assert issubclass(StashBookmarkInvalidVersionRef, ValidationFailedError)
156
+ assert StashBookmarkInvalidVersionRef().kind == "validation_failed"
157
+
158
+ def test_stash_bookmark_duplicate(self):
159
+ assert issubclass(StashBookmarkDuplicateName, ConflictError)
160
+ assert StashBookmarkDuplicateName().kind == "conflict"
161
+
162
+ def test_stash_bookmark_upstream(self):
163
+ assert issubclass(StashBookmarkUpstreamRejected, UpstreamUnavailableError)
164
+ assert StashBookmarkUpstreamRejected().kind == "upstream_unavailable"
165
+
166
+ def test_stash_watch_invalid_url(self):
167
+ assert issubclass(StashWatchInvalidWebhookUrl, ValidationFailedError)
168
+ assert StashWatchInvalidWebhookUrl().kind == "validation_failed"
169
+
170
+ def test_stash_watch_upstream(self):
171
+ assert issubclass(StashWatchUpstreamRejected, UpstreamUnavailableError)
172
+ assert StashWatchUpstreamRejected().kind == "upstream_unavailable"
173
+
174
+ def test_stash_watch_not_found(self):
175
+ assert issubclass(StashWatchNotFound, NotFoundError)
176
+ assert StashWatchNotFound().kind == "not_found"
177
+
178
+
179
+ class TestRaiseForStatus:
180
+ """raise_for_status maps HTTP codes to canonical per-kind classes."""
181
+
182
+ def test_401_raises_unauthorized(self):
183
+ with pytest.raises(UnauthorizedError) as exc:
184
+ raise_for_status(401, "bad token")
185
+ assert exc.value.kind == "unauthorized"
186
+
187
+ def test_403_raises_forbidden(self):
188
+ with pytest.raises(ForbiddenError) as exc:
189
+ raise_for_status(403, "nope")
190
+ assert exc.value.kind == "forbidden"
191
+
192
+ def test_404_raises_not_found(self):
193
+ with pytest.raises(NotFoundError) as exc:
194
+ raise_for_status(404, "gone")
195
+ assert exc.value.kind == "not_found"
196
+
197
+ def test_400_raises_validation_failed(self):
198
+ with pytest.raises(ValidationFailedError) as exc:
199
+ raise_for_status(400, "bad input")
200
+ assert exc.value.kind == "validation_failed"
201
+
202
+ def test_409_raises_conflict(self):
203
+ with pytest.raises(ConflictError) as exc:
204
+ raise_for_status(409, "dup")
205
+ assert exc.value.kind == "conflict"
206
+
207
+ def test_429_raises_rate_limited(self):
208
+ with pytest.raises(RateLimitedError) as exc:
209
+ raise_for_status(429, "slow down")
210
+ assert exc.value.kind == "rate_limited"
211
+
212
+ def test_500_raises_upstream_unavailable(self):
213
+ with pytest.raises(UpstreamUnavailableError) as exc:
214
+ raise_for_status(500, "boom")
215
+ assert exc.value.kind == "upstream_unavailable"
216
+
217
+ def test_504_raises_timeout(self):
218
+ with pytest.raises(TimeoutError) as exc:
219
+ raise_for_status(504, "took too long")
220
+ assert exc.value.kind == "timeout"
221
+
222
+ def test_explicit_wire_kind_wins_over_status_mapping(self):
223
+ with pytest.raises(UnsupportedVersionError) as exc:
224
+ raise_for_status(
225
+ 400,
226
+ "Unsupported API version",
227
+ {"kind": "unsupported_version", "code": "platform.api.unsupported_version"},
228
+ )
229
+ assert exc.value.kind == "unsupported_version"
230
+
231
+ def test_stash_code_dispatches_to_stash_subclass(self):
232
+ with pytest.raises(StashBookmarkArtifactNotFound) as exc:
233
+ raise_for_status(404, "bookmark gone", {"code": "stash.bookmark.artifact_not_found"})
234
+ assert isinstance(exc.value, NotFoundError)
235
+ assert exc.value.kind == "not_found"
236
+
237
+
238
+ class TestDominusErrorKind:
239
+ """DominusError base class carries kind and exposes it in to_dict."""
240
+
241
+ def test_default_kind_is_internal(self):
242
+ err = DominusError("generic")
243
+ assert err.kind == "internal"
244
+
245
+ def test_explicit_kind_overrides_default(self):
246
+ err = DominusError("custom", kind="timeout")
247
+ assert err.kind == "timeout"
248
+
249
+ def test_to_dict_includes_kind(self):
250
+ err = UnauthorizedError("nope")
251
+ d = err.to_dict()
252
+ assert d["kind"] == "unauthorized"
253
+
254
+ def test_to_dict_emits_canonical_wire_fields(self):
255
+ err = NotFoundError(
256
+ "Missing resource",
257
+ details={
258
+ "code": "authority.resource.not_found",
259
+ "request_id": "req-python-envelope-1",
260
+ "correlation_id": "corr-python-envelope-1",
261
+ },
262
+ )
263
+
264
+ envelope = err.to_dict()
265
+ assert {key: envelope.get(key) for key in (
266
+ "kind", "code", "message", "request_id", "correlation_id",
267
+ "retryable", "http_status", "envelope_version",
268
+ )} == {
269
+ "kind": "not_found",
270
+ "code": "authority.resource.not_found",
271
+ "message": "Missing resource",
272
+ "request_id": "req-python-envelope-1",
273
+ "correlation_id": "corr-python-envelope-1",
274
+ "retryable": False,
275
+ "http_status": 404,
276
+ "envelope_version": "1",
277
+ }
@@ -353,3 +353,72 @@ async def test_watch_invalid_webhook_url_raises_typed_exception(monkeypatch, sdk
353
353
 
354
354
  assert exc_info.value.code == "stash.watch.invalid_webhook_url"
355
355
  assert exc_info.value.endpoint == "/svc/stash/watch"
356
+
357
+
358
+ @pytest.mark.asyncio
359
+ async def test_register_kind_posts_backend(monkeypatch, sdk):
360
+ calls = []
361
+
362
+ async def fake_request(**kwargs):
363
+ calls.append(kwargs)
364
+ return {"kind": {"kind": "artifact", "backend": "artifact-v2"}}
365
+
366
+ monkeypatch.setattr(sdk, "_request", fake_request)
367
+
368
+ result = await sdk.stash.register_kind(
369
+ kind="artifact",
370
+ backend="artifact-v2",
371
+ is_sensitive=False,
372
+ description="versioned artifacts",
373
+ )
374
+
375
+ assert result == {"kind": "artifact", "backend": "artifact-v2"}
376
+ assert calls == [
377
+ {
378
+ "endpoint": "/svc/stash/kinds/register",
379
+ "body": {
380
+ "kind": "artifact",
381
+ "backend": "artifact-v2",
382
+ "is_sensitive": False,
383
+ "description": "versioned artifacts",
384
+ },
385
+ "use_gateway": True,
386
+ }
387
+ ]
388
+
389
+
390
+ @pytest.mark.asyncio
391
+ async def test_register_kind_omits_backend_when_unset(monkeypatch, sdk):
392
+ calls = []
393
+
394
+ async def fake_request(**kwargs):
395
+ calls.append(kwargs)
396
+ return {"kind": {"kind": "document"}}
397
+
398
+ monkeypatch.setattr(sdk, "_request", fake_request)
399
+
400
+ result = await sdk.stash.register_kind(kind="document")
401
+
402
+ assert result == {"kind": "document"}
403
+ assert calls == [
404
+ {
405
+ "endpoint": "/svc/stash/kinds/register",
406
+ "body": {"kind": "document"},
407
+ "use_gateway": True,
408
+ }
409
+ ]
410
+
411
+
412
+ def test_stash_backend_literal_covers_live_backends():
413
+ from typing import get_args
414
+
415
+ from dominus.namespaces.stash import StashBackend
416
+
417
+ assert set(get_args(StashBackend)) == {
418
+ "db-row",
419
+ "b2-versioned-bytes",
420
+ "b2-pointer-text",
421
+ "redis-ttl",
422
+ "artifact-v2",
423
+ "journal",
424
+ }