feat(governance): GovernanceRuntime wrapper + runtime registry wiring#126
Closed
aditik0303 wants to merge 5 commits into
Closed
feat(governance): GovernanceRuntime wrapper + runtime registry wiring#126aditik0303 wants to merge 5 commits into
aditik0303 wants to merge 5 commits into
Conversation
There was a problem hiding this comment.
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 fromuipath.runtime. - Wrap
UiPathRuntimeFactoryRegistry.get()results by default viaUiPathWrappedRuntimeFactory, with anapply_wrappers=Falseescape 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 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 |
aeb0d94 to
0664ff6
Compare
f1a607b to
ff14f22
Compare
0664ff6 to
58c7baf
Compare
ff14f22 to
2fb2248
Compare
58c7baf to
20fe69c
Compare
2fb2248 to
1a9ce83
Compare
20fe69c to
d1d42d6
Compare
1a9ce83 to
da816b2
Compare
d1d42d6 to
61e9ff7
Compare
da816b2 to
10b949f
Compare
61e9ff7 to
95cbcb2
Compare
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>
7de6934 to
65c143f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked PR 7/7 — part of splitting
feat/governance-coreinto 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 ontofeat/agentic-governance.feat/governance-corekept untouched as backup.