Skip to content

Commit 6b0d951

Browse files
viswa-uipathclaude
andcommitted
refactor(governance): instance-scope enforcement mode on PolicyLoader
Addresses radu's follow-up on PR #121 (discussion r3465934815): the enforcement mode was still process-level scoped via config._state, defeating the point of the loader instance-scoping when uipath eval runs parallel runtimes with mixed-mode policies. - PolicyLoader now owns _enforcement_mode and exposes it via the enforcement_mode property (defaults to AUDIT when no provider response has supplied a mode) - _load_from_provider writes the instance field instead of calling the global set_enforcement_mode - config.py deleted entirely: _state / _EnforcementModeState / get_enforcement_mode / set_enforcement_mode are gone. No production consumers outside the loader; canonical EnforcementMode lives in uipath.core.governance Tests: - _helpers.reset_enforcement_mode dropped (no global to reset) - test_enforcement_mode_default rewritten around PolicyLoader.enforcement_mode; new test_two_loaders_carry_independent_enforcement_modes pins the cross-instance isolation invariant - test_governance_runtime / test_loader drop the reset fixture and the get/set imports; mode-persistence test exercises two consecutive loads on a single loader 188 passed, ruff/mypy/bandit clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4e6627d commit 6b0d951

7 files changed

Lines changed: 158 additions & 150 deletions

File tree

src/uipath/runtime/governance/config.py

Lines changed: 0 additions & 60 deletions
This file was deleted.

src/uipath/runtime/governance/native/loader.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,12 @@
2222
from collections import Counter
2323

2424
import yaml
25-
from uipath.core.governance import GovernancePolicyProvider, PolicyContext
25+
from uipath.core.governance import (
26+
EnforcementMode,
27+
GovernancePolicyProvider,
28+
PolicyContext,
29+
)
2630

27-
from uipath.runtime.governance.config import set_enforcement_mode
2831
from uipath.runtime.governance.native._yaml_to_index import build_policy_index_from_yaml
2932
from uipath.runtime.governance.native.models import PolicyIndex
3033

