Skip to content
Open
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
85 changes: 85 additions & 0 deletions docs/runtime-wrapper-extension.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
# Governance Integration Point

`uipath-runtime` wraps runtimes with governance via a single direct
function, `apply_governance_wrapper`, gated by the
`EnablePythonGovernanceChecker` feature flag.

Governance contracts (feature-flag, exceptions, models) live in
`uipath.core.governance` (in `uipath-core`); the runtime-side wrapper
lives here in `uipath.runtime.governance`. Runtime has **no separate
`uipath-governance` dependency** — the contracts namespace is always
available because `uipath-core` is already a hard dep. When the flag
is off, `uipath.runtime.governance.wrapper` is **not imported** — its
transitive cost stays off the startup path.

## How it works

```
UiPathRuntimeFactoryRegistry.get(...)
↓ returns
UiPathWrappedRuntimeFactory.new_runtime(...)
↓ calls
apply_governance_wrapper(runtime, context, runtime_id)
if _is_governance_enabled():
from uipath.runtime.governance.wrapper import governance_wrapper # lazy
return governance_wrapper(runtime, context, runtime_id)
else:
return runtime # unwrapped, no governance import
```

## Feature flag

| Setting | Effect |
|---|---|
| `FeatureFlags.configure_flags({"EnablePythonGovernanceChecker": True})` (typically via gitops) | Governance is applied |
| `UIPATH_FEATURE_EnablePythonGovernanceChecker=true` env var | Governance is applied (fallback when no programmatic config) |
| Neither set | Governance **not** applied; `uipath.runtime.governance.wrapper` is **not imported** |

Resolution and fallback semantics come from `uipath-core`'s
`FeatureFlags.is_flag_enabled(..., default=False)`. Programmatic
configuration beats env var.

## API

```python
from uipath.runtime import (
GOVERNANCE_FEATURE_FLAG, # "EnablePythonGovernanceChecker"
apply_governance_wrapper, # the call-site
)
```

`apply_governance_wrapper(runtime, context, runtime_id)` is an
`async` function. It returns the original runtime untouched when the
flag is off or when the wrapper itself raises — governance failures
must never break agent execution.

## Why deferred-import matters

When the flag is off, `apply_governance_wrapper` returns before the
`from uipath.runtime.governance.wrapper import governance_wrapper` line
ever runs. That keeps governance's transitive imports — audit,
evaluator, OpenTelemetry, the policy index — entirely off the startup
hot path.

## Testing

Force the flag on/off per test via `FeatureFlags`:

```python
from uipath.core.feature_flags import FeatureFlags
from uipath.runtime.wrapper import GOVERNANCE_FEATURE_FLAG

# Force enable
FeatureFlags.configure_flags({GOVERNANCE_FEATURE_FLAG: True})

# Force disable
FeatureFlags.configure_flags({GOVERNANCE_FEATURE_FLAG: False})

# Reset (typically in a teardown fixture)
FeatureFlags.reset_flags()
```

Use `sys.modules` patching to stub `uipath.runtime.governance.wrapper`
when you need to assert against the wrapper invocation without
actually importing it — see `tests/test_wrapper.py` for the fixture.
28 changes: 27 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@ description = "Runtime abstractions and interfaces for building agents and autom
readme = { file = "README.md", content-type = "text/markdown" }
requires-python = ">=3.11"
dependencies = [
"uipath-core>=0.5.17, <0.6.0"
"uipath-core>=0.5.18, <0.6.0",
# Governance native-evaluator deps. Live here because the native
# evaluator implementation lives in uipath.runtime.governance.native;
# uipath-core only carries the small governance contracts.
"pyyaml>=6.0",
"vaderSentiment>=3.3.2", # sentiment_concern (A.3.3)
"chardet>=5.2.0", # encoding_concern (A.7.4)
]
classifiers = [
"Intended Audience :: Developers",
Expand Down Expand Up @@ -40,6 +46,7 @@ dev = [
"pytest-cov>=4.1.0",
"pytest-mock>=3.11.1",
"pre-commit>=4.1.0",
"types-PyYAML>=6.0",
]

[tool.hatch.build.targets.wheel]
Expand Down Expand Up @@ -83,6 +90,25 @@ no_implicit_reexport = true

disallow_untyped_defs = false

# Third-party governance-evaluator libs have no type stubs / py.typed marker
[[tool.mypy.overrides]]
module = [
"yaml",
"vaderSentiment.*",
"chardet",
"price_parser",
# uipath.platform.common is imported lazily from traces.py / audit
# sinks to read UiPathConfig context attributes. It's first-party but
# not a uipath-runtime dep, so its stubs aren't installable here.
"uipath.platform.*",
# Optional framework adapters; the absence of the framework simply
# means the adapter no-ops at import time.
"agents",
"langchain_core.*",
"langgraph.*",
]
ignore_missing_imports = true

[tool.pydantic-mypy]
init_forbid_extra = true
init_typed = true
Expand Down
7 changes: 7 additions & 0 deletions src/uipath/runtime/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@
)
from uipath.runtime.schema import UiPathRuntimeSchema
from uipath.runtime.storage import UiPathRuntimeStorageProtocol
from uipath.runtime.wrapper import (
GOVERNANCE_FEATURE_FLAG,
apply_governance_wrapper,
)

__all__ = [
"UiPathExecuteOptions",
Expand Down Expand Up @@ -73,4 +77,7 @@
"UiPathResumeTriggerName",
"UiPathChatProtocol",
"UiPathChatRuntime",
# Governance integration (direct, FF-gated, lazy import)
"GOVERNANCE_FEATURE_FLAG",
"apply_governance_wrapper",
]
70 changes: 70 additions & 0 deletions src/uipath/runtime/governance/audit/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
"""Audit sink framework for governance events.

This module provides a pluggable audit system that supports multiple
output destinations (sinks) for governance events. Events are emitted
to all registered sinks, allowing flexible audit trail configuration.

Usage::

from uipath.runtime.governance.audit import get_audit_manager, AuditEvent

# Get the global audit manager
manager = get_audit_manager()

# Emit an event (goes to all registered sinks)
manager.emit(AuditEvent(
event_type="rule_evaluation",
trace_id="abc-123",
agent_name="my-agent",
data={"rule_id": "ASI-01", "matched": True},
))

# Register a custom sink
manager.register_sink(MyCustomSink())

Built-in sinks:

- :class:`TracesAuditSink` – OpenTelemetry spans for Orchestrator Traces UI
- :class:`ConsoleAuditSink` – stderr output for debugging

Sink registration:

- The ``traces`` sink (OpenTelemetry spans → Orchestrator audit UI) is
**platform-mandated** and always registered. It cannot be disabled by
a developer-side env var — governance is platform-owned.
- The ``console`` sink is a developer aid for local debugging and is
opt-in via env var.

Environment variables (developer-facing, console only):

- ``UIPATH_AUDIT_VERBOSE`` – verbose console output.
- ``UIPATH_GOVERNANCE_CONSOLE_LOG`` – enable the console sink.
"""

from .base import (
AuditEvent,
AuditManager,
AuditSink,
EventType,
get_audit_manager,
reset_audit_manager,
)
from .console import ConsoleAuditSink
from .factory import create_sink
from .traces import TracesAuditSink

__all__ = [
# Core classes
"AuditEvent",
"AuditManager",
"AuditSink",
"EventType",
# Global manager
"get_audit_manager",
"reset_audit_manager",
# Factory
"create_sink",
# Built-in sinks
"ConsoleAuditSink",
"TracesAuditSink",
]
Loading
Loading