feat(cli): shared host-side governance bootstrap for run and debug#1778
feat(cli): shared host-side governance bootstrap for run and debug#1778viswa-uipath wants to merge 1 commit into
Conversation
1a11593 to
04ad14f
Compare
🚨 Heads up:
|
38435f6 to
9d066a2
Compare
9d066a2 to
91a66d9
Compare
🚨 Heads up:
|
91a66d9 to
6d482ee
Compare
|
shouldn t we apply the governance to evals as well? |
| # rename without coordinating with the backend team. | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
| AGENT_TYPE_LOWCODE: Final[str] = "uipath_lowcode" |
There was a problem hiding this comment.
Please remove low-code runtime detection from uipath-python. Public OSS packages should not know about the low-code runtime. If that runtime needs a telemetry label, its factory should return it through UiPathRuntimeFactorySettings.framework, for example low_code, and the CLI should only forward that value.
|
Overall blocking concern: this PR crosses package boundaries by making the public The clean contract is to add |
| return None | ||
| if not response.policies: | ||
| return None | ||
|
|
There was a problem hiding this comment.
nit: combine under the same condition
| ) | ||
| return None | ||
|
|
||
| # The compensator no longer carries a trace id — the worker |
There was a problem hiding this comment.
same issue here identified on every almost other PR. stale comments from coding agents. where the code is self-explanatory please remove comments (or trim them). we are just polluting the code
| # Wrap the provider's async track_event in a non-blocking dispatcher | ||
| # so the AuditManager's sync ``track_event`` invocation never blocks | ||
| # on the ``/runtime/log`` HTTP round-trip. The dispatcher owns a | ||
| # private background asyncio loop; register its shutdown at | ||
| # interpreter exit as a fallback in case the caller forgets to | ||
| # invoke the returned ``dispose`` callable. |
| ) | ||
| new_runtime_kwargs: dict[str, Any] = {} | ||
| if governance_bootstrap is not None: | ||
| new_runtime_kwargs["evaluator"] = governance_bootstrap[ |
There was a problem hiding this comment.
Please do not index into this governance tuple. This is hard to read and fragile because the tuple mixes evaluator, policy index, mode, and dispose. Return a small named object (for example GovernanceBootstrap) or unpack once into named variables, then use governance.evaluator / governance_evaluator here.
| ) | ||
| new_runtime_kwargs: dict[str, Any] = {} | ||
| if governance_bootstrap is not None: | ||
| new_runtime_kwargs["evaluator"] = governance_bootstrap[ |
There was a problem hiding this comment.
Please do not index into this governance tuple. This is hard to read and fragile because the tuple mixes evaluator, policy index, mode, and dispose. Return a small named object (for example GovernanceBootstrap) or unpack once into named variables, then use governance.evaluator / governance_evaluator here.
| # exits. Idempotent. | ||
| live_tracking_processor.shutdown() | ||
| if governance_dispose is not None: | ||
| # Drain the track-event dispatcher (bounded by |
| # Drain the live-tracking span exporter pool so | ||
| # in-flight spans complete before the process | ||
| # exits. Idempotent. |
| f"Governance enabled (mode={response.mode.value}, " | ||
| f"packs={list(policy_index.pack_names)})" | ||
| ) | ||
| return evaluator, policy_index, response.mode, dispose |
There was a problem hiding this comment.
Return GovernanceBootstrap(...) here instead of a tuple. That removes the tuple indexing and duplicate unpacking in cli_run / cli_debug.
| # --------------------------------------------------------------------------- | ||
| # Agent-framework constants | ||
| # | ||
| # Stamped on every governance audit event as |
There was a problem hiding this comment.
Please remove this long framework explanation. The registry itself should go away, and this comment documents cross-repo/framework details that uipath should not own.
| "resolve_governance", | ||
| ] | ||
|
|
||
| # --------------------------------------------------------------------------- |
There was a problem hiding this comment.
Please remove these banner comments. The constant names already identify the group, and the separator blocks add noise without explaining a constraint.
| async def resolve_governance() -> ( | ||
| tuple[GovernanceEvaluator, PolicyIndex, EnforcementMode, Callable[[], None]] | None | ||
| ): | ||
| """Host-side governance bootstrap. |
There was a problem hiding this comment.
Please reduce this docstring. It lists the implementation steps line by line and repeats the body. Keep only the caller contract, especially after replacing the tuple with a named bootstrap object.
| ) | ||
| return None | ||
|
|
||
| # The compensator no longer carries a trace id — the worker |
There was a problem hiding this comment.
Please remove this comment. It explains a previous trace-id implementation instead of a non-obvious constraint in the current code.
| atexit.register(track_event_dispatcher.shutdown) | ||
|
|
||
| def dispose() -> None: | ||
| # Unregister the atexit fallback first so the dispatcher isn't |
There was a problem hiding this comment.
Please remove or reduce this. atexit.unregister(...) followed by shutdown() already shows what happens, and the idempotency detail belongs on the dispatcher API if it needs documenting.
| # request time when ``GovernRequest.trace_id`` is ``None``. | ||
| compensator = GuardrailCompensator(provider) | ||
|
|
||
| # Wrap the provider's async track_event in a non-blocking dispatcher |
There was a problem hiding this comment.
Please reduce this to one short sentence if needed. The current block narrates the next two lines and the dispatcher lifecycle should be obvious once it is owned by the bootstrap object.
| enforcement_mode, | ||
| governance_dispose, | ||
| ) = governance_bootstrap | ||
| # ``UiPathGovernedRuntime`` no longer accepts an |
There was a problem hiding this comment.
Please remove this comment. It documents a removed on_dispose hook and explains the code path instead of letting the cleanup below show ownership.
| enforcement_mode, | ||
| _, | ||
| ) = governance_bootstrap | ||
| # ``UiPathGovernedRuntime`` no longer accepts an |
There was a problem hiding this comment.
Please remove this comment too. It repeats the removed on_dispose detail from cli_run; after the bootstrap shape is fixed, this should not need explanation.
| if factory: | ||
| await factory.dispose() | ||
| if live_tracking_processor is not None: | ||
| # Drain the live-tracking span exporter pool so |
There was a problem hiding this comment.
Please remove this comment. live_tracking_processor.shutdown() is self-explanatory here, and the idempotency detail belongs on the processor API if needed.
| # exits. Idempotent. | ||
| live_tracking_processor.shutdown() | ||
| if governance_dispose is not None: | ||
| # Drain the track-event dispatcher (bounded by |
There was a problem hiding this comment.
Please remove this comment. The call to governance_dispose() already shows the cleanup boundary, and the extra lifecycle narration is noise.
| @@ -0,0 +1,689 @@ | |||
| """Tests for the shared host-side governance bootstrap. | |||
There was a problem hiding this comment.
Please reduce this test module docstring. The file name and test names already describe coverage; keep only setup constraints that are not obvious from the fixtures.
| from uipath.core.governance import EnforcementMode | ||
| from uipath.runtime.governance.native.models import PolicyIndex | ||
|
|
||
| # --------------------------------------------------------------------------- |
There was a problem hiding this comment.
Please remove these section banners. The test class and function names already provide the structure, so these separators just add noise.
| # --------------------------------------------------------------------------- | ||
|
|
||
|
|
||
| class TestConstants: |
There was a problem hiding this comment.
Please remove or reduce this class docstring. The assertions below already show that these constants are wire values; if the contract needs documentation, keep it as a short comment near the constants only.
| def test_returns_coded_when_any_framework_marker_present( | ||
| self, marker: str, cwd: Path, uipath_config_path: Path | ||
| ) -> None: | ||
| """Every framework in ``_FRAMEWORK_MARKERS`` marks the project as |
There was a problem hiding this comment.
Please remove this docstring. It restates the parametrization and the assertion, and this whole marker-based source of truth should go away with the framework discovery change.
| evaluator_cls: type, | ||
| compensator_cls: type, | ||
| ) -> None: | ||
| """Replace the runtime-governance names bound in the bootstrap module. |
There was a problem hiding this comment.
Please reduce this helper docstring. The important point is that the bootstrap module binds these names at import time; the rest can be removed.
| provider.get_policy_async = AsyncMock(side_effect=response_or_exc) | ||
| else: | ||
| provider.get_policy_async = AsyncMock(return_value=response_or_exc) | ||
| # Also mock ``track_event_async`` so the dispatcher's constructor |
There was a problem hiding this comment.
Please remove this comment. It says the same thing as assigning provider.track_event_async = AsyncMock() and adds unnecessary narration.
| cwd: Path, | ||
| uipath_config_path: Path, | ||
| ) -> None: | ||
| """Happy path — the returned tuple has the expected shape and the |
There was a problem hiding this comment.
This test should not document or assert a four-tuple shape once the bootstrap returns a named object. Please update the test to assert named fields or wrapper behavior, and remove the tuple-shape narration.
| assert result is not None | ||
| evaluator, policy_index, mode, dispose = result | ||
|
|
||
| # Shape checks — evaluator wired with the fake compensator + |
There was a problem hiding this comment.
Please remove these assertion comments. The assertions below are already readable, and this will get simpler after the bootstrap object replaces the tuple.
|
|
||
|
|
||
| def build_policy_index_from_yaml(yaml_text: str) -> PolicyIndex: | ||
| """Parse YAML policy packs into a PolicyIndex. |
There was a problem hiding this comment.
Please reduce this docstring. In this private CLI helper, the signature and body already show most of the args and return behavior; keep only the malformed-YAML contract if callers rely on it.
| @@ -0,0 +1,795 @@ | |||
| """Tests for ``build_policy_index_from_yaml``. | |||
There was a problem hiding this comment.
Please reduce this test module docstring. The test names already describe the covered cases.
| @@ -0,0 +1,552 @@ | |||
| """YAML → :class:`PolicyIndex` compiler (CLI-side). | |||
There was a problem hiding this comment.
Please reduce this module docstring. The useful part is the boundary: YAML is compiled in the CLI so runtime does not depend on PyYAML. The rest repeats callers and supported input shapes.
| return rules[0] | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- |
There was a problem hiding this comment.
Please remove these section banners here as well. The test function names provide the structure without adding separator noise.
| return rules[0] | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- |
There was a problem hiding this comment.
Please remove these section banners here as well. The test function names provide the structure without adding separator noise.
Extracts the governance-bootstrap helpers used by ``uipath run`` into a shared ``_cli/_governance_bootstrap.py`` module and wires the same path into ``uipath debug`` so both commands stamp identical audit + telemetry events. GovernanceBootstrap dataclass ----------------------------- ``resolve_governance()`` returns a ``GovernanceBootstrap`` (frozen dataclass with ``evaluator`` / ``policy_index`` / ``enforcement_mode`` / ``dispose`` fields plus a ``wrap_runtime()`` method) or ``None`` when governance should not fire. Callers hand the base runtime to ``bootstrap.wrap_runtime(delegate, agent_name=..., runtime_id=...)`` and call ``bootstrap.dispose()`` in ``finally`` -- no manual ``UiPathGovernedRuntime`` construction and no tuple unpacking. Framework + agent-type identity ------------------------------- Framework identity is taken from ``UiPathRuntimeFactoryRegistry.get_selection(context=ctx)``, which returns a ``(factory, framework)`` pair based on which runtime adapter handled the entrypoint (e.g. ``langgraph`` from ``uipath-langchain-python``'s ``register(name="langgraph", ...)``). A ``hasattr`` fallback keeps the CLI runnable against runtimes older than 0.11.7; it degrades to ``framework="unknown"``. Agent type is derived from the entrypoint filename via ``is_coded_agent()``: ``entrypoint == "agent.json"`` -> ``uipath_lowcode``, otherwise ``uipath_coded``. Both strings are backend wire values -- ``AGENT_TYPE_LOWCODE`` / ``AGENT_TYPE_CODED`` guard them. Error handling -------------- Governance is optional -- a failing bootstrap must not crash the run. Every step in ``resolve_governance()`` that can raise (policy fetch, policy compile, dispatcher init, compensator / audit-manager / evaluator construction) is caught; the CLI logs a warning and runs un-governed. If a failure lands after the dispatcher spawns its background thread and registers its ``atexit`` hook, the recovery path calls ``dispose()`` to unregister and shut down so no thread leaks. ``dispose()`` runs from CLI ``finally`` blocks, so it swallows and debug-logs both ``atexit.unregister`` and dispatcher-shutdown errors -- it must never mask the primary exception. It is idempotent-safe: the dispatcher's ``shutdown`` early-returns on repeat calls and ``atexit.unregister`` is a no-op for missing handlers. cli_debug parity ---------------- ``cli_debug`` calls ``resolve_governance()`` after ``factory.get_settings()``, computes ``governance_runtime_id`` once (``ctx.conversation_id or ctx.job_id or "default"``), passes the evaluator into ``factory.new_runtime`` and wraps via ``bootstrap.wrap_runtime`` -- matching ``cli_run``'s shape exactly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
6d482ee to
5c8fb05
Compare
There was a problem hiding this comment.
Pull request overview
This PR extracts host-side governance bootstrapping into a shared CLI module and reuses it from both uipath run and uipath debug, so they fetch/compile policy and stamp identical audit + telemetry events. It also adds a CLI-side YAML → PolicyIndex compiler and updates dependencies/tests accordingly.
Changes:
- Added shared governance bootstrap module (
_cli/_governance_bootstrap.py) and wired it into bothcli_runandcli_debug. - Added CLI-side governance helpers (
_cli/_governance/yaml_index.py) to compile YAML policies into a runtimePolicyIndex. - Added comprehensive tests for governance bootstrap and YAML compilation, and bumped package/dependency versions (including
pyyaml).
Reviewed changes
Copilot reviewed 10 out of 12 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/uipath/uv.lock | Updates lockfile for new/updated dependencies (runtime bump, PyYAML/types, etc.). |
| packages/uipath/pyproject.toml | Bumps uipath version and updates dependencies (incl. uipath-runtime and pyyaml). |
| packages/uipath/src/uipath/_cli/_governance_bootstrap.py | New shared governance bootstrap used by both run/debug paths. |
| packages/uipath/src/uipath/_cli/_governance/init.py | New CLI governance helper package export. |
| packages/uipath/src/uipath/_cli/_governance/yaml_index.py | New YAML policy compiler producing PolicyIndex for the runtime. |
| packages/uipath/src/uipath/_cli/cli_run.py | Wires shared governance bootstrap into uipath run and manages teardown. |
| packages/uipath/src/uipath/_cli/cli_debug.py | Wires shared governance bootstrap into uipath debug and manages teardown. |
| packages/uipath/src/uipath/_cli/_utils/_common.py | Adds shared is_coded_agent() helper used for consistent agent-type labeling. |
| packages/uipath/src/uipath/_cli/_evals/_telemetry.py | Reuses is_coded_agent() for consistent telemetry labeling. |
| packages/uipath/tests/cli/test_governance_bootstrap.py | New tests covering feature-flag gating, policy fetch/compile branches, cleanup contract, and conversational flag read. |
| packages/uipath/tests/cli/_governance/test_yaml_index.py | New tests covering YAML policy parsing and check-type compilation. |
| packages/uipath/tests/cli/_governance/init.py | Initializes governance test package. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| import asyncio | ||
| import logging | ||
| from typing import Any |
| console = ConsoleLogger() | ||
| logger = logging.getLogger(__name__) |
| from uipath.runtime import UiPathRuntimeProtocol | ||
| from uipath.runtime.governance._audit.base import AuditManager | ||
| from uipath.runtime.governance._audit.metadata import GovernanceRuntimeMetadata | ||
| from uipath.runtime.governance.native import GovernanceEvaluator | ||
| from uipath.runtime.governance.native.guardrail_compensation import ( |
| def dispose() -> None: | ||
| # Called from CLI ``finally`` blocks — must never raise, or it | ||
| # will mask the primary exception. | ||
| dispatcher = track_event_dispatcher | ||
| if dispatcher is None: | ||
| return | ||
| try: |
|



Extracts the governance-bootstrap helpers used by
uipath runinto a shared_cli/_governance_bootstrap.pymodule and wires the same path intouipath debugso both commands stamp identical audit + telemetry events.What the bootstrap owns
detect_agent_type()— classify the project asuipath_lowcode(Agent Builderagent.jsonat project root),uipath_coded(langgraph.json / llama_index.json / openai_agents.json marker, or afunctionsmap in uipath.json), orunknown. Stamped onto every audit event viaGovernanceRuntimeMetadata.agent_type.detect_agent_framework()— finer-grained framework classifier; resolves tounknownfor low-code agents (no Python framework drives the loop in that case).read_is_conversational()— readruntimeOptions.isConversationalfrom uipath.json so the policy fetch can request the right view.resolve_governance()— gate on the FF, fetch the policy, compile the index, and build the evaluator + audit manager + compensator. Returns the tuple a CLI caller hands toUiPathGovernedRuntimeorNonewhen governance should not fire on this run.Import discipline
All
uipath.runtime.governance.*imports happen insideresolve_governance()and at the inlineUiPathGovernedRuntimecall sites incli_run/cli_debug. The runtime governance modules ship from a separate package whose version isn't guaranteed to have them — deferring keeps the CLI modules import-safe across runtime versions. When the modules are missing,resolve_governance()logs at DEBUG and returnsNone; governance silently skips.Default sinks
AuditManager(track_event=provider.track_event, runtime_metadata=…)withregister_default_sinks=True(the default) registers both the always-ontracessink and the platform-mandatedtrack_eventssink. The CLI stacks a_ConsoleAuditSinkon top for local visibility.Trace correlation
GuardrailCompensator(provider)is constructed without atrace_id. The runtime preserves OTel context across its background-pool hop viacontextvars.copy_context, and the concrete provider self-resolves the canonical trace id at request time whenGovernRequest.trace_idisNone.cli_debug parity
cli_debugnow callsresolve_governance()afterfactory.get_settings(), computesgovernance_runtime_idonce (ctx.conversation_id or ctx.job_id or "default"), passes the evaluator intofactory.new_runtimeand wraps the runtime withUiPathGovernedRuntimeusing the same id — matchingcli_run's shape exactly.Development Packages
uipath