Skip to content

feat(guardrails): coded-agent guardrails forward attachment references and accept applies_to [AL-599] - #1145

Draft
apetraru-uipath wants to merge 11 commits into
mainfrom
feat/al-599-guardrail-attachments
Draft

apetraru-uipath wants to merge 11 commits into
mainfrom
feat/al-599-guardrail-attachments

Conversation

@apetraru-uipath

Copy link
Copy Markdown
Contributor

What changed?

Coded-agent guardrails read the files a run holds, the way low-code guardrails have since AL-592/593/595. Both coded flavors are covered.

  • Middleware (UiPath*Middleware). Every built-in middleware gains applies_to (AppliesTo.TEXT / FILES / BOTH, or the strings "Text" / "Files" / "Both"; None sends no parameter so the backend's per-validator default applies: Both for LLM-as-judge, Text for the others). Hooks derive the run's attachment registry from the message history — HumanMessages (additional_kwargs["attachments"] and <uip:attachments> blocks), ToolMessages, SDK-tagged multimodal blocks and the advanced agent's uipath__job_attachments state — and forward references through evaluate_guardrail(..., attachments=[{id, fileName, mimeType}]): whole registry at agent/LLM pre-checks, only registry-held mentions in the tool arguments at tool pre-checks, the tool's own result at tool post-checks, and only the files the output references at LLM/agent post-checks (AL-611 rule). A hand-supplied appliesTo=Text parameter also stops forwarding; an explicit empty list goes out when files were requested but none were found (forward-compatible with uipath-python#1939); a 400 on the attachment list re-evaluates text-only once with a warning, like the low-code node. Text-only payloads are byte-identical to today.
  • Decorator adapter (@guardrail on LangChain tools, chat models, graphs). Tool wraps forward the self-describing mentions in the arguments/result; LLM wraps use the message-list registry; graph wraps scan the input/output state and structured_response. Wrapped tools are now guarded on ainvoke too (a ContextVar token prevents double checks for sync tools routed through an executor) — before this, a @guardrail on a coroutine-only tool under create_agent never ran. The adapter reads the platform evaluator's forwards_attachments flag to skip placeholder-only calls for Text-scoped guardrails.
  • Shared discovery moved to uipath-platform. agent/guardrails/attachment_refs.py is a thin shim over uipath.platform.guardrails.attachments; low-code behaviour is unchanged (its 69 tests pass as they were). agent/multimodal/invoke.py tags SDK-built file/image blocks with the attachment id in extras so the registry can read them back.
  • Exports: AppliesTo and render_attachments_block from uipath_langchain.guardrails.
  • Sample samples/cv-screener-agent: a coded port of the Agents e2e CvScreenerGuardrailSolution mixing both flavors (decorator on the LLM factory and on the Analyze Files tool, middleware for the agent-scope checks), with a PARITY_CHECK switch that doubles every guardrail in the other flavor and a GUARDRAILS_APPLIES_TO control. Fixtures are the e2e files.
  • Docs: docs/guardrails.md gains "Files (attachments)" sections for both flavors (what is forwarded, hook table, how a file becomes visible, security note, Filter is text-only, known gap for raw bytes).
  • Dependencies/version: uipath-platform>=0.2.37 (feat(guardrails): @guardrail forwards attachment references and accepts applies_to [AL-599] uipath-python#1940), package version bumped. uv.lock is regenerated once 0.2.37 is on PyPI (this PR stays draft until then).

Trust boundary and degradation (for the security reviewers)

  • What is forwarded: only {id, fileName, mimeType}; the Agents backend resolves each id through Orchestrator with the run's own identity, so a forwarded id can never read more than that identity already can. Middleware hooks forward tool-call mentions only when the run already holds the id (agent input, conversation attachments, files a tool returned); the decorator's tool wrap forwards the self-describing mentions in the arguments, which is the same reach the Analyze Files tool already gives the model (resolve_attachments_to_file_infos resolves any id the model passes). A tool result that describes a file with an id, name and type makes that file "held" for later checks; that needs a valid id the run identity may read. Logs carry guardrail names and, at debug level, ids and file names, never content.
  • Explicit empty list: when a guardrail's scope includes files and files were requested (applies_to set, or an appliesTo parameter) but none were found, the call carries attachments=[]. With today's uipath-platform that is byte-identical to omitting the field; after feat(guardrails): send an empty attachments list when a step has no files [AL-611] uipath-python#1939 the backend treats it as "files apply, none present" (a Files-only guardrail passes without a provider call, Both still scans the text) — the AL-611 D6 rule low-code already follows.
  • HTTP 400 on the attachment list re-evaluates the text alone once, with a warning naming the guardrail and the number of dropped files, like the low-code node. A guardrail configured for files on a tenant whose file-support flags are off therefore degrades to text, it does not fail the run.

How has this been tested?

  • Full suite green locally (3879 passed); new tests: 100 middleware attachment tests (registry sources, per-hook forwarding, registry-only matching with an injected unknown id, applies_to matrix over all seven classes, explicit empty list, 400 retry, byte-identical no-files run through a real create_agent), 64 adapter tests (tool/LLM/graph discovery, ainvoke guard, stacked guardrails, old 4-positional evaluators), 6 multimodal tagging tests; the 12×2 parity scenarios unchanged; ruff, ruff format, mypy . clean.
  • Alpha (AdminStudioTest/APetraruTenant, deployed Agents backend, no local overrides) with the sample and the e2e fixtures uploaded as Orchestrator attachments: cv_prompt_injection.txt blocked by the decorator prompt-injection check at the LLM pre-check; the same file with GUARDRAILS_APPLIES_TO=Text was not blocked (control); cv_harmful_content.pdf blocked by the middleware harmful-content check at agent input; recipe_not_a_cv.md blocked at the Analyze Files pre-check; cv_non_eu_candidate.pdf logged [GUARDRAIL] [Candidate based outside the EU] Failed: Flagged: CV states current residence in Toronto, Canada… and the run continued. Logs captured per run. PARITY_CHECK=1 on the non-EU CV doubled the validate calls (6 → 12) with both judge twins flagging; on the recipe the middleware twin blocked first.
  • Known, pre-existing and outside this PR: the normalized UiPathChat client (uipath-langchain-client 1.21.0) does not send LangChain file content blocks to the provider (gateway 400 "Missing required parameter: messages[0].content[1].file"); the sample reads files through UiPathChatOpenAI, which works.

Related (AL-599)

🤖 Generated with Claude Code

apetraru-uipath and others added 11 commits October 12, 2026 01:00
… and accept applies_to [AL-599]

Built-in guardrail middlewares now send references to the files a run holds
with each validate call, as low-code guardrails already do. A registry is
rebuilt from the agent state on every hook: conversational attachments, the
<uip:attachments> blocks, SDK-tagged file and image blocks, files tool results
describe, and the advanced agent's uipath__job_attachments state. Agent and
LLM input checks forward the whole registry; output checks only the held files
the output names; a tool call the held files its arguments name, plus the
files its result returns. A call without files is unchanged.

The seven built-in constructors accept applies_to (AppliesTo or "Text",
"Files", "Both"); None sends no parameter so the backend default applies.
Files without text evaluate the "[no text content]" placeholder.
build_file_content_blocks_for tags blocks built from an attachment in extras.
attachment_refs.py delegates to uipath.platform.guardrails.attachments.
AppliesTo and render_attachments_block are exported from
uipath_langchain.guardrails. Raises the uipath-platform floor to 0.2.37.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…idate rejects the attachments [AL-599]

A validate call that carried attachment references and got HTTP 400 used to
fall through to the generic error handler, which only logs, so the whole
guardrail was skipped, text included. _evaluate_guardrail now retries the same
input without attachments on a 400 and returns that verdict, as the low-code
guardrail node does. A 400 without attachments and any other status still
reach the existing handler.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…L-599]

@guardrail on a LangChain tool, chat model, StateGraph or compiled graph now
passes the evaluator the attachment references each call can see, as the
middleware guardrails do. A tool forwards the self-describing files in its
arguments and its result. An LLM or agent input check forwards every file the
messages hold, plus typed Attachment state keys for an agent; an output check
only the held files the response or structured response names, plus the
attachments an agent returns in typed state keys. Files without text evaluate
the "[no text content]" placeholder, which is never written back.

attachments is passed only when there is at least one, so an evaluator with the
old four-positional signature keeps working for text-only calls. The evaluator
drops attachments for appliesTo=Text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…or drops files [AL-599]

A decorated chat model, StateGraph or compiled graph that sees files but no
text evaluated the "[no text content]" placeholder even when the guardrail
was scoped to text, so the validator judged only the constant. The adapter
now reads the evaluator's forwards_attachments flag (set by uipath-platform's
_make_evaluator; an evaluator without it is assumed to forward files) and
collects no attachments when it is False, so a files-only call is skipped and
a text call is unchanged. Text scope, an appliesTo=Text validator parameter,
custom validators and legacy run() overrides are covered; the backend default
(applies_to=None) keeps forwarding, since the judge defaults to Both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd the explicit empty list [AL-599]

The adapter skips a files-only call when the @guardrail evaluator says it
drops files. A contract test now reads that attribute from the evaluator the
installed uipath-platform hands the adapter, so a platform release without it
fails here by name instead of silently making placeholder calls.

The bare-id and unknown-id tests assert the exact request again: under the
Both scope the platform sends attachments=[] rather than omitting the field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nt sample [AL-599]

docs/guardrails.md documents applies_to on the built-in middleware classes and
on @guardrail, and adds a Files section for each flavor: what is forwarded
(references only, at most 5, gated by the tenant's file-support flags), how a
file becomes visible to a guardrail, which files each hook forwards, the
held-files rule, the [no text content] placeholder, the HTTP 400 text-only
retry, text-only filters, and the per-target discovery of the decorator,
including that adapter-wrapped tools guard invoke only.

samples/cv-screener-agent ports the Agents e2e CvScreener low-code agent with
its prompts, judge rules, examples, thresholds, action reasons and fixtures.
Prompt injection is a decorator on the LLM factory; harmful content, the EU
judge, the only-CVs judge and protected code are middleware. The only-CVs judge
is middleware rather than a decorator on the Analyze Files tool because that
tool has only a coroutine and the decorator guards invoke only. PARITY_CHECK=1
adds the other flavor of each guardrail; GUARDRAILS_APPLIES_TO overrides every
applies_to for a Text control run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ke [AL-599]

Only Tool/StructuredTool without a coroutine route ainvoke through invoke;
coroutine tools and BaseTool subclasses bypass the adapter's tool guard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The adapter overrode invoke only, so a tool create_agent runs through
ainvoke with a coroutine (async def, or coroutine= like the Analyze Files
tool) or a BaseTool subclass never reached the guard. ainvoke now applies
PRE and POST around the base call; a per-wrap ContextVar token makes the
invoke that StructuredTool/Tool.ainvoke routes a sync tool through skip its
own check, so each stage is evaluated once, stacked guardrails stay
independent and a tool that calls itself is still checked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ttachments [AL-599]

Middleware guardrails:
- forward nothing when an appliesTo validator parameter scopes the
  guardrail to text, not only when applies_to=TEXT;
- send attachments=[] when files were requested (Files/Both, by applies_to
  or parameter) and none was found, mirroring BuiltInGuardrailValidator.run;
  None and Text keep the bare call, and the 400 retry stays bare;
- read the structured response for references when the output has no
  AIMessage.

Decorator adapter: LLM POST with list content and files evaluates the text
blocks instead of the placeholder; docstrings name the echoed-input and
no-messages limits. BYO docstring states the connector default.

Tests type-check under CI's mypy run, pin the appliesTo EnumParameterValue
wire shape and cover the agent-scope placeholder without write-back.

Sample: Only CVs is the decorator on the Analyze Files tool again, with the
middleware as its PARITY_CHECK twin; the attachments block drops
"metadata": null. Docs and README follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-agent sample [AL-599]

Analyze Files failed with a gateway 400 because the normalized UiPathChat
client does not send file content blocks to the provider. The tool now reads
the file through UiPathChatOpenAI; the agent's own model never receives file
content and stays on UiPathChat. The README also describes the decorator's
Analyze Files check correctly: it forwards the mentions in the arguments with
the name and type they carry.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ate, docs nit [AL-599]

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
0.0% Coverage on New Code (required ≥ 90%)

See analysis details on SonarQube Cloud

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant