dominus-sdk-python 4.5.0__tar.gz → 4.6.1__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 (62) hide show
  1. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/PKG-INFO +1 -1
  2. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/__init__.py +3 -1
  3. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/__init__.py +2 -0
  4. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/browser.py +24 -3
  5. dominus_sdk_python-4.6.1/dominus/namespaces/recipes.py +165 -0
  6. dominus_sdk_python-4.6.1/dominus/namespaces/stash.py +275 -0
  7. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/start.py +8 -0
  8. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus_sdk_python.egg-info/PKG-INFO +1 -1
  9. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus_sdk_python.egg-info/SOURCES.txt +2 -0
  10. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/pyproject.toml +1 -1
  11. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/tests/test_browser_namespace.py +6 -2
  12. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/tests/test_public_exports.py +3 -0
  13. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/README.md +0 -0
  14. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/config/__init__.py +0 -0
  15. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/config/endpoints.py +0 -0
  16. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/errors.py +0 -0
  17. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/helpers/__init__.py +0 -0
  18. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/helpers/auth.py +0 -0
  19. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/helpers/cache.py +0 -0
  20. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/helpers/console_capture.py +0 -0
  21. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/helpers/core.py +0 -0
  22. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/helpers/crypto.py +0 -0
  23. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/helpers/sse.py +0 -0
  24. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/helpers/trace.py +0 -0
  25. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/admin.py +0 -0
  26. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/ai.py +0 -0
  27. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/artifacts.py +0 -0
  28. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/auth.py +0 -0
  29. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/authority.py +0 -0
  30. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/courier.py +0 -0
  31. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/db.py +0 -0
  32. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/ddl.py +0 -0
  33. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/deployer.py +0 -0
  34. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/fastapi.py +0 -0
  35. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/files.py +0 -0
  36. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/health.py +0 -0
  37. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/jobs.py +0 -0
  38. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/logs.py +0 -0
  39. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/portal.py +0 -0
  40. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/processor.py +0 -0
  41. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/redis.py +0 -0
  42. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/secrets.py +0 -0
  43. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/secure.py +0 -0
  44. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/sync.py +0 -0
  45. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/warden.py +0 -0
  46. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/namespaces/workflow.py +0 -0
  47. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus/services/__init__.py +0 -0
  48. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus_sdk_python.egg-info/dependency_links.txt +0 -0
  49. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus_sdk_python.egg-info/requires.txt +0 -0
  50. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/dominus_sdk_python.egg-info/top_level.txt +0 -0
  51. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/setup.cfg +0 -0
  52. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/tests/test_auth.py +0 -0
  53. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/tests/test_authority_public_vocabulary.py +0 -0
  54. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/tests/test_control_plane_namespaces.py +0 -0
  55. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/tests/test_errors.py +0 -0
  56. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/tests/test_flat_commands.py +0 -0
  57. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/tests/test_health.py +0 -0
  58. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/tests/test_logs.py +0 -0
  59. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/tests/test_provisioning_parity.py +0 -0
  60. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/tests/test_transport_compat.py +0 -0
  61. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/tests/test_workflow_lifecycle.py +0 -0
  62. {dominus_sdk_python-4.5.0 → dominus_sdk_python-4.6.1}/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: 4.5.0
3
+ Version: 4.6.1
4
4
  Summary: Python SDK for the Dominus gateway-first platform
5
5
  Author-email: CareBridge Systems <dev@carebridge.io>
6
6
  License: Proprietary
@@ -112,6 +112,7 @@ from .namespaces.processor import ProcessorNamespace
112
112
  from .namespaces.sync import SyncNamespace
113
113
  from .namespaces.authority import AuthorityNamespace
114
114
  from .namespaces.browser import BrowserNamespace
115
+ from .namespaces.recipes import RecipesNamespace
115
116
  from .namespaces.deployer import DeployerNamespace
116
117
  from .namespaces.warden import WardenNamespace
117
118
 
@@ -164,7 +165,7 @@ from .errors import (
164
165
  TimeoutError as DominusTimeoutError,
165
166
  )
166
167
 
