Skip to content

feat(cli): shared host-side governance bootstrap for run and debug#1778

Open
viswa-uipath wants to merge 1 commit into
mainfrom
feat/cli-changes
Open

feat(cli): shared host-side governance bootstrap for run and debug#1778
viswa-uipath wants to merge 1 commit into
mainfrom
feat/cli-changes

Conversation

@viswa-uipath

@viswa-uipath viswa-uipath commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

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.

What the bootstrap owns

  • detect_agent_type() — classify the project as uipath_lowcode (Agent Builder agent.json at project root), uipath_coded (langgraph.json / llama_index.json / openai_agents.json marker, or a functions map in uipath.json), or unknown. Stamped onto every audit event via GovernanceRuntimeMetadata.agent_type.
  • detect_agent_framework() — finer-grained framework classifier; resolves to unknown for low-code agents (no Python framework drives the loop in that case).
  • read_is_conversational() — read runtimeOptions.isConversational from 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 to UiPathGovernedRuntime or None when governance should not fire on this run.

Import discipline

All uipath.runtime.governance.* imports happen inside resolve_governance() and at the inline UiPathGovernedRuntime call sites in cli_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 returns None; governance silently skips.

Default sinks

AuditManager(track_event=provider.track_event, runtime_metadata=…) with register_default_sinks=True (the default) registers both the always-on traces sink and the platform-mandated track_events sink. The CLI stacks a _ConsoleAuditSink on top for local visibility.

Trace correlation

GuardrailCompensator(provider) is constructed without a trace_id. The runtime preserves OTel context across its background-pool hop via contextvars.copy_context, and the concrete provider self-resolves the canonical trace id at request time when GovernRequest.trace_id is None.

cli_debug parity

cli_debug now 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 the runtime with UiPathGovernedRuntime using the same id — matching cli_run's shape exactly.

Development Packages

uipath

[project]
dependencies = [
  # Exact version (copy-paste ready):
  "uipath==2.12.5.dev1017787094",

  # Any version from this PR (uncomment to use a range instead):
  # "uipath>=2.12.5.dev1017780000,<2.12.5.dev1017790000",
]

[[tool.uv.index]]
name = "testpypi"
url = "https://test.pypi.org/simple/"
publish-url = "https://test.pypi.org/legacy/"
explicit = true

[tool.uv.sources]
uipath = { index = "testpypi" }

@viswa-uipath viswa-uipath added the build:dev Create a dev build from the pr label Jul 1, 2026
@github-actions github-actions Bot added test:uipath-langchain Triggers tests in the uipath-langchain-python repository test:uipath-integrations labels Jul 1, 2026
@viswa-uipath viswa-uipath force-pushed the feat/cli-changes branch 2 times, most recently from 1a11593 to 04ad14f Compare July 1, 2026 15:07
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown

🚨 Heads up: uipath-integrations cross-tests are FAILING 🚨

Your changes may break one or more integrations in uipath-integrations-python:

  • uipath-openai-agents
  • uipath-google-adk
  • uipath-agent-framework
  • uipath-llamaindex
  • uipath-pydantic-ai

⚠️ These checks are NOT enforced by branch protection rules. Please review the failures before merging.

🔍 Inspect the failed run →

@viswa-uipath viswa-uipath force-pushed the feat/cli-changes branch 3 times, most recently from 38435f6 to 9d066a2 Compare July 1, 2026 17:59
@viswa-uipath viswa-uipath added build:dev Create a dev build from the pr and removed build:dev Create a dev build from the pr labels Jul 1, 2026
@viswa-uipath viswa-uipath added build:dev Create a dev build from the pr and removed build:dev Create a dev build from the pr labels Jul 1, 2026
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown

🚨 Heads up: uipath-langchain cross-tests are FAILING 🚨

Your changes may break the uipath-langchain-python integration.

⚠️ These checks are NOT enforced by branch protection rules. Please review the failures before merging.

🔍 Inspect the failed run →

@viswa-uipath viswa-uipath marked this pull request as ready for review July 1, 2026 18:48
@radu-mocanu

Copy link
Copy Markdown
Collaborator

shouldn t we apply the governance to evals as well?

# rename without coordinating with the backend team.
# ---------------------------------------------------------------------------

AGENT_TYPE_LOWCODE: Final[str] = "uipath_lowcode"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

Comment thread packages/uipath/src/uipath/_cli/_governance_bootstrap.py Outdated
Comment thread packages/uipath/src/uipath/_cli/_governance_bootstrap.py Outdated
@radu-mocanu

Copy link
Copy Markdown
Collaborator

Overall blocking concern: this PR crosses package boundaries by making the public uipath CLI know about product/runtime identities and framework adapter discovery. uipath-python should not reference or special-case the low-code runtime, and it should not infer framework identity from framework-specific files or constants.

The clean contract is to add framework: str = "unknown" to UiPathRuntimeFactorySettings in uipath-runtime. Each registered factory should provide its own value at the existing entry-point factory seam: framework adapters set their framework (langchain, llamaindex, etc.), the functions runtime sets functions, and the low-code runtime can set low_code from its own package. The CLI should only read factory_settings.framework and forward it into GovernanceRuntimeMetadata, defaulting to unknown when settings are absent.

return None
if not response.policies:
return None

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: combine under the same condition

)
return None

# The compensator no longer carries a trace id — the worker

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Comment on lines +325 to +330
# 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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

same here

)
new_runtime_kwargs: dict[str, Any] = {}
if governance_bootstrap is not None:
new_runtime_kwargs["evaluator"] = governance_bootstrap[

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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[

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

remove comment

Comment on lines +278 to +280
# Drain the live-tracking span exporter pool so
# in-flight spans complete before the process
# exits. Idempotent.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

remove comment

f"Governance enabled (mode={response.mode.value}, "
f"packs={list(policy_index.pack_names)})"
)
return evaluator, policy_index, response.mode, dispose

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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",
]

# ---------------------------------------------------------------------------

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

# ---------------------------------------------------------------------------

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please remove these section banners. The test class and function names already provide the structure, so these separators just add noise.

# ---------------------------------------------------------------------------


class TestConstants:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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 +

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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``.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please reduce this test module docstring. The test names already describe the covered cases.

@@ -0,0 +1,552 @@
"""YAML → :class:`PolicyIndex` compiler (CLI-side).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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]


# ---------------------------------------------------------------------------

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please remove these section banners here as well. The test function names provide the structure without adding separator noise.

return rules[0]


# ---------------------------------------------------------------------------

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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>
Copilot AI review requested due to automatic review settings July 2, 2026 05:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 both cli_run and cli_debug.
  • Added CLI-side governance helpers (_cli/_governance/yaml_index.py) to compile YAML policies into a runtime PolicyIndex.
  • 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.

Comment on lines 1 to +3
import asyncio
import logging
from typing import Any
Comment on lines 44 to +45
console = ConsoleLogger()
logger = logging.getLogger(__name__)
Comment on lines +33 to +37
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 (
Comment on lines +164 to +170
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:
@sonarqubecloud

sonarqubecloud Bot commented Jul 2, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

build:dev Create a dev build from the pr test:uipath-integrations test:uipath-langchain Triggers tests in the uipath-langchain-python repository

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants