dominus-sdk-python 2.17.0__tar.gz → 2.18.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 (53) hide show
  1. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/PKG-INFO +11 -3
  2. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/README.md +10 -2
  3. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus/__init__.py +5 -1
  4. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus/helpers/core.py +36 -15
  5. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus/namespaces/artifacts.py +84 -60
  6. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus/namespaces/logs.py +35 -6
  7. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus/namespaces/workflow.py +299 -107
  8. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus_sdk_python.egg-info/PKG-INFO +11 -3
  9. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus_sdk_python.egg-info/SOURCES.txt +1 -0
  10. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/pyproject.toml +1 -1
  11. dominus_sdk_python-2.18.1/tests/test_logs.py +65 -0
  12. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/tests/test_workflow_lifecycle.py +101 -87
  13. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/tests/test_workflow_refs.py +12 -10
  14. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus/config/__init__.py +0 -0
  15. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus/config/endpoints.py +0 -0
  16. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus/errors.py +0 -0
  17. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus/helpers/__init__.py +0 -0
  18. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus/helpers/auth.py +0 -0
  19. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus/helpers/cache.py +0 -0
  20. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus/helpers/console_capture.py +0 -0
  21. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus/helpers/crypto.py +0 -0
  22. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus/helpers/sse.py +0 -0
  23. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus/helpers/trace.py +0 -0
  24. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus/namespaces/__init__.py +0 -0
  25. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus/namespaces/admin.py +0 -0
  26. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus/namespaces/ai.py +0 -0
  27. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus/namespaces/auth.py +0 -0
  28. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus/namespaces/courier.py +0 -0
  29. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus/namespaces/db.py +0 -0
  30. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus/namespaces/ddl.py +0 -0
  31. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus/namespaces/fastapi.py +0 -0
  32. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus/namespaces/files.py +0 -0
  33. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus/namespaces/health.py +0 -0
  34. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus/namespaces/jobs.py +0 -0
  35. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus/namespaces/open.py +0 -0
  36. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus/namespaces/oracle/__init__.py +0 -0
  37. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus/namespaces/oracle/audio_capture.py +0 -0
  38. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus/namespaces/oracle/oracle_websocket.py +0 -0
  39. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus/namespaces/oracle/session.py +0 -0
  40. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus/namespaces/oracle/types.py +0 -0
  41. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus/namespaces/oracle/vad_gate.py +0 -0
  42. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus/namespaces/portal.py +0 -0
  43. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus/namespaces/processor.py +0 -0
  44. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus/namespaces/redis.py +0 -0
  45. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus/namespaces/secrets.py +0 -0
  46. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus/namespaces/secure.py +0 -0
  47. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus/namespaces/sync.py +0 -0
  48. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus/services/__init__.py +0 -0
  49. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus/start.py +0 -0
  50. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus_sdk_python.egg-info/dependency_links.txt +0 -0
  51. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus_sdk_python.egg-info/requires.txt +0 -0
  52. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/dominus_sdk_python.egg-info/top_level.txt +0 -0
  53. {dominus_sdk_python-2.17.0 → dominus_sdk_python-2.18.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dominus-sdk-python
3
- Version: 2.17.0
3
+ Version: 2.18.1
4
4
  Summary: Python SDK for the Dominus Orchestrator Platform
5
5
  Author-email: CareBridge Systems <dev@carebridge.io>
6
6
  License: Proprietary
@@ -49,7 +49,7 @@ Async Python SDK for CareBridge Dominus platform services. Routes calls through
49
49
  - Server-side, asyncio-first Python SDK (3.9+)
50
50
  - Namespace API with root-level shortcuts for common operations
51
51
  - Targets production Cloudflare Workers (gateway, JWT, logs) and Cloud Run (orchestrator)
52
- - Version: 2.16.0
52
+ - Version: 2.18.1
53
53
 
54
54
  ## Quick Start
55
55
 
@@ -89,7 +89,15 @@ async for chunk in dominus.ai.stream_agent(
89
89
  ):
90
90
  print(chunk.get("content", ""), end="", flush=True)
91
91
 
92
- # Saved workflow lifecycle through workflow-manager
92
+ # Preferred Authority-backed one-call lifecycle
93
+ execution = await dominus.workflow.ensure(
94
+ "wf://carebridge/report-cycle",
95
+ subject="PCM47474562",
96
+ company="summit-radiology",
97
+ inputs={"report_snapshot": "ar://carebridge/summit-radiology/production/snapshot/report-1"},
98
+ )
99
+
100
+ # Saved workflow lifecycle through workflow-manager when you need explicit artifacts
93
101
  run = await dominus.workflow.create_run(
94
102
  workflow_id="wf_saved_123",
95
103
  context={"report_ref": {"report_id": "r-1", "accession": "a-1", "session_number": "2"}},
@@ -7,7 +7,7 @@ Async Python SDK for CareBridge Dominus platform services. Routes calls through
7
7
  - Server-side, asyncio-first Python SDK (3.9+)
8
8
  - Namespace API with root-level shortcuts for common operations
9
9
  - Targets production Cloudflare Workers (gateway, JWT, logs) and Cloud Run (orchestrator)
10
- - Version: 2.16.0
10
+ - Version: 2.18.1
11
11
 
12
12
  ## Quick Start
13
13
 
@@ -47,7 +47,15 @@ async for chunk in dominus.ai.stream_agent(
47
47
  ):
48
48
  print(chunk.get("content", ""), end="", flush=True)
49
49
 
50
- # Saved workflow lifecycle through workflow-manager
50
+ # Preferred Authority-backed one-call lifecycle
51
+ execution = await dominus.workflow.ensure(
52
+ "wf://carebridge/report-cycle",
53
+ subject="PCM47474562",
54
+ company="summit-radiology",
55
+ inputs={"report_snapshot": "ar://carebridge/summit-radiology/production/snapshot/report-1"},
56
+ )
57
+
58
+ # Saved workflow lifecycle through workflow-manager when you need explicit artifacts
51
59
  run = await dominus.workflow.create_run(
52
60
  workflow_id="wf_saved_123",
53
61
  context={"report_ref": {"report_id": "r-1", "accession": "a-1", "session_number": "2"}},
@@ -154,6 +154,8 @@ from .helpers.cache import (
154
154
  from .helpers.core import (
155
155
  verify_jwt_locally,
156
156
  is_jwt_valid,
157
+ mint_selected_project_jwt,
158
+ delegate_jwt,
157
159
  )
158
160
 
159
161
  # Export trace utilities for distributed tracing
@@ -180,7 +182,7 @@ from .errors import (
180
182
  TimeoutError as DominusTimeoutError,
181
183
  )
182
184
 
183
- __version__ = "2.17.0"
185
+ __version__ = "2.18.1"
184
186
  __all__ = [
185
187
  # Main SDK instance
186
188
  "dominus",
@@ -250,6 +252,8 @@ __all__ = [
250
252
  # JWT verification utilities
251
253
  "verify_jwt_locally",
252
254
  "is_jwt_valid",
255
+ "mint_selected_project_jwt",
256
+ "delegate_jwt",
253
257
  # Trace utilities
254
258
  "generate_trace_id",
255
259
  "get_current_trace_id",
@@ -521,29 +521,31 @@ async def _ensure_valid_jwt(psk_token: str, sovereign_url: str) -> str:
521
521
  return jwt
522
522
 
523
523
 
524
- async def delegate_jwt(
524
+ async def mint_selected_project_jwt(
525
525
  psk_token: str,
526
526
  target_project_id: str,
527
527
  target_environment: str,
528
528
  use_shared: bool = False
529
529
  ) -> str:
530
530
  """
531
- Mint a delegated JWT for a target project.
531
+ Mint a selected-project JWT for a target project.
532
532
 
533
- This is used by admin-level services (like MCP) to get JWTs
534
- that allow operations on behalf of other projects.
533
+ This is used by admin-level services to act within a selected project
534
+ context. The compatibility route name remains `/jwt/delegate`, but
535
+ the canonical contract is selected-project targeting, not a separate
536
+ delegated token family.
535
537
 
536
538
  Args:
537
539
  psk_token: Admin PSK token (DOMINUS_TOKEN for admin project)
538
540
  target_project_id: UUID of the target project
539
541
  target_environment: Environment (development, staging, production)
540
- use_shared: If True, includes shared_project_id in claims
542
+ use_shared: Deprecated compatibility flag; selected-project JWTs infer shared context from the target project
541
543
 
542
544
  Returns:
543
- Delegated JWT token string with system=delegated
545
+ Selected-project JWT string
544
546
 
545
547
  Raises:
546
- RuntimeError: If delegation fails
548
+ RuntimeError: If selected-project minting fails
547
549
  """
548
550
  from ..config.endpoints import get_gateway_url, get_proxy_config
549
551
  gateway_url = get_gateway_url()
@@ -557,13 +559,11 @@ async def delegate_jwt(
557
559
  "X-Parent-Span-Id": get_current_span_id(),
558
560
  }
559
561
 
560
- # Body format for /jwt/delegate endpoint
561
562
  body_json = {
562
563
  "target_project_id": target_project_id,
563
564
  "target_environment": target_environment
564
565
  }
565
- if use_shared:
566
- body_json["use_shared"] = True
566
+ _ = use_shared
567
567
 
568
568
  body_b64 = _b64_encode(body_json)
569
569
 
@@ -576,13 +576,13 @@ async def delegate_jwt(
576
576
  result = _b64_decode(response.text)
577
577
 
578
578
  if not result.get("success"):
579
- error_msg = result.get("error", "Unknown delegation error")
580
- raise RuntimeError(f"Delegation error: {error_msg}")
579
+ error_msg = result.get("error", "Unknown selected-project mint error")
580
+ raise RuntimeError(f"Selected-project mint error: {error_msg}")
581
581
 
582
582
  data = result.get("data", {})
583
583
  jwt = data.get("token") or data.get("access_token")
584
584
  if not jwt:
585
- raise RuntimeError("No JWT token in delegation response")
585
+ raise RuntimeError("No JWT token in selected-project mint response")
586
586
 
587
587
  return jwt
588
588
 
@@ -594,10 +594,31 @@ async def delegate_jwt(
594
594
  error_detail = error_body.get("error") or str(error_body)
595
595
  except Exception:
596
596
  error_detail = e.response.text[:200] if e.response.text else str(e)
597
- raise RuntimeError(f"Delegation failed: {error_detail}") from e
597
+ raise RuntimeError(f"Selected-project JWT mint failed: {error_detail}") from e
598
598
 
599
599
  except Exception as e:
600
- raise RuntimeError(f"Delegation failed: {e}") from e
600
+ raise RuntimeError(f"Selected-project JWT mint failed: {e}") from e
601
+
602
+
603
+ async def delegate_jwt(
604
+ psk_token: str,
605
+ target_project_id: str,
606
+ target_environment: str,
607
+ use_shared: bool = False
608
+ ) -> str:
609
+ """
610
+ Compatibility alias for :func:`mint_selected_project_jwt`.
611
+
612
+ .. deprecated::
613
+ Prefer :func:`mint_selected_project_jwt` for new code; "delegated" naming is
614
+ legacy terminology only.
615
+ """
616
+ return await mint_selected_project_jwt(
617
+ psk_token=psk_token,
618
+ target_project_id=target_project_id,
619
+ target_environment=target_environment,
620
+ use_shared=use_shared,
621
+ )
601
622
 
602
623
 
603
624
  def verify_token_format(token: str) -> bool:
@@ -4,10 +4,14 @@ Artifacts Namespace - Temporary artifact storage.
4
4
  Provides auto-tiered artifact storage (Redis < 1MB, B2 >= 1MB)
5
5
  with TTL-based expiration. Routes through gateway to artifact-worker.
6
6
 
7
+ The artifact worker returns HTTP 410 for legacy paths (/store, /retrieve, /list, /delete).
8
+ This SDK only calls /api/artifact/v2/*; convenience ``store``/``retrieve``/``delete`` map
9
+ those shapes to V2 compatibility addressing (see ``store_v2`` for canonical addressed writes).
10
+
7
11
  Usage:
8
12
  from dominus import dominus
9
13
 
10
- # Store
14
+ # Prefer store_v2 / addressed refs for new code; legacy-shaped store maps to V2 internally
11
15
  result = await dominus.artifacts.store(data="base64data...", ttl_seconds=7200)
12
16
 
13
17
  # Retrieve
@@ -271,6 +275,21 @@ class ArtifactsNamespace:
271
275
  def __init__(self, client: "Dominus"):
272
276
  self._client = client
273
277
 
278
+ async def _legacy_project_context(self) -> tuple[str, str]:
279
+ """project_id and env from the cached service JWT (for V1-shaped -> V2 compat calls)."""
280
+ from ..helpers.core import _ensure_valid_jwt
281
+ from ..start import _BASE_URL, _TOKEN
282
+
283
+ jwt = await _ensure_valid_jwt(_TOKEN, _BASE_URL)
284
+ claims = await self._client.validate_jwt(jwt)
285
+ pid = _safe_string(claims.get("project_id"))
286
+ env = _safe_string(claims.get("env")) or "production"
287
+ if not pid:
288
+ raise RuntimeError(
289
+ "dominus.artifacts legacy methods require project_id on the service JWT",
290
+ )
291
+ return pid, env
292
+
274
293
  async def _api(
275
294
  self,
276
295
  endpoint: str,
@@ -293,19 +312,27 @@ class ArtifactsNamespace:
293
312
  category: Optional[str] = None,
294
313
  ) -> Dict[str, Any]:
295
314
  """
296
- List artifacts for the current project/environment.
315
+ List artifact heads (V2 / legacy kind) for the service JWT project.
297
316
 
298
- Args:
299
- limit: Max items to return (default 100, max 500)
300
- category: Optional category filter
301
-
302
- Returns:
303
- {artifacts: [...], count: int, total_size_bytes: int}
317
+ Category filtering is client-side when rows include ``category``.
304
318
  """
305
- body: Dict[str, Any] = {"limit": limit}
319
+ project_id, environment = await self._legacy_project_context()
320
+ lim = max(1, min(int(limit or 100), 500))
321
+ raw = await self.list_v2(
322
+ lim,
323
+ group="dominus",
324
+ owner=f"project:{project_id}",
325
+ environment=environment,
326
+ kind="legacy",
327
+ )
328
+ artifacts = list(raw.get("artifacts") or [])
306
329
  if category:
307
- body["category"] = category
308
- return await self._api("/api/artifact/list", body=body)
330
+ artifacts = [a for a in artifacts if (a or {}).get("category") == category]
331
+ count = raw.get("count", len(artifacts))
332
+ total = raw.get("total_size_bytes")
333
+ if total is None:
334
+ total = sum(int((a or {}).get("size_bytes") or 0) for a in artifacts)
335
+ return {"artifacts": artifacts, "count": count, "total_size_bytes": total}
309
336
 
310
337
  async def get_health(self) -> Dict[str, Any]:
311
338
  """Check artifact worker health."""
@@ -320,52 +347,56 @@ class ArtifactsNamespace:
320
347
  content_type: Optional[str] = None,
321
348
  ) -> Dict[str, Any]:
322
349
  """
323
- Store an artifact.
324
-
325
- Args:
326
- data: Base64-encoded data
327
- key: Optional caller-provided key (UUID generated if not provided)
328
- ttl_seconds: Time-to-live in seconds (default 3600)
329
- category: Optional category for organization
330
-
331
- Returns:
332
- {key, ref, storage_type, size_bytes, expires_at}
350
+ Legacy-shaped store mapped to V2 (dominus / project:{id} / legacy / encoded key).
333
351
  """
334
- body: Dict[str, Any] = {
335
- "data": data,
336
- "ttl_seconds": ttl_seconds,
352
+ import uuid
353
+
354
+ project_id, environment = await self._legacy_project_context()
355
+ use_key = key or str(uuid.uuid4())
356
+ v2 = await self.store_v2(
357
+ group="dominus",
358
+ owner=f"project:{project_id}",
359
+ environment=environment,
360
+ kind="legacy",
361
+ artifact_key=quote(use_key, safe=""),
362
+ data=data,
363
+ ttl_seconds=ttl_seconds,
364
+ category=category,
365
+ content_type=content_type,
366
+ )
367
+ disp = build_display_artifact_ref(use_key)
368
+ return {
369
+ "key": use_key,
370
+ "ref": v2.get("compatibility_ref") or v2.get("head_ref") or disp,
371
+ "storage_type": v2.get("storage_type") or "redis",
372
+ "size_bytes": v2.get("size_bytes", 0),
373
+ "expires_at": v2.get("expires_at") or "",
337
374
  }
338
- if key:
339
- body["key"] = key
340
- if category:
341
- body["category"] = category
342
- if content_type:
343
- body["content_type"] = content_type
344
- return await self._api("/api/artifact/store", body=body)
345
375
 
346
376
  async def retrieve(self, key: str) -> Dict[str, Any]:
347
377
  """
348
- Retrieve an artifact by key.
349
-
350
- Args:
351
- key: Artifact key
352
-
353
- Returns:
354
- {key, data, storage_type, size_bytes, expires_at}
378
+ Retrieve by key via V2 display ref + target_project_id (compat with legacy data).
355
379
  """
356
- return await self._api("/api/artifact/retrieve", body={"key": key})
380
+ project_id, _environment = await self._legacy_project_context()
381
+ return await self._api(
382
+ "/api/artifact/v2/retrieve",
383
+ body={
384
+ "ref": f"{DISPLAY_REF_PREFIX}{key}",
385
+ "target_project_id": project_id,
386
+ },
387
+ )
357
388
 
358
389
  async def delete(self, key: str) -> Dict[str, Any]:
359
- """
360
- Delete an artifact by key.
361
-
362
- Args:
363
- key: Artifact key
364
-
365
- Returns:
366
- {deleted: bool, key: str}
367
- """
368
- return await self._api("/api/artifact/delete", body={"key": key})
390
+ """Delete legacy compatibility head via V2 selector."""
391
+ project_id, environment = await self._legacy_project_context()
392
+ await self.delete_v2(
393
+ group="dominus",
394
+ owner=f"project:{project_id}",
395
+ environment=environment,
396
+ kind="legacy",
397
+ artifact_key=quote(key, safe=""),
398
+ )
399
+ return {"deleted": True, "key": key}
369
400
 
370
401
  async def cleanup(
371
402
  self,
@@ -373,19 +404,12 @@ class ArtifactsNamespace:
373
404
  limit: int = 100,
374
405
  ) -> Dict[str, Any]:
375
406
  """
376
- Cleanup expired artifacts (admin only).
377
-
378
- Args:
379
- force: Skip throttle check
380
- limit: Max items to clean (default 100)
407
+ HTTP /cleanup is not exposed on the worker; opportunistic cleanup runs after v2 store.
381
408
 
382
- Returns:
383
- {cleaned: int, skipped: bool}
409
+ Returns a skipped sentinel for API compatibility.
384
410
  """
385
- body: Dict[str, Any] = {"limit": limit}
386
- if force:
387
- body["force"] = True
388
- return await self._api("/api/artifact/cleanup", body=body)
411
+ del force, limit
412
+ return {"cleaned": 0, "skipped": True}
389
413
 
390
414
  def _selector_body(self, options: Dict[str, Any]) -> Dict[str, Any]:
391
415
  body: Dict[str, Any] = {}
@@ -11,6 +11,7 @@ import sys
11
11
  import time
12
12
  from datetime import datetime, timezone
13
13
  from typing import Any, Dict, List, Optional, Tuple, TYPE_CHECKING
14
+ from urllib.parse import urlencode
14
15
 
15
16
  if TYPE_CHECKING:
16
17
  from ..start import Dominus
@@ -321,6 +322,13 @@ class LogsNamespace:
321
322
  minutes: int = 10,
322
323
  limit: int = 100,
323
324
  trace_id: Optional[str] = None,
325
+ since: Optional[str] = None,
326
+ company: Optional[str] = None,
327
+ subject: Optional[str] = None,
328
+ run_id: Optional[str] = None,
329
+ deploy_id: Optional[str] = None,
330
+ installation_id: Optional[str] = None,
331
+ artifact_ref: Optional[str] = None,
324
332
  ) -> List[Dict[str, Any]]:
325
333
  """Tail recent logs from the Logs Worker.
326
334
 
@@ -330,18 +338,39 @@ class LogsNamespace:
330
338
  minutes: Time window — 1, 10, or 60 (default: 10)
331
339
  limit: Maximum results, 1-1000 (default: 100)
332
340
  trace_id: Optional trace_id filter to return only events from a specific trace
341
+ since: Optional ISO timestamp lower bound
342
+ company: Optional canonical company slug filter
343
+ subject: Optional domain subject filter
344
+ run_id: Optional workflow or Authority run id filter
345
+ deploy_id: Optional deploy authority id filter
346
+ installation_id: Optional managed-client installation id filter
347
+ artifact_ref: Optional artifact ref filter
333
348
 
334
349
  Returns:
335
350
  List of log event dicts
336
351
  """
337
- params = []
352
+ params: Dict[str, Any] = {}
338
353
  if minutes != 10:
339
- params.append(f"minutes={minutes}")
354
+ params["minutes"] = minutes
340
355
  if limit != 100:
341
- params.append(f"limit={limit}")
356
+ params["limit"] = limit
342
357
  if trace_id:
343
- params.append(f"trace_id={trace_id}")
344
- qs = "&".join(params)
358
+ params["trace_id"] = trace_id
359
+ if since:
360
+ params["since"] = since
361
+ if company:
362
+ params["company"] = company
363
+ if subject:
364
+ params["subject"] = subject
365
+ if run_id:
366
+ params["run_id"] = run_id
367
+ if deploy_id:
368
+ params["deploy_id"] = deploy_id
369
+ if installation_id:
370
+ params["installation_id"] = installation_id
371
+ if artifact_ref:
372
+ params["artifact_ref"] = artifact_ref
373
+ qs = urlencode(params)
345
374
  endpoint = f"/api/logs/tail?{qs}" if qs else "/api/logs/tail"
346
375
 
347
376
  try:
@@ -372,7 +401,7 @@ class LogsNamespace:
372
401
  Returns:
373
402
  List of log event dicts
374
403
  """
375
- return await self.tail(minutes=minutes, limit=limit)
404
+ return await self.tail(minutes=minutes, limit=limit, **kwargs)
376
405
 
377
406
  async def batch(
378
407
  self,