167
- __version__ = "4.5.0"
168
+ __version__ = "4.6.1"
168
169
  __all__ = [
169
170
  # Main SDK instance
170
171
  "dominus",
@@ -215,6 +216,7 @@ __all__ = [
215
216
  "SyncNamespace",
216
217
  "AuthorityNamespace",
217
218
  "BrowserNamespace",
219
+ "RecipesNamespace",
218
220
  "DeployerNamespace",
219
221
  "WardenNamespace",
220
222
  # AI namespace for agent-runtime operations
@@ -16,6 +16,7 @@ from .processor import ProcessorNamespace
16
16
  from .sync import SyncNamespace
17
17
  from .authority import AuthorityNamespace
18
18
  from .browser import BrowserNamespace
19
+ from .recipes import RecipesNamespace
19
20
  from .deployer import DeployerNamespace
20
21
  from .warden import WardenNamespace
21
22
  from .ai import (
@@ -44,6 +45,7 @@ __all__ = [
44
45
  "SyncNamespace",
45
46
  "AuthorityNamespace",
46
47
  "BrowserNamespace",
48
+ "RecipesNamespace",
47
49
  "DeployerNamespace",
48
50
  "WardenNamespace",
49
51
  "AiNamespace",
@@ -77,7 +77,7 @@ class BrowserNamespace:
77
77
  async def ensure_run(
78
78
  self,
79
79
  *,
80
- target: Dict[str, Any],
80
+ target: Optional[Dict[str, Any]] = None,
81
81
  idempotency_key: Optional[str] = None,
82
82
  run_kind: Optional[str] = None,
83
83
  route_label: Optional[str] = None,
@@ -92,11 +92,29 @@ class BrowserNamespace:
92
92
  org_id: Optional[str] = None,
93
93
  app_slug: Optional[str] = None,
94
94
  env: Optional[str] = None,
95
+ recipe_ref: Optional[str] = None,
96
+ recipe: Optional[Any] = None,
97
+ recipe_inputs: Optional[Dict[str, Any]] = None,
95
98
  timeout: float = 30.0,
96
99
  ) -> Dict[str, Any]:
97
100
  """
98
101
  Ensure a browser run. ``POST /api/browser/runs/ensure``.
99
102
 
103
+ ``target`` shorthand, ``recipe_ref``, or inline ``recipe`` must be
104
+ present. When ``recipe_ref`` is set, the worker resolves the
105
+ recipe through the recipe-worker, parses it, resolves variables, and
106
+ runs it through the recipe executor — supporting multi-step flows
107
+ including clicks, fills, assertions, extracts, and stash-backed
108
+ credential fills.
109
+
110
+ ``recipe`` may be an inline YAML string or JSON object for one-off
111
+ runs. Stable/shared recipes should be published and referenced via
112
+ ``recipe_ref``. Target/assertion shorthand runs are synthesized into a
113
+ ``browser-recipe-v1`` recipe internally; there is no separate runner path.
114
+
115
+ ``recipe_inputs`` supplies scalar values consumed by ``recipe-input://``
116
+ variable refs declared in the recipe's vars map.
117
+
100
118
  Browser run metadata remains browser-worker runtime state. Artifact V2
101
119
  is used for sanitized `browser-run` result payloads and, when opted-in
102
120
  via ``capture_policy["storage_state"] = "always"`` plus
@@ -109,8 +127,8 @@ class BrowserNamespace:
109
127
  lookup, returns no storage_state on first call) or an explicit
110
128
  ``session_artifact_ref`` (hard-error if the ref does not exist).
111
129
  """
112
- if not target:
113
- raise ValueError("target is required")
130
+ if not target and not recipe_ref and recipe is None:
131
+ raise ValueError("target, recipe_ref, or recipe is required")
114
132
  body = _compact({
115
133
  "idempotency_key": idempotency_key,
116
134
  "run_kind": run_kind,
@@ -127,6 +145,9 @@ class BrowserNamespace:
127
145
  "org_id": org_id,
128
146
  "app_slug": app_slug,
129
147
  "env": env,
148
+ "recipe_ref": recipe_ref,
149
+ "recipe": recipe,
150
+ "recipe_inputs": recipe_inputs,
130
151
  })
131
152
  return await self._post("/api/browser/runs/ensure", body, timeout=timeout)
132
153
 
@@ -0,0 +1,165 @@
1
+ """
2
+ Recipes Namespace - kernel-tier recipe protocol.
3
+
4
+ Routes through ``/svc/recipe/*`` on the Dominus gateway. The recipe worker
5
+ owns the type registry, JSON-Schema-based publish validation, three-tier
6
+ resolution (project -> group -> platform), and B2-backed durable storage.
7
+
8
+ Recipe types currently registered:
9
+ - browser-recipe (owned by dominus-browser-worker; body version is browser-recipe-v1)
10
+
11
+ Refs follow ``recipe://{type}/{name}[@v{N}][?tier={tier}]``.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ from typing import Any, Dict, List, Optional, TYPE_CHECKING
16
+
17
+ if TYPE_CHECKING:
18
+ from ..start import Dominus
19
+
20
+
21
+ def _compact(payload: Dict[str, Any]) -> Dict[str, Any]:
22
+ return {k: v for k, v in payload.items() if v is not None and v != ""}
23
+
24
+
25
+ class RecipesNamespace:
26
+ """
27
+ Recipe worker namespace.
28
+
29
+ Usage::
30
+
31
+ types = await dominus.recipes.list_types()
32
+ await dominus.recipes.publish(
33
+ type="browser-recipe",
34
+ tier="platform",
35
+ name="health-check",
36
+ body="version: browser-recipe-v1\\nsteps:\\n - kind: goto\\n url: https://x/\\n",
37
+ )
38
+ recipe = await dominus.recipes.get(type="browser-recipe", name="health-check")
39
+ """
40
+
41
+ def __init__(self, client: "Dominus"):
42
+ self._client = client
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
+ )
52
+
53
+ async def _get(self, endpoint: str, *, timeout: float = 30.0) -> Dict[str, Any]:
54
+ return await self._client._request(
55
+ endpoint=endpoint,
56
+ method="GET",
57
+ use_gateway=True,
58
+ timeout=timeout,
59
+ )
60
+
61
+ async def list_types(self, *, owning_worker: Optional[str] = None, timeout: float = 15.0) -> Dict[str, Any]:
62
+ """List registered recipe types. ``GET /api/recipe/types``."""
63
+ query = f"?owning_worker={owning_worker}" if owning_worker else ""
64
+ return await self._get(f"/api/recipe/types{query}", timeout=timeout)
65
+
66
+ async def get_type(self, name: str, *, timeout: float = 15.0) -> Dict[str, Any]:
67
+ """Fetch a registered recipe type's schema. ``GET /api/recipe/types/{name}``."""
68
+ return await self._get(f"/api/recipe/types/{name}", timeout=timeout)
69
+
70
+ async def publish(
71
+ self,
72
+ *,
73
+ type: str,
74
+ tier: str,
75
+ name: str,
76
+ body: str,
77
+ description: Optional[str] = None,
78
+ timeout: float = 30.0,
79
+ ) -> Dict[str, Any]:
80
+ """Publish a recipe. ``POST /api/recipe/recipes/publish``."""
81
+ return await self._post(
82
+ "/api/recipe/recipes/publish",
83
+ _compact({
84
+ "type": type,
85
+ "tier": tier,
86
+ "name": name,
87
+ "body": body,
88
+ "description": description,
89
+ }),
90
+ timeout=timeout,
91
+ )
92
+
93
+ async def validate(
94
+ self,
95
+ *,
96
+ type: str,
97
+ body: str,
98
+ timeout: float = 15.0,
99
+ ) -> Dict[str, Any]:
100
+ """Validate a recipe body against its type schema. ``POST /api/recipe/recipes/validate``."""
101
+ return await self._post(
102
+ "/api/recipe/recipes/validate",
103
+ {"type": type, "body": body},
104
+ timeout=timeout,
105
+ )
106
+
107
+ async def list(
108
+ self,
109
+ *,
110
+ type: Optional[str] = None,
111
+ tier: Optional[str] = None,
112
+ name_prefix: Optional[str] = None,
113
+ timeout: float = 15.0,
114
+ ) -> Dict[str, Any]:
115
+ """List recipes filtered by type/tier/name. ``GET /api/recipe/recipes/list``."""
116
+ params: List[str] = []
117
+ if type:
118
+ params.append(f"type={type}")
119
+ if tier:
120
+ params.append(f"tier={tier}")
121
+ if name_prefix:
122
+ params.append(f"name_prefix={name_prefix}")
123
+ query = ("?" + "&".join(params)) if params else ""
124
+ return await self._get(f"/api/recipe/recipes/list{query}", timeout=timeout)
125
+
126
+ async def get(
127
+ self,
128
+ *,
129
+ type: str,
130
+ name: str,
131
+ version: Optional[int] = None,
132
+ tier: Optional[str] = None,
133
+ timeout: float = 15.0,
134
+ ) -> Dict[str, Any]:
135
+ """Resolve a recipe through the three-tier chain. ``GET /api/recipe/recipes/{type}/{name}[@v{N}]``."""
136
+ path = f"/api/recipe/recipes/{type}/{name}"
137
+ if version is not None:
138
+ path += f"@v{version}"
139
+ if tier:
140
+ path += f"?tier={tier}"
141
+ return await self._get(path, timeout=timeout)
142
+
143
+ async def register_type(
144
+ self,
145
+ *,
146
+ name: str,
147
+ owning_worker: str,
148
+ schema_version: int,
149
+ json_schema: Dict[str, Any],
150
+ timeout: float = 15.0,
151
+ ) -> Dict[str, Any]:
152
+ """Register a recipe type. ``POST /api/recipe/types/register``.
153
+
154
+ Owning workers do this on cold start. Normal callers should not need it.
155
+ """
156
+ return await self._post(
157
+ "/api/recipe/types/register",
158
+ {
159
+ "name": name,
160
+ "owning_worker": owning_worker,
161
+ "schema_version": schema_version,
162
+ "json_schema": json_schema,
163
+ },
164
+ timeout=timeout,
165
+ )
@@ -0,0 +1,275 @@
1
+ """
2
+ Stash Namespace - per-scope durable items (credentials + configs).
3
+
4
+ Routes through ``/svc/stash/*`` on the Dominus gateway. The stash worker
5
+ stores items in each project's ``stash.*`` schema and transparently falls
6
+ back to a designated shared project on read.
7
+ """
8
+ from typing import Any, Dict, List, Optional, TYPE_CHECKING
9
+
10
+ if TYPE_CHECKING:
11
+ from ..start import Dominus
12
+
13
+
14
+ class StashNamespace:
15
+ """
16
+ Stash worker namespace.
17
+
18
+ Usage:
19
+ item = await dominus.stash.resolve(
20
+ kind="credential.vendor.fuji_synapse",
21
+ scope={"user_id": user_id, "organization_code": "piedmont"},
22
+ purpose="extraction",
23
+ )
24
+ await dominus.stash.mark_verified(id=item["id"], purpose="extraction")
25
+ """
26
+
27
+ def __init__(self, client: "Dominus"):
28
+ self._client = client
29
+
30
+ async def upsert(
31
+ self,
32
+ *,
33
+ kind: str,
34
+ scope: Dict[str, Any],
35
+ value: Dict[str, Any],
36
+ id: Optional[str] = None,
37
+ target: Optional[str] = None,
38
+ item_key: Optional[str] = None,
39
+ role: Optional[str] = None,
40
+ priority: Optional[int] = None,
41
+ is_sensitive: Optional[bool] = None,
42
+ rotation_interval_days: Optional[int] = None,
43
+ expires_at: Optional[str] = None,
44
+ purpose: Optional[str] = None,
45
+ ) -> Dict[str, Any]:
46
+ """Insert or update a stash item."""
47
+ body: Dict[str, Any] = {"kind": kind, "scope": scope, "value": value}
48
+ if id is not None:
49
+ body["id"] = id
50
+ if target is not None:
51
+ body["target"] = target
52
+ if item_key is not None:
53
+ body["item_key"] = item_key
54
+ if role is not None:
55
+ body["role"] = role
56
+ if priority is not None:
57
+ body["priority"] = priority
58
+ if is_sensitive is not None:
59
+ body["is_sensitive"] = is_sensitive
60
+ if rotation_interval_days is not None:
61
+ body["rotation_interval_days"] = rotation_interval_days
62
+ if expires_at is not None:
63
+ body["expires_at"] = expires_at
64
+ if purpose is not None:
65
+ body["purpose"] = purpose
66
+ return await self._client._request(
67
+ endpoint="/svc/stash/items/upsert",
68
+ body=body,
69
+ use_gateway=True,
70
+ )
71
+
72
+ async def get(
73
+ self,
74
+ id: str,
75
+ *,
76
+ purpose: Optional[str] = None,
77
+ include_shared: Optional[bool] = None,
78
+ ) -> Optional[Dict[str, Any]]:
79
+ """Fetch a single item by id; decrypts sensitive values."""
80
+ body: Dict[str, Any] = {"id": id}
81
+ if purpose is not None:
82
+ body["purpose"] = purpose
83
+ if include_shared is not None:
84
+ body["include_shared"] = include_shared
85
+ result = await self._client._request(
86
+ endpoint="/svc/stash/items/get",
87
+ body=body,
88
+ use_gateway=True,
89
+ )
90
+ return result.get("item") if isinstance(result, dict) else None
91
+
92
+ async def resolve(
93
+ self,
94
+ *,
95
+ kind: str,
96
+ scope: Dict[str, Any],
97
+ purpose: str,
98
+ fallback_roles: Optional[List[str]] = None,
99
+ health_status_in: Optional[List[str]] = None,
100
+ machine_id: Optional[str] = None,
101
+ include_shared: Optional[bool] = None,
102
+ ) -> Optional[Dict[str, Any]]:
103
+ """Resolve the best-priority match by (kind, scope) with fallback walk."""
104
+ body: Dict[str, Any] = {"kind": kind, "scope": scope, "purpose": purpose}
105
+ if fallback_roles is not None:
106
+ body["fallback_roles"] = fallback_roles
107
+ if health_status_in is not None:
108
+ body["health_status_in"] = health_status_in
109
+ if machine_id is not None:
110
+ body["machine_id"] = machine_id
111
+ if include_shared is not None:
112
+ body["include_shared"] = include_shared
113
+ result = await self._client._request(
114
+ endpoint="/svc/stash/items/resolve",
115
+ body=body,
116
+ use_gateway=True,
117
+ )
118
+ return result.get("item") if isinstance(result, dict) else None
119
+
120
+ async def list(
121
+ self,
122
+ *,
123
+ kind: Optional[str] = None,
124
+ scope: Optional[Dict[str, Any]] = None,
125
+ role: Optional[str] = None,
126
+ health_status: Optional[Any] = None,
127
+ scope_origin: Optional[str] = None,
128
+ include_deleted: Optional[bool] = None,
129
+ limit: Optional[int] = None,
130
+ offset: Optional[int] = None,
131
+ ) -> Dict[str, Any]:
132
+ """List items by (kind, partial scope, role, health). Metadata only."""
133
+ body: Dict[str, Any] = {}
134
+ if kind is not None:
135
+ body["kind"] = kind
136
+ if scope is not None:
137
+ body["scope"] = scope
138
+ if role is not None:
139
+ body["role"] = role
140
+ if health_status is not None:
141
+ body["health_status"] = health_status
142
+ if scope_origin is not None:
143
+ body["scope_origin"] = scope_origin
144
+ if include_deleted is not None:
145
+ body["include_deleted"] = include_deleted
146
+ if limit is not None:
147
+ body["limit"] = limit
148
+ if offset is not None:
149
+ body["offset"] = offset
150
+ return await self._client._request(
151
+ endpoint="/svc/stash/items/list",
152
+ body=body,
153
+ use_gateway=True,
154
+ )
155
+
156
+ async def mark_verified(
157
+ self, *, id: str, purpose: str, detail: Optional[str] = None
158
+ ) -> Optional[Dict[str, Any]]:
159
+ """Mark an item as verified after a successful use; resets failure_count."""
160
+ body: Dict[str, Any] = {"id": id, "purpose": purpose}
161
+ if detail is not None:
162
+ body["detail"] = detail
163
+ result = await self._client._request(
164
+ endpoint="/svc/stash/items/mark-verified",
165
+ body=body,
166
+ use_gateway=True,
167
+ )
168
+ return result.get("item") if isinstance(result, dict) else None
169
+
170
+ async def mark_failed(
171
+ self,
172
+ *,
173
+ id: str,
174
+ purpose: str,
175
+ failure_reason: Optional[str] = None,
176
+ detail: Optional[str] = None,
177
+ ) -> Optional[Dict[str, Any]]:
178
+ """Mark an item as failed; increments failure_count, sets last_failed_at."""
179
+ body: Dict[str, Any] = {"id": id, "purpose": purpose}
180
+ if failure_reason is not None:
181
+ body["failure_reason"] = failure_reason
182
+ if detail is not None:
183
+ body["detail"] = detail
184
+ result = await self._client._request(
185
+ endpoint="/svc/stash/items/mark-failed",
186
+ body=body,
187
+ use_gateway=True,
188
+ )
189
+ return result.get("item") if isinstance(result, dict) else None
190
+
191
+ async def delete(self, id: str, *, purpose: Optional[str] = None) -> bool:
192
+ """Soft-delete an item (sets deleted_at)."""
193
+ body: Dict[str, Any] = {"id": id}
194
+ if purpose is not None:
195
+ body["purpose"] = purpose
196
+ result = await self._client._request(
197
+ endpoint="/svc/stash/items/delete",
198
+ body=body,
199
+ use_gateway=True,
200
+ )
201
+ return bool(result.get("deleted")) if isinstance(result, dict) else False
202
+
203
+ async def register_kind(
204
+ self,
205
+ *,
206
+ kind: str,
207
+ is_sensitive: Optional[bool] = None,
208
+ value_schema: Optional[Dict[str, Any]] = None,
209
+ default_rotation_days: Optional[int] = None,
210
+ verification_probe_ref: Optional[str] = None,
211
+ description: Optional[str] = None,
212
+ ) -> Dict[str, Any]:
213
+ """Register or update a kind in stash.kind_registry. Admin-scoped."""
214
+ body: Dict[str, Any] = {"kind": kind}
215
+ if is_sensitive is not None:
216
+ body["is_sensitive"] = is_sensitive
217
+ if value_schema is not None:
218
+ body["value_schema"] = value_schema
219
+ if default_rotation_days is not None:
220
+ body["default_rotation_days"] = default_rotation_days
221
+ if verification_probe_ref is not None:
222
+ body["verification_probe_ref"] = verification_probe_ref
223
+ if description is not None:
224
+ body["description"] = description
225
+ result = await self._client._request(
226
+ endpoint="/svc/stash/kinds/register",
227
+ body=body,
228
+ use_gateway=True,
229
+ )
230
+ return result.get("kind", {}) if isinstance(result, dict) else {}
231
+
232
+ async def list_kinds(self) -> List[Dict[str, Any]]:
233
+ """List registered kinds (own + shared merged)."""
234
+ result = await self._client._request(
235
+ endpoint="/svc/stash/kinds/list",
236
+ body={},
237
+ use_gateway=True,
238
+ )
239
+ return result.get("kinds", []) if isinstance(result, dict) else []
240
+
241
+ async def query_audit(
242
+ self,
243
+ *,
244
+ item_id: Optional[str] = None,
245
+ kind: Optional[str] = None,
246
+ caller_id: Optional[str] = None,
247
+ action: Optional[str] = None,
248
+ occurred_after: Optional[str] = None,
249
+ occurred_before: Optional[str] = None,
250
+ limit: Optional[int] = None,
251
+ offset: Optional[int] = None,
252
+ ) -> Dict[str, Any]:
253
+ """Query the append-only audit log. Hashes only — never plaintext."""
254
+ body: Dict[str, Any] = {}
255
+ if item_id is not None:
256
+ body["item_id"] = item_id
257
+ if kind is not None:
258
+ body["kind"] = kind
259
+ if caller_id is not None:
260
+ body["caller_id"] = caller_id
261
+ if action is not None:
262
+ body["action"] = action
263
+ if occurred_after is not None:
264
+ body["occurred_after"] = occurred_after
265
+ if occurred_before is not None:
266
+ body["occurred_before"] = occurred_before
267
+ if limit is not None:
268
+ body["limit"] = limit
269
+ if offset is not None:
270
+ body["offset"] = offset
271
+ return await self._client._request(
272
+ endpoint="/svc/stash/audit/query",
273
+ body=body,
274
+ use_gateway=True,
275
+ )
@@ -182,6 +182,14 @@ class Dominus:
182
182
  self.deployer = DeployerNamespace(self)
183
183
  self.warden = WardenNamespace(self)
184
184
 
185
+ # Stash worker (per-scope durable items: credentials + configs)
186
+ from .namespaces.stash import StashNamespace
187
+ self.stash = StashNamespace(self)
188
+
189
+ # Recipe worker (kernel-tier; hosts browser-recipe-v1 today, workflow + envoy + others later)
190
+ from .namespaces.recipes import RecipesNamespace
191
+ self.recipes = RecipesNamespace(self)
192
+
185
193
  # Cache for JWT public key
186
194
  self._public_key_cache = None
187
195
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dominus-sdk-python
3
- Version: 4.5.0
3
+ Version: 4.6.1
4
4
  Summary: Python SDK for the Dominus gateway-first platform
5
5
  Author-email: CareBridge Systems <dev@carebridge.io>
6
6
  License: Proprietary
@@ -31,9 +31,11 @@ dominus/namespaces/jobs.py
31
31
  dominus/namespaces/logs.py
32
32
  dominus/namespaces/portal.py
33
33
  dominus/namespaces/processor.py
34
+ dominus/namespaces/recipes.py
34
35
  dominus/namespaces/redis.py
35
36
  dominus/namespaces/secrets.py
36
37
  dominus/namespaces/secure.py
38
+ dominus/namespaces/stash.py
37
39
  dominus/namespaces/sync.py
38
40
  dominus/namespaces/warden.py
39
41
  dominus/namespaces/workflow.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dominus-sdk-python"
7
- version = "4.5.0"
7
+ version = "4.6.1"
8
8
  description = "Python SDK for the Dominus gateway-first platform"
9
9
  readme = "README.md"
10
10
  license = {text = "Proprietary"}
@@ -59,6 +59,8 @@ async def test_browser_namespace_uses_gateway_routed_api_browser_paths(monkeypat
59
59
  ],
60
60
  authority={"run_id": "auth-run-1", "artifact_observations": True},
61
61
  metadata={"route": "dashboard"},
62
+ recipe={"version": "browser-recipe-v1", "steps": [{"kind": "goto", "url": "https://summit.example/dashboard"}]},
63
+ recipe_inputs={"tenant": "summit"},
62
64
  )
63
65
  await sdk.browser.start_run("br_123")
64
66
  await sdk.browser.get_run_status("br_123")
@@ -99,9 +101,11 @@ async def test_browser_namespace_uses_gateway_routed_api_browser_paths(monkeypat
99
101
  "run_id": "auth-run-1",
100
102
  "artifact_observations": True,
101
103
  }
104
+ assert ensure_body["recipe"]["version"] == "browser-recipe-v1"
105
+ assert ensure_body["recipe_inputs"] == {"tenant": "summit"}
102
106
 
103
107
 
104
108
  @pytest.mark.asyncio
105
- async def test_browser_ensure_run_requires_target(sdk):
106
- with pytest.raises(ValueError, match="target is required"):
109
+ async def test_browser_ensure_run_requires_target_or_recipe(sdk):
110
+ with pytest.raises(ValueError, match="target, recipe_ref, or recipe is required"):
107
111
  await sdk.browser.ensure_run(target={})
@@ -1,6 +1,7 @@
1
1
  from dominus import (
2
2
  BrowserNamespace,
3
3
  DeployerNamespace,
4
+ RecipesNamespace,
4
5
  WardenNamespace,
5
6
  gateway_circuit_breaker,
6
7
  mint_selected_scope_jwt,
@@ -31,9 +32,11 @@ def test_top_level_exports_drop_delegate_alias():
31
32
  assert DeployerNamespace is not None
32
33
  assert WardenNamespace is not None
33
34
  assert BrowserNamespace is not None
35
+ assert RecipesNamespace is not None
34
36
 
35
37
 
36
38
  def test_singleton_exposes_browser_namespace():
37
39
  from dominus import dominus
38
40
 
39
41
  assert callable(dominus.browser.get_health)
42
+ assert callable(dominus.recipes.list_types)