Skip to content

feat(governance): GovernanceRuntime wrapper + runtime registry wiring#126

Closed
aditik0303 wants to merge 5 commits into
feat/governance-delegation-guardfrom
feat/governance-wrapper-wiring
Closed

feat(governance): GovernanceRuntime wrapper + runtime registry wiring#126
aditik0303 wants to merge 5 commits into
feat/governance-delegation-guardfrom
feat/governance-wrapper-wiring

Conversation

@aditik0303

Copy link
Copy Markdown

Stacked PR 7/7 — part of splitting feat/governance-core into reviewable slices. Base: feat/governance-delegation-guard. One logical slice (branch is cumulative so CI is green). Merge in order #1#7 and delete each branch on merge so the next PR auto-retargets onto feat/agentic-governance. feat/governance-core kept untouched as backup.

Copilot AI left a comment

Copy link
Copy Markdown

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 introduces a governance integration layer at the runtime boundary by wiring a feature-flag–gated wrapper into the runtime factory registry, so newly created runtimes can be transparently wrapped with GovernanceRuntime when governance is enabled.

Changes:

  • Add apply_governance_wrapper (lazy-import, FF-gated) and re-export it from uipath.runtime.
  • Wrap UiPathRuntimeFactoryRegistry.get() results by default via UiPathWrappedRuntimeFactory, with an apply_wrappers=False escape hatch.
  • Add a new governance runtime wrapper implementation (uipath.runtime.governance.wrapper) plus extensive tests and docs for wrapper behavior.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
tests/test_wrapper.py Adds boundary tests for apply_governance_wrapper (FF gate, lazy import, fail-open behavior).
tests/test_wrapper_internals.py Adds unit tests for GovernanceRuntime helper internals and module entry points (governance_wrapper, wrap_agent).
tests/test_registry.py Updates registry tests for new apply_wrappers behavior and adds wrapper-specific coverage.
tests/test_dispose_isolation.py Adds isolation tests ensuring GovernanceRuntime.dispose() runs all cleanup steps and only propagates delegate disposal errors.
src/uipath/runtime/wrapper.py Introduces the FF-gated, lazy-import runtime wrapper entry point (apply_governance_wrapper).
src/uipath/runtime/registry.py Adds UiPathWrappedRuntimeFactory and apply_wrappers plumbing to auto-apply governance on runtime creation.
src/uipath/runtime/governance/wrapper.py Adds the GovernanceRuntime implementation (adapter wrapping, evaluator lifecycle, input/output extraction, dispose behavior).
src/uipath/runtime/init.py Re-exports governance wrapper API (GOVERNANCE_FEATURE_FLAG, apply_governance_wrapper).
docs/runtime-wrapper-extension.md Documents the governance integration point, flag semantics, and recommended testing approach.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +339 to +353
# Bind the model-name ContextVar so adapters running in this
# runtime's context see the right value under concurrent
# agents. The token is stashed so dispose() can reset the
# var — without that, the value leaks into sibling tasks
# that inherit this context and outlive the runtime.
model_name = self._extract_model_name(delegate, context)
self._model_name = model_name
self._model_name_token = _current_model_name.set(model_name)

# Record agent-type before the policy prefetch fires so the
# fetch can ask the server for the matching container key
# (conversational vs autonomous).
set_agent_conversational(
self._extract_is_conversational(delegate, context)
)
# full chat history (e.g. ``{"messages": [...]}``). Pass
# ``latest_only=True`` so governance evaluates the most
# recent user message and not the entire transcript.
agent_input = _extract_governable_text(input, latest_only=True)
Comment on lines +405 to +420
# Try nested delegate chain (unwrap wrappers)
if not model_name:
inner = getattr(delegate, "_delegate", None) or getattr(
delegate, "delegate", None
)
while inner and not model_name:
agent_def = getattr(inner, "_agent_definition", None)
if agent_def:
settings = getattr(agent_def, "settings", None)
if settings:
model_name = getattr(settings, "model", None) or ""
break
inner = getattr(inner, "_delegate", None) or getattr(
inner, "delegate", None
)

