Skip to content

refactor(governance): hoist policy fetch to host; drop PolicyLoader#133

Merged
viswa-uipath merged 2 commits into
feat/governance-guardrail-compensationfrom
feat/governance-policy-fetch-hoist
Jun 26, 2026
Merged

refactor(governance): hoist policy fetch to host; drop PolicyLoader#133
viswa-uipath merged 2 commits into
feat/governance-guardrail-compensationfrom
feat/governance-policy-fetch-hoist

Conversation

@viswa-uipath

Copy link
Copy Markdown

Summary

Addresses architecture-review item Sec 2.4"policy fetch belongs to the async host, not the runtime layer."

GovernanceRuntime is now a pure consumer of a resolved policy snapshot. The host (uipath CLI) does the async fetch via GovernancePolicyProvider.get_policy_async, compiles the YAML through build_policy_index_from_yaml, and passes the resulting PolicyIndex + EnforcementMode into the constructor. The runtime layer no longer carries any I/O or background-thread machinery.

What changed

  • Deleted PolicyLoader (343 LOC) along with its hand-rolled future on threading.Thread + threading.Event. Async fetch is the async host's job.
  • New GovernanceRuntime constructor signature:
    GovernanceRuntime(
        delegate,
        policy_index,         # already resolved
        enforcement_mode,     # already resolved
        *,
        trace_id=None,
    )
    No more policy_provider / is_conversational params. Agent-type selection lives in the host's PolicyContext construction.
  • Exposed build_policy_index_from_yaml from native/__init__.py so the host has a clean import path for the compile step.
  • Deleted StubPolicyProvider test helper and test_enforcement_mode_default.py — mode is a constructor arg now, no default-resolution path to test.

Net diff

`-890 LOC` (mostly the loader, its tests, and the stub provider).

Constraint check

pyproject.toml already pins uipath-core>=0.5.19, <0.6.0. The get_policy_async protocol method (unreleased 0.5.23) falls within that range. No bump needed in this PR.

Stacking

Layered on top of #124 (feat/governance-evaluator). The host-side wiring in uipath-python is a separate follow-up PR — it consumes the new constructor shape.

Test plan

  • `uv run ruff check .` — clean
  • `uv run mypy src/uipath/runtime/governance` — clean
  • `uv run pytest` — 326 passed, 1 skipped (the 31-test drop = deleted loader + enforcement-default tests)
  • `bandit -r src/uipath/runtime/governance` — clean
  • Monorepo grep for `PolicyLoader` / `StubPolicyProvider` / `policy_provider` / `is_conversational` — zero hits in `src/` and `tests/`
  • uipath-python host wiring PR (follow-up) consumes the new constructor

🤖 Generated with Claude Code

GovernanceRuntime now takes a resolved PolicyIndex + EnforcementMode at
construction. The host (uipath CLI) does the async fetch via the
GovernancePolicyProvider, compiles the YAML through
build_policy_index_from_yaml, and hands the snapshot in. The runtime
becomes a passive consumer; the host owns lifecycle.

- Delete PolicyLoader (343 LOC) and its hand-rolled future
  (threading.Thread + Event). Async I/O belongs to the async host.
- Delete StubPolicyProvider test helper + enforcement-mode-default
  tests (the mode is now a constructor arg, no default needed).
- GovernanceRuntime ctor: (delegate, policy_index, enforcement_mode,
  *, trace_id=None). No more policy_provider / is_conversational
  parameters. Agent-type selection lives in the host's PolicyContext
  construction.
- Expose build_policy_index_from_yaml from native/__init__.py for the
  host's compile step.

Net: -890 LOC. Addresses architecture-review item Sec 2.4.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- runtime.py: drop §2.4 PR ref and historical "staging caveat"
  language from module/class docstrings; drop downstream LangChain
  class name from the generic runtime layer; replace defensive
  getattr(result, "output", None) with result.output (the outer
  fail-open try/except already covers a malformed delegate).
- evaluator.py: fix stale "loader" reference in docstring →
  GovernanceRuntime.
- _audit/traces.py: rewrite three comments referencing the deleted
  PolicyLoader to describe the per-runtime model.
- _audit/base.py: rewrite two docstrings referencing the deleted
  PolicyLoader.
- native/_yaml_to_index.py: fix broken :mod: link to the deleted
  native.loader module; describe the platform-host compile flow.

No behavior change. ruff/mypy clean, 326 passed + 1 skipped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
return str(payload)


class GovernanceRuntime:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's name it UiPathGovernedRuntime to be consistent with the other runtime names in this repo

logger = logging.getLogger(__name__)


def _serialize_payload(payload: Any) -> str:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we have serialization helpers in uipath-core and we can get rid of this one

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a difference in the implementation.

Uploading image.png…

return self._enforcement_mode

@property
def trace_id(self) -> str | None:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure why we need to expose these properties (trace_id, enforcement_mode, policy_index)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

policy_index, and enforcement_mode is needed to run the evaluator in runtime. Rules for each hook resides within that.

@viswa-uipath viswa-uipath changed the base branch from feat/governance-evaluator to feat/governance-guardrail-compensation June 26, 2026 09:33
@viswa-uipath viswa-uipath merged commit 079569e into feat/governance-guardrail-compensation Jun 26, 2026
79 of 84 checks passed
viswa-uipath added a commit that referenced this pull request Jun 26, 2026
…contextvars

Addresses cristipufu's PR #133 review (rename + drop properties + drop
local serializer) and the wider point that ``trace_id`` shouldn't live
on the generic runtime layer at all. The platform side
(uipath-platform / PR #1761) now self-resolves ``GovernRequest.trace_id``
when the runtime sends an empty value, and the compensator preserves
live OTel context across its background-pool hop via
``contextvars.copy_context()`` — so the platform-side resolver still
sees the agent's live span when the worker calls
``provider.compensate(...)``.

Runtime wrapper (``runtime.py``)
- Renamed ``GovernanceRuntime`` → ``UiPathGovernedRuntime`` to match
  the repo's other runtime names (UiPathResumableRuntime,
  UiPathDebugRuntime, etc.).
- Dropped ``trace_id`` ctor arg.
- Dropped the ``policy_index`` / ``enforcement_mode`` / ``trace_id``
  read-only properties — they were dead surface area; consumers
  receive the values from the host at construction time and don't
  need to read them back through the wrapper.
- Replaced the bespoke ``_serialize_payload`` (4 branches + nested
  try/except) with a 9-line version that delegates the complex case
  to ``uipath.core.serialization.serialize_object``. ``None → ""`` and
  ``str → passthrough`` stay as governance-scan special cases (the
  evaluator's regex / contains / sentiment checks would mismatch
  against ``"null"`` or ``'"hello"'``).

Compensator (``guardrail_compensation.py``)
- Dropped ``trace_id`` ctor arg.
- Dropped the per-call ``trace_id`` arg from ``submit()``.
- Deleted the ``_resolve_trace_id(supplied, fallback)`` helper.
- Added ``import contextvars``; ``submit()`` snapshots the caller's
  context (``ctx = contextvars.copy_context()``) and the pool runs
  the worker as ``pool.submit(ctx.run, _run)``. The worker therefore
  sees the agent's live OTel span; the platform's ``resolve_trace_id``
  resolves correctly on the worker thread.
- ``GovernRequest.trace_id="" `` on the wire — platform fills.

Evaluator (``native/evaluator.py``)
- All six ``evaluate_*`` per-call methods now default
  ``trace_id: str = ""`` (was required). Callers that already supply
  a value (e.g. legacy callers passing through resolved ids)
  continue to work unchanged.
- ``_dispatch_compensation`` no longer passes ``trace_id`` to
  ``compensator.submit(...)``.

Tests
- ``test_governance_runtime.py``: rewritten for the renamed class +
  dropped properties + dropped ctor arg. Asserts internal
  ``_policy_index`` / ``_enforcement_mode`` instead of properties.
- ``test_guardrail_compensation.py``: dropped the four
  ``_resolve_trace_id`` tests + the constructor-trace-id test.
  Replaced ``test_submit_captures_live_trace_before_thread_hop`` with
  ``test_submit_propagates_otel_context_to_worker_thread``: now
  asserts that ``trace.get_current_span()`` *inside the worker
  callable* returns the agent's live span (proves the contextvars
  snapshot propagation works end-to-end). 319 passed, 1 skipped.
- ``conftest.py`` / ``test_traces_severity.py``: docstring renames
  only.

ruff + mypy clean (10 source files). Test count: 319 passed, 1 skipped
(was 357 — drop is the deleted ``_resolve_trace_id`` tests + the
ctor-trace-id test).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants