Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/uipath-core/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "uipath-core"
version = "0.5.30"
version = "0.5.31"
description = "UiPath Core abstractions"
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
Expand Down
8 changes: 8 additions & 0 deletions packages/uipath-core/src/uipath/core/governance/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@

from .config import (
GOVERNANCE_FEATURE_FLAG,
REGO_FEATURE_FLAG,
is_governance_enabled,
is_rego_enabled,
)
from .exceptions import (
GovernanceBlockException,
Expand All @@ -19,10 +21,12 @@
)
from .models import Action, AuditRecord, EnforcementMode, LifecycleHook, RuleEvaluation
from .providers import (
AllPoliciesResponse,
FiredRule,
GovernanceCompensationProvider,
GovernancePolicyProvider,
GovernRequest,
HookBundle,
PolicyContext,
PolicyResponse,
)
Expand All @@ -36,17 +40,21 @@
"RuleEvaluation",
# Config
"GOVERNANCE_FEATURE_FLAG",
"REGO_FEATURE_FLAG",
"is_governance_enabled",
"is_rego_enabled",
# Exceptions
"GovernanceBlockException",
"GovernanceConfigError",
"GovernanceViolation",
"Severity",
# Provider protocols + wire models
"AllPoliciesResponse",
"FiredRule",
"GovernanceCompensationProvider",
"GovernancePolicyProvider",
"GovernRequest",
"HookBundle",
"PolicyContext",
"PolicyResponse",
]
22 changes: 22 additions & 0 deletions packages/uipath-core/src/uipath/core/governance/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
# same toggle.
GOVERNANCE_FEATURE_FLAG = "EnablePythonGovernanceChecker"

# Feature flag name controlling whether the Rego/WASM evaluator runs.
# Independent of the native evaluator flag — both can be enabled simultaneously.
REGO_FEATURE_FLAG = "EnablePythonGovernanceRegoEvaluator"


def is_governance_enabled() -> bool:
"""Return whether the ``EnablePythonGovernanceChecker`` flag is enabled.
Expand All @@ -35,3 +39,21 @@ def is_governance_enabled() -> bool:
2. Default ``False`` (governance disabled).
"""
return FeatureFlags.is_flag_enabled(GOVERNANCE_FEATURE_FLAG, default=False)


def is_rego_enabled() -> bool:
"""Return whether the ``EnablePythonGovernanceRegoEvaluator`` flag is enabled.

Rego evaluation is **off by default** — the flag must be explicitly
set to ``true`` (programmatically via the ``FeatureFlags`` registry,
or via the ``UIPATH_FEATURE_EnablePythonGovernanceRegoEvaluator`` env
var) for this function to return ``True``.

Resolution order:

1. :meth:`uipath.core.feature_flags.FeatureFlagsManager.is_flag_enabled` -
the in-process programmatic registry (typically populated from
gitops) and its own ``UIPATH_FEATURE_<name>`` env-var fallback.
2. Default ``False`` (Rego evaluation disabled).
"""
return FeatureFlags.is_flag_enabled(REGO_FEATURE_FLAG, default=False)
48 changes: 48 additions & 0 deletions packages/uipath-core/src/uipath/core/governance/providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,54 @@ def _coerce_mode(cls, value: object) -> EnforcementMode | None:
return None


class HookBundle(BaseModel):
"""Metadata for one hook's WASM policy bundle.

Returned as an element of :class:`AllPoliciesResponse` from the
``/all-policies/{tenant_id}`` endpoint.

Attributes:
hook_type: Lifecycle hook identifier (e.g. ``"before_agent"``).
bundle_url: Pre-signed URL for the WASM ``.tar.gz`` bundle.
No platform auth required — the URL carries its own credentials.
etag: Server-assigned ETag for the bundle. Used by the Rego loader
to skip unchanged bundles on background refresh. ``None`` when
the server does not provide one.
"""

model_config = ConfigDict(populate_by_name=True, extra="ignore")

hook_type: str = Field(alias="hookType")
bundle_url: str = Field(alias="bundleUrl")
etag: str | None = Field(default=None)


class AllPoliciesResponse(BaseModel):
"""Parsed response from the ``/all-policies/{tenant_id}`` endpoint.

Wire envelope::

{
"hookBundles": [
{
"hookType": "before_agent",
"bundleUrl": "https://url.example.com/...",
"etag": "abc123"
}
]
}

Attributes:
hook_bundles: One entry per lifecycle hook that has a compiled
WASM bundle. Empty when no policies are configured for the
tenant.
"""

model_config = ConfigDict(populate_by_name=True, extra="ignore")

hook_bundles: list[HookBundle] = Field(default_factory=list, alias="hookBundles")


