Skip to content

Commit d763f4f

Browse files
viswa-uipathclaude
andcommitted
feat(governance): platform self-resolves trace_id when caller leaves it empty
Lets the runtime layer (uipath-runtime-python) stop carrying ``trace_id`` through ``UiPathGovernedRuntime`` / ``GuardrailCompensator``. The runtime emits compensation requests with ``trace_id=""`` and the platform fills in the canonical agent trace id at HTTP-call time via the existing ``resolve_trace_id()`` helper — same fallback ``track_event`` (PR #1745) already uses. uipath-core - ``GovernRequest.trace_id`` relaxes from required ``str`` to ``str = ""`` default. Docstring documents the platform-side self-resolve contract so wire callers know an empty value is legitimate. uipath-platform - ``GovernanceService._compensate`` / ``_compensate_async`` now call a new ``_resolve_request_trace_id()`` helper before the POST. When ``request.trace_id`` is empty the helper resolves via ``resolve_trace_id()`` (env → LLMOps external span → OTel current span). Caller-supplied values win — the runtime captures live OTel context across its background-pool hop via ``contextvars.copy_context()``, so when the worker calls ``provider.compensate(...)`` the platform-side resolver sees the agent's live span and returns the same canonical id. Caller-supplied non-empty trace ids continue to pass through unchanged. Tests - uipath-core governance suite: 32 passed. - uipath-platform governance service suite: 27 passed. - ruff + mypy clean on ``src/uipath/core/governance`` and ``src/uipath/platform/governance``. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent afcf7a5 commit d763f4f

2 files changed

Lines changed: 38 additions & 3 deletions

File tree

packages/uipath-core/src/uipath/core/governance/providers.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,13 @@ class GovernRequest(BaseModel):
106106
them by leaving them ``None``. How unset fields are resolved (e.g.
107107
auto-filled from environment) is the concrete provider's concern,
108108
not part of this wire contract.
109+
110+
``trace_id`` is also optional — when the caller passes an empty
111+
string (or omits the field), the concrete provider is expected to
112+
resolve the canonical trace id itself at HTTP-call time (typically
113+
via :func:`uipath.platform.common._base_service.resolve_trace_id`).
114+
Hosts that already hold a resolved value pass it in; hosts that
115+
don't can let the provider do the work.
109116
"""
110117

111118
model_config = ConfigDict(populate_by_name=True)
@@ -114,7 +121,7 @@ class GovernRequest(BaseModel):
114121
rules: list[FiredRule]
115122
data: dict[str, Any]
116123
hook: str
117-
trace_id: str = Field(alias="traceId")
124+
trace_id: str = Field(default="", alias="traceId")
118125
src_timestamp: str # wire key is intentionally snake_case
119126
agent_name: str = Field(alias="agentName")
120127
runtime_id: str = Field(alias="runtimeId")

packages/uipath-platform/src/uipath/platform/governance/_governance_service.py

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
PolicyResponse,
2323
)
2424

25-
from ..common._base_service import BaseService
25+
from ..common._base_service import BaseService, resolve_trace_id
2626
from ..common._config import UiPathConfig
2727
from ..common._service_url_overrides import (
2828
inject_routing_headers,
@@ -262,18 +262,46 @@ def _compensate(self, request: GovernRequest) -> None:
262262
to satisfy :class:`uipath.core.governance.GovernanceCompensationProvider`
263263
without unpacking the request. The public ergonomic counterpart
264264
is :meth:`compensate`.
265+
266+
When ``request.trace_id`` is empty the service resolves the
267+
canonical trace id itself via :func:`resolve_trace_id` — same
268+
fallback ``track_event`` uses. Callers that have a resolved
269+
value still pass it in; callers that don't (e.g. the runtime
270+
layer, which intentionally stays env-free) leave it empty and
271+
let the service do the work.
265272
"""
273+
request = self._resolve_request_trace_id(request)
266274
url, headers = self._build_org_scoped_request(GOVERN_API_PATH)
267275
payload = self._build_govern_payload(request)
268276
self.request("POST", url=url, headers=headers, json=payload)
269277

270278
@traced(name="governance_compensate", run_type="uipath")
271279
async def _compensate_async(self, request: GovernRequest) -> None:
272-
"""Async variant of :meth:`_compensate`."""
280+
"""Async variant of :meth:`_compensate`.
281+
282+
Same ``trace_id`` self-resolution behavior as the sync variant.
283+
"""
284+
request = self._resolve_request_trace_id(request)
273285
url, headers = self._build_org_scoped_request(GOVERN_API_PATH)
274286
payload = self._build_govern_payload(request)
275287
await self.request_async("POST", url=url, headers=headers, json=payload)
276288

289+
@staticmethod
290+
def _resolve_request_trace_id(request: GovernRequest) -> GovernRequest:
291+
"""Fill ``request.trace_id`` from :func:`resolve_trace_id` when empty.
292+
293+
Caller-supplied values win — the runtime captures on the hook
294+
thread (via ``contextvars.copy_context`` for the background
295+
pool) and the resolver here only fires when nothing was
296+
captured.
297+
"""
298+
if request.trace_id:
299+
return request
300+
resolved = resolve_trace_id()
301+
if not resolved:
302+
return request
303+
return request.model_copy(update={"trace_id": resolved})
304+
277305
# ── Internals ────────────────────────────────────────────────────
278306

279307
def _build_org_scoped_request(self, path: str) -> tuple[str, dict[str, str]]:

0 commit comments

Comments
 (0)