Comment on lines +216 to +222
# Last-resort: walk public attributes on opaque objects (e.g. a
# framework-specific result class without model_dump/dict).
public = {
name: getattr(value, name)
for name in dir(value)
if not name.startswith("_") and not callable(getattr(value, name, None))
}
Comment thread docs/runtime-wrapper-extension.md Outdated
Comment on lines +327 to +336
if not is_governance_enabled():
self._init_failed = True
self._evaluator_ready = True # don't try to materialise later
logger.info(
"GovernanceRuntime initialized as no-op: governance feature "
"flag is OFF (agent='%s', runtime_id='%s')",
self._agent_name,
runtime_id,
)
return
@aditik0303 aditik0303 force-pushed the feat/governance-delegation-guard branch from aeb0d94 to 0664ff6 Compare June 16, 2026 11:03
@aditik0303 aditik0303 force-pushed the feat/governance-wrapper-wiring branch from f1a607b to ff14f22 Compare June 16, 2026 11:03
@aditik0303 aditik0303 force-pushed the feat/governance-delegation-guard branch from 0664ff6 to 58c7baf Compare June 17, 2026 06:54
@aditik0303 aditik0303 force-pushed the feat/governance-wrapper-wiring branch from ff14f22 to 2fb2248 Compare June 17, 2026 06:57
@aditik0303 aditik0303 force-pushed the feat/governance-delegation-guard branch from 58c7baf to 20fe69c Compare June 17, 2026 08:35
@aditik0303 aditik0303 force-pushed the feat/governance-wrapper-wiring branch from 2fb2248 to 1a9ce83 Compare June 17, 2026 08:36
@aditik0303 aditik0303 force-pushed the feat/governance-delegation-guard branch from 20fe69c to d1d42d6 Compare June 19, 2026 08:08
@aditik0303 aditik0303 force-pushed the feat/governance-wrapper-wiring branch from 1a9ce83 to da816b2 Compare June 19, 2026 08:08
@aditik0303 aditik0303 force-pushed the feat/governance-delegation-guard branch from d1d42d6 to 61e9ff7 Compare June 19, 2026 11:25
@aditik0303 aditik0303 force-pushed the feat/governance-wrapper-wiring branch from da816b2 to 10b949f Compare June 19, 2026 11:25
@viswa-uipath viswa-uipath force-pushed the feat/governance-delegation-guard branch from 61e9ff7 to 95cbcb2 Compare June 25, 2026 07:01
aditik0303 and others added 5 commits June 25, 2026 12:33
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… FF gate (no-op when OFF, single dispose token); latest_only only for conversational agents; depth-cap model-name delegate walk; guard getattr in text extractor; doc is_governance_enabled name

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ts._helpers

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The governance wrapper extracts the framework agent from the delegate runtime by probing a list of known attribute names (_agent, agent, _graph, ...). LlamaIndex's runtime exposes its agent as 'self.workflow', which wasn't in the list — so the wrapper couldn't find it and skipped attaching governance (wrapped=False). Add '_workflow'/'workflow' to _AGENT_ATTRS so LlamaIndex workflows are governed. Verified end-to-end on a cloud LlamaIndex coded agent (adapter=LlamaIndex, wrapped=True).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…Runtime

Closes architecture-review §2.1 + §2.2 — the
UiPathWrappedRuntimeFactory bolted governance onto the generic
runtime-factory registry (apply_wrappers=True turned every registered
factory into a different type, breaking isinstance checks), and the
second GovernanceRuntime in governance/wrapper.py reached into
delegate._agent_definition / framework-specific private attrs through
a 10-level walk to install framework-blind callbacks. Both patterns
the doc unambiguously says to delete.

Composition belongs in the host's decorator chain, FF-gated, where
UiPathResumableRuntime already wraps the framework runtime; this
PR's wrapper machinery was an end-run around that.

Deletions
- src/uipath/runtime/governance/wrapper.py (1002 LOC) — the second
  GovernanceRuntime with _AGENT_ATTRS / _replace_agent_in_delegate /
  model-context-var introspection.
- src/uipath/runtime/wrapper.py (55 LOC) — the lazy-import dispatch
  shim that called the deleted governance_wrapper.
- tests/test_dispose_isolation.py, tests/test_wrapper.py,
  tests/test_wrapper_internals.py (~650 LOC combined) — entire test
  suites for the deleted modules.

Updates
- src/uipath/runtime/registry.py — UiPathWrappedRuntimeFactory class
  and the apply_wrappers kwarg removed from get(). The registry
  returns the registered factory unchanged; cross-cutting concerns
  (governance, audit, …) are composed by the host into the decorator
  chain, not auto-applied here.
- src/uipath/runtime/__init__.py — drop GOVERNANCE_FEATURE_FLAG /
  apply_governance_wrapper exports.
- tests/test_registry.py — strip every apply_wrappers=False kwarg
  (the kwarg is gone) and drop the wrapping-behaviour section + its
  fixtures.

Conflict resolution
The rebase onto #125's tip replayed the upstream e186f5f commit (a
cosmetic helper-import touch) into three test files that my PR
#122/#123/#124 refactors had already rewritten end-to-end. HEAD-side
resolution kept the refactored form in test_evaluator.py,
test_evaluator_operators.py, test_guardrail_compensation.py — the
incoming side referenced symbols (governance.audit,
governance.config, tests._helpers.reset_enforcement_mode) that the
post-rebase stack no longer ships.

Verification
- Monorepo grep for UiPathWrappedRuntimeFactory, apply_wrappers,
  apply_governance_wrapper, governance_wrapper, and the deleted
  module import paths: zero hits.
- ruff clean, mypy clean (45 source files), 357 passed + 1 skipped.

Net diff on top of #125's tip: −2005 / +38 LOC = −1967 net.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@viswa-uipath viswa-uipath force-pushed the feat/governance-wrapper-wiring branch from 7de6934 to 65c143f Compare June 25, 2026 08:07
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.

3 participants