class FiredRule(BaseModel):
"""Per-rule metadata carried in the ``/runtime/govern`` payload.

Expand Down
2 changes: 1 addition & 1 deletion packages/uipath-platform/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "uipath-platform"
version = "0.2.6"
version = "0.2.7"
description = "HTTP client library for programmatic access to UiPath Platform"
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from ._governance_provider import UiPathPlatformGovernanceProvider
from ._governance_service import GovernanceService
from .compensate import FiredRule, GovernRequest
from .policy import PolicyContext, PolicyResponse
from .policy import AllPoliciesResponse, HookBundle, PolicyContext, PolicyResponse

# ``_live_track_event_dispatcher.LiveTrackEventDispatcher`` is intentionally
# **not** re-exported. It is host-wiring glue (the runtime sink's
Expand All @@ -20,9 +20,11 @@
# )

__all__ = [
"AllPoliciesResponse",
"FiredRule",
"GovernRequest",
"GovernanceService",
"HookBundle",
"PolicyContext",
"PolicyResponse",
"UiPathPlatformGovernanceProvider",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
- ``POST /{org}/agenticgovernance_/api/v1/runtime/govern`` — compensating
governance call fired when a ``guardrail_fallback`` rule matches
(see :meth:`GovernanceService.compensate`).
- ``GET /{org}/agenticgovernance_/api/v1/all-policies/{tenant_id}`` — fetch
WASM bundle metadata for all hooks (see :meth:`GovernanceService.retrieve_all_policies`).

A third backend endpoint —
``POST /{org}/agenticgovernance_/api/v1/runtime/log`` — emits custom
Expand All @@ -23,6 +25,7 @@

from uipath.core import traced
from uipath.core.governance import (
AllPoliciesResponse,
FiredRule,
GovernRequest,
PolicyContext,
Expand All @@ -44,6 +47,7 @@
POLICY_API_PATH = "api/v1/runtime/policy"
GOVERN_API_PATH = "api/v1/runtime/govern"
LOG_API_PATH = "api/v1/runtime/log"
ALL_POLICIES_API_PATH = "api/v1/all-policies"
AGENT_TYPE_PARAM = "agentType"

# Caller-set correlation id that becomes the App Insights ``operation_Id``
Expand Down Expand Up @@ -144,6 +148,104 @@ async def get_policy_async(self, context: PolicyContext) -> PolicyResponse:
is_conversational=context.is_conversational
)

# ── Rego WASM bundle fetch ────────────────────────────────────────

@traced(name="governance_retrieve_all_policies", run_type="uipath")
def retrieve_all_policies(self) -> AllPoliciesResponse:
"""Fetch WASM bundle metadata for all hooks for the active tenant.

Calls ``GET /{org}/agenticgovernance_/api/v1/all-policies/{tenant_id}``
and returns the list of :class:`HookBundle` objects — one per
lifecycle hook that has a compiled WASM policy bundle. Download
each bundle's bytes separately with :meth:`download_bundle`.

Returns:
AllPoliciesResponse: List of hook bundles with pre-signed
URLs and ETags for cache-conditional re-fetches. The list
is empty when no Rego policies are configured for the
tenant.

Raises:
ValueError: If ``UiPathConfig.organization_id`` or
``UiPathConfig.tenant_id`` is not set.
EnrichedException: If the backend returns a non-2xx response.

Examples:
```python
from uipath.platform import UiPath

client = UiPath()
resp = client.governance.retrieve_all_policies()
for bundle in resp.hook_bundles:
data = client.governance.download_bundle(bundle.bundle_url)
```
"""
url, headers = self._build_org_scoped_request(
f"{ALL_POLICIES_API_PATH}/{UiPathConfig.tenant_id}"
)
response = self.request("GET", url=url, headers=headers)
return AllPoliciesResponse.model_validate(response.json())

@traced(name="governance_retrieve_all_policies", run_type="uipath")
async def retrieve_all_policies_async(self) -> AllPoliciesResponse:
"""Asynchronously fetch WASM bundle metadata for all hooks.

See :meth:`retrieve_all_policies` for parameter and return semantics.
"""
url, headers = self._build_org_scoped_request(
f"{ALL_POLICIES_API_PATH}/{UiPathConfig.tenant_id}"
)
response = await self.request_async("GET", url=url, headers=headers)
return AllPoliciesResponse.model_validate(response.json())

def download_bundle(self, bundle_url: str) -> bytes:
"""Download a WASM bundle from a pre-signed URL.

The URL returned by :meth:`retrieve_all_policies` is pre-signed
and carries its own credentials — no platform Bearer token is
sent. Callers should cache the result on disk; use the
:attr:`~HookBundle.etag` from :class:`AllPoliciesResponse` to
skip unchanged bundles on subsequent fetches.

Args:
bundle_url: Pre-signed URL for the WASM ``.tar.gz`` bundle,
as returned in :attr:`HookBundle.bundle_url`.

Returns:
Raw bytes of the ``.tar.gz`` bundle file.

Raises:
httpx.HTTPStatusError: If the returns a non-2xx response.

Examples:
```python
from uipath.platform import UiPath

client = UiPath()
resp = client.governance.retrieve_all_policies()
for bundle in resp.hook_bundles:
data = client.governance.download_bundle(bundle.bundle_url)
# write data to disk ...
```
"""
import httpx

response = httpx.get(bundle_url, follow_redirects=True)
response.raise_for_status()
return response.content

async def download_bundle_async(self, bundle_url: str) -> bytes:
"""Asynchronously download a WASM bundle from a pre-signed URL.

See :meth:`download_bundle` for parameter and return semantics.
"""
import httpx

async with httpx.AsyncClient() as client:
response = await client.get(bundle_url, follow_redirects=True)
response.raise_for_status()
return response.content

# ── Compensating governance call ─────────────────────────────────

def compensate(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
keeps the existing ``uipath.platform.governance`` import paths working.
"""

from uipath.core.governance import PolicyContext, PolicyResponse
from uipath.core.governance import (
AllPoliciesResponse,
HookBundle,
PolicyContext,
PolicyResponse,
)

__all__ = ["PolicyContext", "PolicyResponse"]
__all__ = ["AllPoliciesResponse", "HookBundle", "PolicyContext", "PolicyResponse"]
Loading
Loading