Repository navigation
feat(guardrails): @guardrail forwards attachment references and accepts applies_to [AL-599] - #1940
apetraru-uipath wants to merge 12 commits into
Conversation
… guardrails [AL-599] Move the attachment-reference discovery from uipath-langchain's attachment_refs.py into uipath.platform.guardrails.attachments so the @guardrail decorator and the LangChain middlewares share it. Mentions are recognised in wire ({"ID","FullName","MimeType"}) and snake-case shapes with str or uuid.UUID ids; references are capped at 5, names truncated to 260, registry-matched for middleware hooks or self-describing for decorated functions. Adds AppliesTo, with_applies_to and scope_includes_files. Pure, synchronous and never raises. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… [AL-599]
Scanning every value of a mention made a wire mention {ID, FullName, MimeType}
cost four of the 10,000 scanned values, so large low-code tool payloads hit the
node bound about four times sooner than the langchain scan and missed held files.
Only a mention's nested models and collections are queued now, which keeps
finding files next to an object's own id.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dd applies_to [AL-599] @guardrail now discovers Attachment instances and self-describing mentions ({"ID","FullName","MimeType"} or {"id","full_name","mime_type"}) in the raw arguments (PRE) and return value (POST) and forwards them by reference to API-based validators. GuardrailExclude'd parameters are not scanned. The new applies_to argument (AppliesTo member or "Text"/"Files"/"Both") is validated at decoration time and sent as the appliesTo enum parameter; Text never forwards files and None keeps the backend's per-validator default. Only BuiltInGuardrailValidator.run receives the new keyword arguments, so custom and third-party validators and adapters keep their signatures. Pydantic arguments are now dumped with mode="json" (falling back to the plain dump for fields without a JSON form), so an Attachment id reaches the backend and EscalateAction as a string. Bumps uipath-platform to 0.2.37. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…PI [AL-599] Document applies_to, attachment discovery and the references-only wire contract for @guardrail, extend the low-level example with GuardrailAttachment and flagged_attachment_ids, and add API reference entries for the attachments module. The generated SDK reference is unchanged: update_agents_md.py documents UiPath service methods only, and its output is identical for origin/main and this branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e for @guardrail [AL-599] BuiltInGuardrailValidator.run now re-evaluates the data once without attachments when a validate call that carried them fails with HTTP 400, logging a warning that names the guardrail, as the LangChain middleware and the low-code guardrail node do. Any other status, and a 400 on a text-only call, still propagate. run also drops the attachments when the guardrail it sends is scoped to text, so an appliesTo supplied by hand as a validator parameter (for example on a ByoValidator) is honoured, not only the decorator's applies_to argument. The evaluator compares applies_to by value instead of identity, and the attachments module docstring uses plain backticks so mkdocstrings renders it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…, ordered discovery [AL-599] - Pass attachments/applies_to only to a BuiltInGuardrailValidator whose run() takes them (or **kwargs); an old-signature override is called positionally and warned about once, so a blocking guardrail no longer silently passes. - Send attachments=[] when the scope explicitly includes files and none was found; the text-only request stays byte-identical otherwise. - Discover attachments in source order, so the first five are kept under the cap. - The HTTP 400 retry warning carries the dropped count; ids are logged at debug. - Type applies_to to accept the documented "Text"/"Files"/"Both" strings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…AL-599] Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…[AL-599] _make_evaluator now sets forwards_attachments on the evaluator: False when every attachment would be dropped (applies_to=TEXT, an appliesTo=Text validator parameter, a custom validator, or a built-in validator that overrides run() without the attachments/applies_to parameters). The LangChain adapter reads it to skip a placeholder-only call when a message has files but no text and the files would not reach the validator. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…warding [AL-599] Completes 6ac85c6 against the Task 3d brief: the _EvaluatorFn docstring now says forwards_attachments is an optional attribute, not a Protocol member, and that an evaluator without it counts as forwarding; a test pins the getattr(evaluator, "forwards_attachments", True) default adapters rely on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e [AL-599] The Protocol docstring already states that a missing forwards_attachments means forwarding. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
🟡 Changes recommended
Empty attachment lists are not transmitted, and the scanner’s node limit does not bound container materialization.
2 open findings
What changed in this PR
Adds attachment-aware guardrails and configurable text/file evaluation scopes to uipath-platform.
Changes:
- Adds bounded attachment discovery and reference conversion utilities.
- Extends
@guardrailand built-in validators with attachment forwarding andapplies_to. - Adds documentation, extensive tests, and version updates.
| File | Description |
|---|---|
packages/uipath/uv.lock |
Updates the platform dependency version. |
packages/uipath/docs/core/guardrails.md |
Documents file-aware guardrails and low-level usage. |
packages/uipath-platform/uv.lock |
Updates the package lock version. |
packages/uipath-platform/tests/services/test_guardrails_decorators_attachments.py |
Tests decorator attachment behavior. |
packages/uipath-platform/tests/services/test_guardrails_attachments.py |
Tests discovery, scoping, and bounds. |
packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/_base.py |
Adds scoped attachment forwarding and fallback. |
packages/uipath-platform/src/uipath/platform/guardrails/decorators/_guardrail.py |
Adds applies_to and attachment discovery. |
packages/uipath-platform/src/uipath/platform/guardrails/decorators/_core.py |
Extends evaluator and serialization behavior. |
packages/uipath-platform/src/uipath/platform/guardrails/decorators/__init__.py |
Exports AppliesTo. |
packages/uipath-platform/src/uipath/platform/guardrails/attachments.py |
Implements attachment discovery utilities. |
packages/uipath-platform/src/uipath/platform/guardrails/__init__.py |
Exports attachment APIs. |
packages/uipath-platform/pyproject.toml |
Bumps the package version to 0.2.37. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if isinstance(value, BaseModel): | ||
| children: list[Any] = [value.model_dump(by_alias=True)] | ||
| elif isinstance(value, Mapping): | ||
| children = list(value.values()) | ||
| if _mapping_id(value) is not None: | ||
| yield value | ||
| children = [item for item in children if isinstance(item, _CONTAINERS)] | ||
| elif isinstance(value, (list, tuple, set, frozenset)): | ||
| children = list(value) |
| if not files_apply or not attachments: | ||
| if files_requested: | ||
| return guardrails.evaluate_guardrail(data, built_in, attachments=[]) |
There was a problem hiding this comment.
Epixa reviewed this pull request and added non-blocking comments.
1 medium, 2 low · in packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/_base.py, packages/uipath-platform/src/uipath/platform/guardrails/decorators/_guardrail.py, packages/uipath-platform/src/uipath/platform/guardrails/decorators/_core.py
| guardrails = self._uipath.guardrails | ||
| if not files_apply or not attachments: | ||
| if files_requested: | ||
| return guardrails.evaluate_guardrail(data, built_in, attachments=[]) |
There was a problem hiding this comment.
Important
Severity: Medium
Kind: logic_bug
This PR sends an empty attachment list that never reaches the guardrails API
This PR adds return guardrails.evaluate_guardrail(data, built_in, attachments=[]) at _base.py:178 to tell the backend that files are in scope but none exist.
Impact: A guardrail scoped to Files that discovers no file sends the same payload as a guardrail with no file scope, so the backend cannot separate the two cases. The docstring at _base.py:152-156 and guardrails.md promise an empty attachment list, and that promise is not kept. The new test only asserts the mocked SDK keyword, so continuous integration cannot catch the gap.
Fix: Forward an explicit empty list: change if attachments: to if attachments is not None: in GuardrailsService.evaluate_guardrail, and keep the truthy checks for the folder header and the longer timeout. The alternative is to delete the files_requested branch and the matching sentences in the docstring and in packages/uipath/docs/core/guardrails.md.
Evidence:
if not files_apply or not attachments:
if files_requested:
return guardrails.evaluate_guardrail(data, built_in, attachments=[])
return guardrails.evaluate_guardrail(data, built_in)| ) -> None: | ||
| """Evaluate PRE guardrail and apply any modifications to *bound* in-place.""" | ||
| input_data = _collect_input(bound, excluded) | ||
| attachments = _discover_input_attachments(bound, excluded) |
There was a problem hiding this comment.
Note
Severity: Low
Kind: config_risk
This change scans every call for attachments even when the guardrail throws them away
This PR calls _discover_input_attachments at _guardrail.py:55 and _discover_output_attachments at _guardrail.py:85 on every invocation.
Impact: Each call of a function guarded by a CustomValidator, a text scoped guardrail, or a validator with the original run signature pays an extra deep scan of up to MAX_SCAN_NODES (10,000) values plus a pydantic dump of each nested model. The references are discarded inside _eval, so the cost buys nothing and the guardrail overhead on large payloads about doubles.
Fix: Skip discovery when the evaluator reports that it drops files: guard both calls with getattr(evaluator, "forwards_attachments", True). The behavior stays the same, because _eval and BuiltInGuardrailValidator.run already drop the references in exactly those cases.
Evidence:
input_data = _collect_input(bound, excluded)
attachments = _discover_input_attachments(bound, excluded)| """ | ||
| built_in = isinstance(validator, BuiltInGuardrailValidator) | ||
| forwards_files = built_in and _accepts_files(validator) | ||
| warned = False |
There was a problem hiding this comment.
Note
Severity: Low
Kind: config_risk
This PR accepts applies_to on a local validator and then ignores it with no warning
This PR adds the applies_to parameter to @guardrail for every validator type, and _core.py:138-139 sets forwards_files to False for each validator that is not a BuiltInGuardrailValidator.
Impact: An author who writes @guardrail(validator=CustomValidator(rule), action=BlockAction(), applies_to=AppliesTo.FILES) believes the files are checked. The local rule receives the serialized text only, no file is examined, and no log line reports the gap, so a file blocking control that never runs looks healthy.
Fix: Warn once at decoration time when applies_to is set and the validator is not a BuiltInGuardrailValidator, next to the existing warn-once branch in _make_evaluator.
Evidence:
built_in = isinstance(validator, BuiltInGuardrailValidator)
forwards_files = built_in and _accepts_files(validator)
warned = False
Epixa-Dogfood ReviewApproved · 3 medium, 4 low · 13/13 files reviewed What this PR doesThis change lets a coded guardrail inspect the files a decorated function receives or returns. The runtime finds attachment references in the arguments and in the return value, then forwards only the identifier, the file name, and the media type to the validation service. A new option tells the service whether to evaluate text, files, or both. New tests cover discovery limits, scope handling, and the fallback when the service rejects the references. Details
Full review: 02e9da6 |
34f9971 to
1db820a
Compare
…s no arguments [AL-599]
A decorated zero-argument factory sent a validate call with input {} at
import time and logged an ERROR traceback when no backend was reachable.
PRE now returns early, with a debug log, when the collected input is empty;
POST behaviour is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
🚨 Heads up:
|
🚨 Heads up:
|
There was a problem hiding this comment.
Epixa reviewed this pull request and added non-blocking comments.
2 medium, 2 low · in packages/uipath-platform/src/uipath/platform/guardrails/attachments.py, packages/uipath-platform/src/uipath/platform/guardrails/decorators/_core.py, packages/uipath-platform/src/uipath/platform/guardrails/decorators/validators/_base.py
| elif isinstance(item, Mapping): | ||
| attachment_id = _mapping_id(item) | ||
| file_name = _first_text(item, _NAME_KEYS) | ||
| mime_type = _first_text(item, _MIME_TYPE_KEYS) |
There was a problem hiding this comment.
Important
Severity: Medium
Kind: question
This PR forwards the caller's own file name and media type, so a tool caller can relabel the file a guardrail inspects
This PR forwards the mention's own FullName and MimeType for every discovered mapping . The registry path replaces both with the run's values because, as the module docstring says, a payload can name anything.
Impact: A model can pair a real attachment id with a benign name and media type. The module docstring states that the backend 'decides which validators and file types it can inspect'. If the backend trusts the forwarded media type, a file-scoped guardrail inspects nothing, returns a pass, and a block action allows the call.
Fix: Confirm whether the Agents backend re-resolves the file name and media type from Orchestrator for a given attachment id. If the backend trusts the forwarded values, resolve them from Orchestrator before the decorator forwards them, as referenced_attachments already does with the registry.
Evidence:
elif isinstance(item, Mapping):
attachment_id = _mapping_id(item)
file_name = _first_text(item, _NAME_KEYS)
mime_type = _first_text(item, _MIME_TYPE_KEYS)| kwargs: dict[str, Any] = {} | ||
| if applies_to is not None: | ||
| kwargs["applies_to"] = applies_to | ||
| if attachments and applies_to != AppliesTo.TEXT: |
There was a problem hiding this comment.
Note
Severity: Low
Kind: config_risk
This change forwards file references when no scope is set, so a text-only guardrail now pays the 60-second attachment timeout
This PR forwards attachments on the default scope: _core.py line 163 tests only applies_to != AppliesTo.TEXT, and an absent appliesTo parameter also keeps files in scope.
Impact: _guardrails_service.py line 174 sets a 60-second timeout when the attachment list is not empty, against the 30-second client default in _http_config.py. A guarded function that takes an attachment argument can now block for 60 seconds per PRE and POST evaluation, and it sends file ids and names that the backend discards.
Fix: Forward the references only when the resolved scope includes files. Let each built-in validator declare its backend default scope, so the judge validator keeps the current behavior and a text-default validator sends no attachment list and keeps the 30-second timeout.
Evidence:
if attachments and applies_to != AppliesTo.TEXT:
kwargs["attachments"] = list(attachments)| seen.add(reference.id) | ||
| references.append(reference) | ||
| if len(references) == MAX_ATTACHMENTS: | ||
| break |
There was a problem hiding this comment.
Note
Severity: Low
Kind: config_risk
This change drops every file past the fifth with no log line, so a block guardrail never sees them
This PR caps the forwarded references at five and breaks out of the loop silently . Every other drop path in this module logs at debug or warning level, so this cap is the only silent loss of a complete, valid file reference.
Impact: A decorated function that receives six or more files forwards only the first five. The guardrail never inspects the rest, and a block action allows the call. The operator sees a pass with no record that files were dropped.
Fix: Log a warning with the guardrail name and the dropped count before the loop ends. Add a test that asserts the warning when more than five valid references are discovered.
Evidence:
references.append(reference)
if len(references) == MAX_ATTACHMENTS:
break| name, | ||
| ", ".join(attachment.id for attachment in attachments), | ||
| ) | ||
| return guardrails.evaluate_guardrail(data, built_in) |
There was a problem hiding this comment.
Important
Severity: Medium
Kind: config_risk
This change lets a file-scoped guardrail pass silently when the service rejects the file references
This PR catches HTTP 400 from the attachment call and repeats the call without attachments . The repeated request keeps the appliesTo scope, so a Files guardrail evaluates with no file at all and most likely returns a pass.
Impact: A rejected reference turns a file guardrail into a no-op. A block action does not fire, the function runs on unchecked file content, and only one warning log line records the degradation.
Fix: Treat the fallback result as degraded. Re-evaluate with the text scope only when the guardrail also covers text, and fail closed or raise for a Files scope, so a block action still protects the call.
Evidence:
if exc.status_code != 400:
raise
logger.warning(
"Guardrail '%s' rejected the attachment references (HTTP 400); "
"re-evaluating without attachments (%d dropped).",
|
|
CI note: the only red non-integration check, |




What changed?
Coded-agent guardrails built with
@guardrailcan now read the files a run holds, the way low-code guardrails have since AL-592/593/595.uipath.platform.guardrails.attachments. The attachment-discovery core that lived in uipath-langchain's low-code node moves here so both coded flavors (this decorator and the LangChain middlewares) share it:AppliesTo(Text/Files/Both, the wire values of theappliesTovalidator parameter),with_applies_to,scope_includes_files,iter_attachment_mentions,mention_id,to_references,referenced_attachments(registry-gated: only ids the run holds, name and type from the registry) anddiscover_attachments(self-describing mentions). Everything is synchronous, pure, bounded (depth 32, 10 000 values, 5 references, 260-char names) and never raises. Mentions are recognised by wire keys (ID/FullName/MimeType) and snake-case keys, ids asstroruuid.UUID.@guardrail(..., applies_to=...).AppliesTomember or one of the strings"Text","Files","Both"(validated at decoration time).None(default) sends no parameter, so the Agents backend applies its per-validator default:Bothfor LLM-as-judge,Textfor the others — identical to a low-code guardrail without the dropdown. Attachment references found in the raw arguments (PRE) and the raw return value (POST) are forwarded asattachments=[{id, fileName, mimeType}];GuardrailExcluded parameters are not scanned. The backend resolves each id through Orchestrator with the run's own identity; no URL or bytes cross the wire.BuiltInGuardrailValidator.run(..., *, attachments=None, applies_to=None)applies the scope (also a hand-suppliedappliesToparameter), drops files for a text scope (explicit or via a hand-suppliedappliesToparameter), sends an explicit empty list when files were requested but none were found (forward-compatible with feat(guardrails): send an empty attachments list when a step has no files [AL-611] #1939's wire change), and re-evaluates text-only once when the backend rejects the attachment list with HTTP 400, like the low-code node and the middleware. The text-only call stays byte-identical to today._EvaluatorFnis aProtocolwith an optional keywordattachments; kwargs reach a validator'srun()only when its signature accepts them (a third-party override with the old signature is called positionally and warned about once), and the evaluator exposesforwards_attachmentsso adapters can skip useless calls. Custom validators are untouched. A decorated callable that receives no arguments (an LLM/agent factory) no longer sends a PRE call with{}._serialize_valuedumps models in JSON mode (fallback to the plain dump for arbitrary-type fields):uuid.UUID,datetimeand enums become JSON-safe strings in the guardrailinput, which also fixes a latentjson.dumpscrash in uipath-langchain'sEscalateActionfor a typedAttachmentargument. Note for release notes: the text payload for such fields changes from the Python repr (UUID('…')) to the plain string.docs/core/guardrails.mdgains a "Files (attachments)" section (what is forwarded, trust note, defaults, known gap for raw bytes, degradation rules) and the low-level API example.How has this been tested?
packages/uipath-platform(7 skipped need credentials); 150+ new tests acrosstest_guardrails_attachments.pyandtest_guardrails_decorators_attachments.py: mention shapes and bounds, cap/dedupe/truncation, registry-vs-mention name precedence,with_applies_toround trip, scope matrix, decorator PRE/POST forwarding on sync and async functions,GuardrailExclude,applies_tovalidation and defaults, legacyrun()signature, custom validators untouched, 400 retry / 500 propagate, explicit empty list, byte-identical text-only call.test_attachment_refs.pytests pass against this module through the shim that UiPath/uipath-langchain-python (AL-599 branch) adds.Related (AL-599)
cv-screener-agentsample; draft until this release is on PyPI). Proven on alpha with the sample: injected CV blocked at the LLM pre-check, harmful PDF at agent input, recipe at the Analyze Files pre-check, non-EU CV logged by the judge; Text-scope control completes.attachments: []on the wire) — both branches bump the platform version from 0.2.35; the second to merge rebases one line.🤖 Generated with Claude Code