@@ -75,6 +78,12 @@ def __init__(
7578
self._provider = provider
7679
self._is_conversational = is_conversational
7780
self._policy_index: PolicyIndex | None = None
81+
# Enforcement mode supplied by the provider on the most recent
82+
# load. ``None`` until the first load lands (or whenever the
83+
# provider omits a mode); :attr:`enforcement_mode` returns
84+
# ``AUDIT`` in that case. Instance-scoped so parallel runtimes
85+
# (e.g. ``uipath eval``) don't clobber each other.
86+
self._enforcement_mode: EnforcementMode | None = None
7887
# ``_prefetch_event`` is set once the background load finishes
7988
# (success OR failure); callers of ``get_policy_index`` wait on
8089
# it. ``_prefetch_lock`` guards the start-once semantics so
@@ -244,7 +253,7 @@ def _load_from_provider(
244253
return None
245254

246255
if response.mode is not None:
247-
set_enforcement_mode(response.mode)
256+
self._enforcement_mode = response.mode
248257
logger.info("Enforcement mode set from provider: %s", response.mode.value)
249258

250259
if not response.policies:
@@ -292,6 +301,24 @@ def _log_index_summary(self, index: PolicyIndex) -> None:
292301
dict(hook_counts),
293302
)
294303

304+
@property
305+
def enforcement_mode(self) -> EnforcementMode:
306+
"""Active enforcement mode for this loader.
307+
308+
The canonical source is whatever the policy provider supplied on
309+
the most recent load. Until that load lands (or if the provider
310+
omits a mode), the default is :attr:`EnforcementMode.AUDIT` —
311+
evaluate and log without blocking. Defaulting to AUDIT avoids
312+
the chicken-and-egg where a DISABLED default would short-circuit
313+
evaluation before the background load could ever opt the tenant
314+
in.
315+
"""
316+
return (
317+
self._enforcement_mode
318+
if self._enforcement_mode is not None
319+
else EnforcementMode.AUDIT
320+
)
321+
295322
@property
296323
def available_packs(self) -> list[str]:
297324
"""Pack names from the currently loaded policy index.

tests/_helpers.py

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
"""Shared test-only helpers.
22
3-
Keeps test concerns out of the production governance package: per-test
4-
isolation utilities and shared stubs live here rather than inside the
5-
production modules.
3+
Keeps test concerns out of the production governance package: shared
4+
stubs live here rather than inside the production modules.
5+
6+
The enforcement-mode reset helper is gone because the mode is now
7+
instance-scoped on :class:`PolicyLoader` — tests that want a clean
8+
slate just construct a fresh loader instead of touching a global.
69
"""
710

811
from __future__ import annotations
@@ -11,17 +14,6 @@
1114

1215
from uipath.core.governance import PolicyContext, PolicyResponse
1316

14-
from uipath.runtime.governance import config
15-
16-
17-
def reset_enforcement_mode() -> None:
18-
"""Clear the process-wide enforcement mode so the AUDIT default re-applies.
19-
20-
Test isolation only — production code never resets the mode; the policy
21-
loader sets it from the provider-supplied :class:`PolicyResponse`.
22-
"""
23-
config._state.mode = None
24-
2517

2618
class StubPolicyProvider:
2719
"""Minimal in-memory :class:`GovernancePolicyProvider` for tests.

tests/conftest.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,7 @@ def temp_dir() -> Generator[str, None, None]:
1919
yield tmp_dir
2020

2121

22-
# The loader no longer keeps provider / selector at module scope —
23-
# state is owned by each :class:`PolicyLoader` instance — so no
24-
# autouse cross-test reset is needed. Tests that share enforcement
25-
# mode call :func:`reset_enforcement_mode` from ``tests._helpers``
26-
# directly.
22+
# Governance state — provider, conversational selector, policy cache,
23+
# enforcement mode — is owned by each :class:`PolicyLoader` instance,
24+
# so no autouse cross-test reset is needed. Tests that want a clean
25+
# slate just construct a fresh loader.
Lines changed: 97 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,114 @@
1-
"""Tests for the default enforcement-mode resolution.
1+
"""Tests for the default enforcement-mode resolution on :class:`PolicyLoader`.
22
33
The default is :attr:`EnforcementMode.AUDIT` so the wrapper attaches at
44
runtime construction and the background policy load can run. If the
5-
provider later returns ``disabled``, ``set_enforcement_mode`` flips
6-
the mode and ``evaluate()`` short-circuits per-call.
5+
provider later returns ``disabled``, the loader records it and
6+
:attr:`enforcement_mode` flips.
77
8-
Resolution (per :func:`get_enforcement_mode`):
9-
1. The provider-supplied value applied via ``set_enforcement_mode`` by
10-
the policy loader.
11-
2. Default ``AUDIT``.
8+
Resolution (per :attr:`PolicyLoader.enforcement_mode`):
9+
1. The provider-supplied value on the most recent load.
10+
2. Default :attr:`EnforcementMode.AUDIT`.
1211
"""
1312

1413
from __future__ import annotations
1514

16-
import pytest
15+
from uipath.core.governance import EnforcementMode, PolicyResponse
1716

18-
from tests._helpers import reset_enforcement_mode
19-
from uipath.runtime.governance.config import (
20-
EnforcementMode,
21-
get_enforcement_mode,
22-
set_enforcement_mode,
23-
)
24-
25-
26-
@pytest.fixture(autouse=True)
27-
def _isolate_mode():
28-
"""Each test starts from a clean module-state slate."""
29-
reset_enforcement_mode()
30-
yield
31-
reset_enforcement_mode()
17+
from tests._helpers import StubPolicyProvider
18+
from uipath.runtime.governance.native.loader import PolicyLoader
3219

3320

3421
def test_default_mode_is_audit() -> None:
35-
"""No backend-supplied mode → AUDIT.
22+
"""No provider-supplied mode yet → AUDIT.
3623
3724
AUDIT is the default so the wrapper attaches and the background
3825
policy fetch can run. The backend can flip the mode to DISABLED
3926
on fetch when the tenant has no policies.
4027
"""
41-
assert get_enforcement_mode() is EnforcementMode.AUDIT
42-
43-
44-
def test_backend_disabled_wins_over_default() -> None:
45-
"""The backend mode (via ``set_enforcement_mode``) overrides the default."""
46-
set_enforcement_mode(EnforcementMode.DISABLED)
47-
assert get_enforcement_mode() is EnforcementMode.DISABLED
48-
49-
50-
def test_backend_enforce_wins_over_default() -> None:
51-
set_enforcement_mode(EnforcementMode.ENFORCE)
52-
assert get_enforcement_mode() is EnforcementMode.ENFORCE
53-
54-
55-
def test_reset_returns_to_default() -> None:
56-
"""``reset_enforcement_mode`` clears the mode so the default re-applies."""
57-
set_enforcement_mode(EnforcementMode.ENFORCE)
58-
assert get_enforcement_mode() is EnforcementMode.ENFORCE
59-
reset_enforcement_mode()
60-
assert get_enforcement_mode() is EnforcementMode.AUDIT
28+
loader = PolicyLoader(None)
29+
assert loader.enforcement_mode is EnforcementMode.AUDIT
30+
31+
32+
def test_provider_disabled_wins_over_default() -> None:
33+
"""A provider supplying DISABLED overrides the AUDIT default."""
34+
provider = StubPolicyProvider(
35+
response=PolicyResponse(mode=EnforcementMode.DISABLED, policies="")
36+
)
37+
loader = PolicyLoader(provider)
38+
loader.load_policy_index()
39+
assert loader.enforcement_mode is EnforcementMode.DISABLED
40+
41+
42+
def test_provider_enforce_wins_over_default() -> None:
43+
"""A provider supplying ENFORCE flips the loader to enforce."""
44+
provider = StubPolicyProvider(
45+
response=PolicyResponse(
46+
mode=EnforcementMode.ENFORCE,
47+
policies="standard: p\nrules: [{id: r1, hook: before_model, "
48+
"checks: [{type: regex, patterns: ['x']}]}]\n",
49+
)
50+
)
51+
loader = PolicyLoader(provider)
52+
loader.load_policy_index()
53+
assert loader.enforcement_mode is EnforcementMode.ENFORCE
54+
55+
56+
def test_loader_with_none_mode_response_keeps_previous_value() -> None:
57+
"""Provider returning ``mode=None`` doesn't clobber a previously-set mode.
58+
59+
The wire response model treats ``None`` as "no opinion" — the loader
60+
must not overwrite a real value with it. Otherwise a transient
61+
provider response could silently demote a tenant's enforcement
62+
posture.
63+
"""
64+
p1 = StubPolicyProvider(
65+
response=PolicyResponse(
66+
mode=EnforcementMode.ENFORCE,
67+
policies="standard: p\nrules: [{id: r1, hook: before_model, "
68+
"checks: [{type: regex, patterns: ['x']}]}]\n",
69+
)
70+
)
71+
loader = PolicyLoader(p1)
72+
loader.load_policy_index()
73+
assert loader.enforcement_mode is EnforcementMode.ENFORCE
74+
75+
# A second provider response that omits mode should not flip back to AUDIT.
76+
loader._provider = StubPolicyProvider(
77+
response=PolicyResponse(
78+
mode=None,
79+
policies="standard: p\nrules: [{id: r1, hook: before_model, "
80+
"checks: [{type: regex, patterns: ['x']}]}]\n",
81+
)
82+
)
83+
loader.clear_cache()
84+
loader.load_policy_index()
85+
assert loader.enforcement_mode is EnforcementMode.ENFORCE
86+
87+
88+
def test_two_loaders_carry_independent_enforcement_modes() -> None:
89+
"""The whole point of the refactor: parallel loaders don't share mode.
90+
91+
Previously :func:`set_enforcement_mode` wrote a module global, so an
92+
ENFORCE-mode loader and a DISABLED-mode loader running concurrently
93+
in the same process clobbered each other (last writer wins).
94+
Instance-scoped mode means each loader's mode is read-isolated.
95+
"""
96+
p_enforce = StubPolicyProvider(
97+
response=PolicyResponse(
98+
mode=EnforcementMode.ENFORCE,
99+
policies="standard: e\nrules: [{id: r1, hook: before_model, "
100+
"checks: [{type: regex, patterns: ['x']}]}]\n",
101+
)
102+
)
103+
p_disabled = StubPolicyProvider(
104+
response=PolicyResponse(mode=EnforcementMode.DISABLED, policies="")
105+
)
106+
107+
enforce_loader = PolicyLoader(p_enforce)
108+
disabled_loader = PolicyLoader(p_disabled)
109+
110+
enforce_loader.load_policy_index()
111+
disabled_loader.load_policy_index()
112+
113+
assert enforce_loader.enforcement_mode is EnforcementMode.ENFORCE
114+
assert disabled_loader.enforcement_mode is EnforcementMode.DISABLED

0 commit comments

Comments
 (0)