Skip to content

Commit 0df17c8

Browse files
ci(platform): enforce uipath.platform layering with import-linter
Add an import-linter layers contract over uipath.platform, enforced as a CI step in the lint-uipath-platform job. The contract statically forbids import cycles and upward imports between domain packages — the class of bug behind the 0.1.89 identity/common incident (#1787). Type-checking imports are excluded; the contract is exhaustive so new subpackages must be assigned a layer explicitly. The contract is intentionally committed without ignores: lint-imports fails at this commit on the pre-existing upward imports from common/interrupt_models.py, fixed in the next commit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent c5924ec commit 0df17c8

30 files changed

Lines changed: 1006 additions & 482 deletions

.github/workflows/lint-packages.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,11 @@ jobs:
139139
working-directory: packages/uipath-platform
140140
run: uv run ruff format --check .
141141

142+
- name: Check import contracts
143+
if: steps.check.outputs.skip != 'true'
144+
working-directory: packages/uipath-platform
145+
run: uv run lint-imports
146+
142147
lint-uipath:
143148
name: Lint uipath
144149
needs: detect-changed-packages

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ wheels/
2828

2929
.cache/*
3030
.cache
31+
.import_linter_cache/
3132
site
3233

3334

packages/uipath-platform/CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ pytest tests/services/test_assets_service.py # Single test file
1717
ruff check . # Lint
1818
ruff format --check . # Format check
1919
mypy src tests # Type check
20+
uv run lint-imports # Import-graph layering contract
2021
```
2122

2223
No justfile exists for this package — run commands directly.

packages/uipath-platform/pyproject.toml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ dev = [
4747
"pytest-cov>=4.1.0",
4848
"pytest-mock>=3.11.1",
4949
"pre-commit>=4.1.0",
50+
"import-linter>=2.0",
5051
]
5152

5253
[tool.hatch.build.targets.wheel]
@@ -126,6 +127,31 @@ exclude_lines = [
126127
"@(abc\\.)?abstractmethod",
127128
]
128129

130+
[tool.importlinter]
131+
root_package = "uipath.platform"
132+
exclude_type_checking_imports = true
133+
134+
[[tool.importlinter.contracts]]
135+
id = "platform-layers"
136+
name = "uipath-platform layered architecture"
137+
type = "layers"
138+
containers = ["uipath.platform"]
139+
exhaustive = true
140+
# TODO: Remove this exception in the next breaking-change release.
141+
ignore_imports = [
142+
"uipath.platform.common.interrupt_models -> uipath.platform.resume_triggers.interrupt_models",
143+
]
144+
unmatched_ignore_imports_alerting = "error"
145+
layers = [
146+
"guardrails",
147+
"_uipath : resume_triggers",
148+
"_guardrails_service | agenthub | automation_ops | automation_tracker | connections | context_grounding | entities | external_applications | governance | memory | pii_detection | portal | resource_catalog | semantic_proxy",
149+
"orchestrator",
150+
"action_center | attachments | chat | documents | identity",
151+
"common",
152+
"constants | errors",
153+
]
154+
129155
[tool.uv]
130156
exclude-newer = "2 days"
131157

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""Internal UiPath Guardrails service layer."""
2+
3+
from ._guardrails_service import GuardrailsService
4+
from .guardrails import (
5+
BYO_VALIDATOR_TYPE,
6+
BuiltInValidatorGuardrail,
7+
EnumListParameterValue,
8+
EnumParameterValue,
9+
GuardrailType,
10+
MapEnumParameterValue,
11+
NumberParameterValue,
12+
TextListParameterValue,
13+
TextParameterValue,
14+
ValidatorParameter,
15+
)
16+
17+
__all__ = [
18+
"BYO_VALIDATOR_TYPE",
19+
"BuiltInValidatorGuardrail",
20+
"EnumListParameterValue",
21+
"EnumParameterValue",
22+
"GuardrailType",
23+
"GuardrailsService",
24+
"MapEnumParameterValue",
25+
"NumberParameterValue",
26+
"TextListParameterValue",
27+
"TextParameterValue",
28+
"ValidatorParameter",
29+
]

packages/uipath-platform/src/uipath/platform/guardrails/_guardrails_service.py renamed to packages/uipath-platform/src/uipath/platform/_guardrails_service/_guardrails_service.py

File renamed without changes.
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
"""Guardrails models for UiPath Platform."""
2+
3+
from enum import Enum
4+
from typing import Annotated, Literal
5+
6+
from pydantic import BaseModel, ConfigDict, Field
7+
from uipath.core.guardrails import BaseGuardrail
8+
9+
10+
class EnumListParameterValue(BaseModel):
11+
"""Enum list parameter value."""
12+
13+
parameter_type: Literal["enum-list"] = Field(alias="$parameterType")
14+
id: str
15+
value: list[str]
16+
17+
model_config = ConfigDict(populate_by_name=True, extra="allow")
18+
19+
20+
class MapEnumParameterValue(BaseModel):
21+
"""Map enum parameter value."""
22+
23+
parameter_type: Literal["map-enum"] = Field(alias="$parameterType")
24+
id: str
25+
value: dict[str, float]
26+
27+
model_config = ConfigDict(populate_by_name=True, extra="allow")
28+
29+
30+
class NumberParameterValue(BaseModel):
31+
"""Number parameter value."""
32+
33+
parameter_type: Literal["number"] = Field(alias="$parameterType")
34+
id: str
35+
value: float
36+
37+
model_config = ConfigDict(populate_by_name=True, extra="allow")
38+
39+
40+
class EnumParameterValue(BaseModel):
41+
"""Single-select enum parameter value."""
42+
43+
parameter_type: Literal["enum"] = Field(alias="$parameterType")
44+
id: str
45+
value: str
46+
47+
model_config = ConfigDict(populate_by_name=True, extra="allow")
48+
49+
50+
class TextParameterValue(BaseModel):
51+
"""Free-text parameter value."""
52+
53+
parameter_type: Literal["text"] = Field(alias="$parameterType")
54+
id: str
55+
value: str
56+
57+
model_config = ConfigDict(populate_by_name=True, extra="allow")
58+
59+
60+
class TextListParameterValue(BaseModel):
61+
"""List-of-text parameter value."""
62+
63+
parameter_type: Literal["text-list"] = Field(alias="$parameterType")
64+
id: str
65+
value: list[str]
66+
67+
model_config = ConfigDict(populate_by_name=True, extra="allow")
68+
69+
70+
ValidatorParameter = Annotated[
71+
EnumListParameterValue
72+
| MapEnumParameterValue
73+
| NumberParameterValue
74+
| EnumParameterValue
75+
| TextParameterValue
76+
| TextListParameterValue,
77+
Field(discriminator="parameter_type"),
78+
]
79+
80+
81+
#: Sentinel ``validator_type`` for Bring Your Own Guardrail (BYOG) guardrails; the
82+
#: connector-backed configuration is referenced by ``byo_validator_name`` instead.
83+
BYO_VALIDATOR_TYPE = "byo"
84+
85+
86+
class BuiltInValidatorGuardrail(BaseGuardrail):
87+
"""Built-in validator guardrail model."""
88+
89+
guardrail_type: Literal["builtInValidator"] = Field(alias="$guardrailType")
90+
validator_type: str = Field(alias="validatorType")
91+
validator_parameters: list[ValidatorParameter] = Field(
92+
default_factory=list, alias="validatorParameters"
93+
)
94+
byo_validator_name: str | None = Field(default=None, alias="byoValidatorName")
95+
96+
model_config = ConfigDict(populate_by_name=True, extra="allow")
97+
98+
99+
class GuardrailType(str, Enum):
100+
"""Guardrail type enumeration."""
101+
102+
BUILT_IN_VALIDATOR = "builtInValidator"
103+
CUSTOM = "custom"

packages/uipath-platform/src/uipath/platform/_uipath.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
from uipath.platform.automation_tracker import AutomationTrackerService
77

8+
from ._guardrails_service import GuardrailsService
89
from .action_center import TasksService
910
from .agenthub._agenthub_service import AgentHubService
1011
from .agenthub._remote_a2a_service import RemoteA2aService
@@ -23,7 +24,6 @@
2324
from .errors import BaseUrlMissingError, SecretMissingError
2425
from .external_applications import ExternalApplicationService
2526
from .governance import GovernanceService
26-
from .guardrails import GuardrailsService
2727
from .memory import MemoryService
2828
from .orchestrator import (
2929
AssetsService,

packages/uipath-platform/src/uipath/platform/common/__init__.py

Lines changed: 79 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,41 @@
33
This module contains common models used across multiple services.
44
"""
55

6+
from typing import TYPE_CHECKING, Any
7+
68
from uipath.core.triggers import UiPathResumeMetadata
79

10+
# TODO: Remove the interrupt-model compatibility exports in the next breaking-change release.
11+
if TYPE_CHECKING:
12+
from .interrupt_models import (
13+
CreateBatchTransform,
14+
CreateDeepRag,
15+
CreateDeepRagRaw,
16+
CreateEphemeralIndex,
17+
CreateEphemeralIndexRaw,
18+
CreateEscalation,
19+
CreateTask,
20+
DocumentExtraction,
21+
DocumentExtractionValidation,
22+
InvokeProcess,
23+
InvokeProcessRaw,
24+
InvokeSystemAgent,
25+
WaitBatchTransform,
26+
WaitDeepRag,
27+
WaitDeepRagRaw,
28+
WaitDocumentExtraction,
29+
WaitDocumentExtractionValidation,
30+
WaitEphemeralIndex,
31+
WaitEphemeralIndexRaw,
32+
WaitEscalation,
33+
WaitIntegrationEvent,
34+
WaitJob,
35+
WaitJobRaw,
36+
WaitSystemAgent,
37+
WaitTask,
38+
WaitUntil,
39+
)
40+
841
from ._api_client import ApiClient
942
from ._base_service import BaseService, resolve_trace_id
1043
from ._bindings import (
@@ -41,34 +74,6 @@
4174
from ._user_agent import user_agent_value
4275
from .auth import TokenData
4376
from .dynamic_schema import jsonschema_to_pydantic
44-
from .interrupt_models import (
45-
CreateBatchTransform,
46-
CreateDeepRag,
47-
CreateDeepRagRaw,
48-
CreateEphemeralIndex,
49-
CreateEphemeralIndexRaw,
50-
CreateEscalation,
51-
CreateTask,
52-
DocumentExtraction,
53-
DocumentExtractionValidation,
54-
InvokeProcess,
55-
InvokeProcessRaw,
56-
InvokeSystemAgent,
57-
WaitBatchTransform,
58-
WaitDeepRag,
59-
WaitDeepRagRaw,
60-
WaitDocumentExtraction,
61-
WaitDocumentExtractionValidation,
62-
WaitEphemeralIndex,
63-
WaitEphemeralIndexRaw,
64-
WaitEscalation,
65-
WaitIntegrationEvent,
66-
WaitJob,
67-
WaitJobRaw,
68-
WaitSystemAgent,
69-
WaitTask,
70-
WaitUntil,
71-
)
7277
from .paging import PagedResult
7378
from .timeout import (
7479
UiPathTimeoutError,
@@ -151,3 +156,49 @@
151156
]
152157

153158
from .validation import validate_pagination_params
159+
160+
_INTERRUPT_MODEL_NAMES = {
161+
"CreateBatchTransform",
162+
"CreateDeepRag",
163+
"CreateDeepRagRaw",
164+
"CreateEphemeralIndex",
165+
"CreateEphemeralIndexRaw",
166+
"CreateEscalation",
167+
"CreateTask",
168+
"DocumentExtraction",
169+
"DocumentExtractionValidation",
170+
"InvokeProcess",
171+
"InvokeProcessRaw",
172+
"InvokeSystemAgent",
173+
"WaitBatchTransform",
174+
"WaitDeepRag",
175+
"WaitDeepRagRaw",
176+
"WaitDocumentExtraction",
177+
"WaitDocumentExtractionValidation",
178+
"WaitEphemeralIndex",
179+
"WaitEphemeralIndexRaw",
180+
"WaitEscalation",
181+
"WaitIntegrationEvent",
182+
"WaitJob",
183+
"WaitJobRaw",
184+
"WaitSystemAgent",
185+
"WaitTask",
186+
"WaitUntil",
187+
}
188+
189+
190+
def __getattr__(name: str) -> Any:
191+
"""Resolve moved interrupt models on demand."""
192+
if name not in _INTERRUPT_MODEL_NAMES:
193+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
194+
195+
from . import interrupt_models
196+
197+
value = interrupt_models._resolve(name)
198+
globals()[name] = value
199+
return value
200+
201+
202+
def __dir__() -> list[str]:
203+
"""List common exports, including compatibility aliases."""
204+
return sorted(set(globals()) | set(__all__))

0 commit comments

Comments
 (0)