Repository navigation
feat(guardrails): give post-execution guardrails only the files the output references [AL-611] - #1144
Conversation
…utput references [AL-611] The guardrail at agent output and the guardrail after each LLM call used to receive every file in the run's attachment registry. Input uploads were judged again as if the agent had produced them, which gave the Intellectual Property guardrail false positives, and with inputs first under the five-file cap the files the agent actually produced could be dropped. Each guardrail node now takes one attachment source. The agent-output check scans the agent result, and the post-LLM check scans the tool-call arguments of the last message. Each sends only the registered files they reference. The pre-execution checks at Agent and LLM scope still send the whole registry, and the tool checks keep their behaviour on the same mechanism. The scan walks the payload in document order, so the files written first are the ones kept under the cap. Resolution now returns one of three answers. None says nothing about files: the guardrail is text only, or the payload could not be scanned, and the backend falls back to the text. An empty list says files apply and there are none, so a files-only guardrail passes without judging the text. Otherwise the list holds the files to inspect. The scope is read before the empty-registry shortcut. The 400 retry still drops the references, and a 400 on an empty list propagates. The output-file verification node also registers the references Orchestrator verified, so the agent-output check can resolve files from tools that do not register their attachments. Without a job key nothing was verified and nothing is registered. That verification runs only when output files are enabled for the agent. When it is off, a file from such a tool is not in the registry at agent output, so the check sends an empty list for it, as the full-registry send did before. A test pins that an agent-output check in Files scope with no referenced file puts an empty attachments list on the wire, which uipath-platform sends from 0.2.36; the floor moves to uipath-platform>=0.2.36. The version moves to 0.18.34. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
df71bc4 to
0da42bd
Compare
There was a problem hiding this comment.
🟡 Changes recommended
A UUID-valued non-attachment ID can mask nested file references from post-execution guardrails.
1 open finding
What changed in this PR
Restricts post-execution guardrails to files referenced by ReAct agent outputs or LLM tool calls.
Changes:
- Adds three-way attachment resolution:
None,[], or referenced files. - Registers Orchestrator-verified output attachments.
- Updates dependencies, version, and guardrail test coverage.
| File | Description |
|---|---|
pyproject.toml |
Bumps package and platform dependency versions. |
uv.lock |
Updates locked package versions. |
src/uipath_langchain/agent/attachments/output_files.py |
Builds registry entries from verified outputs. |
src/uipath_langchain/agent/guardrails/attachment_refs.py |
Resolves payload-specific attachment references. |
src/uipath_langchain/agent/guardrails/guardrail_nodes.py |
Selects attachment sources by execution stage. |
src/uipath_langchain/agent/react/output_files_node.py |
Registers verified output files before termination. |
tests/agent/attachments/test_output_files.py |
Tests verified attachment registration. |
tests/agent/guardrails/test_agent_terminate_guardrails_subgraph_run.py |
Tests output guardrail subgraph behavior. |
tests/agent/guardrails/test_attachment_refs.py |
Tests three-way resolution and ordering. |
tests/agent/guardrails/test_guardrail_nodes.py |
Tests scoped post-execution attachments. |
tests/agent/react/test_output_file_guardrails.py |
Adds full-graph attachment filtering coverage. |
tests/agent/react/test_output_files_node.py |
Tests output-node registry updates. |
🧠 Review effort: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| if _is_mention(value): | ||
| yield value | ||
| else: | ||
| stack.extend((item, depth + 1) for item in value.values()) | ||
| stack.extend( | ||
| (item, depth + 1) for item in reversed(list(value.values())) | ||
| ) |
| attachment_source=( | ||
| _tool_call_args | ||
| if execution_stage == ExecutionStage.POST_EXECUTION | ||
| else None |
There was a problem hiding this comment.
Why for LLM guardrail node we pass None for PRE exec?
There was a problem hiding this comment.
the None is attachment_source=None, which means "no single payload to read files from". It doesn't mean "send no files". At LLM PRE the guardrail still gets every file in the run, exactly as on main.
How the code reads it: in _resolve_attachments (guardrail_nodes.py:212-238), there are two cases:
- No source: the node judges the conversation so far, so it sends the run's whole attachment registry through resolve_guardrail_attachments. That covers the input files and the files earlier tools registered.
- A source: the node judges one payload, so it sends only the files that payload mentions.
Why PRE and POST differ:
- LLM PRE checks what goes into the model. The model is about to receive the whole context, including files attached many turns back. If PRE only read the last message, a file uploaded at the start would never be checked before the model reads it.
- LLM POST checks what the model produced, meaning its tool-call arguments. Sending the whole registry there was the AL-611 bug: input files were scanned again as if the model had produced them, which gave IP false positives. So POST now sends only the files the tool-call arguments reference (D3), or [] when there are none.
- PRE is unchanged on purpose. On main, Agent and LLM scope already sent the whole registry at every stage. AL-611 is about output files, so only the post-execution checks were narrowed. Agent PRE also passes no source, for the same reason.
There was a problem hiding this comment.
At LLM pre, the guardrail is sent references to every file the run has registered so far. attachment_source=None only means there is no single payload to search for file mentions; the files come from the run's attachment registry, inner_state.job_attachments.
1. The registry is filled as the run goes. It is a dict from attachment id to Attachment, typed in types.py:27 with a merge_dicts reducer, so writes only add entries. Three places write to it:
- The agent's input files. init_node.py:63-99 adds every job-attachment field in the agent's input schema.
- Files that tools return. job_attachment_wrapper.py:110-122 adds the file fields of a tool result, for tools whose output schema has job-attachment fields (Create File, for example).
- Output files Orchestrator verified. output_files_node.py:93, new in this PR, adds the agent-output file references that Orchestrator confirmed.
2. Without a source, the node reads the whole registry. In guardrail_nodes.py:224-226, _resolve_attachments calls resolve_guardrail_attachments(state.inner_state.job_attachments, guardrail). That function, in attachment_refs.py:81-95, returns one of three things:
Noneif the guardrail'sappliesTois Text only. The backend then checks only the text.[]if the run has no files yet. A Files-only guardrail then passes without a provider call (D6).- Otherwise up to 5 references,
{id, fileName, mimeType}each, built from the registry entries.
3. Only references go to the backend, never file content. The node sends the list as the attachments field of the validate call. The Agents backend resolves each id through Orchestrator, downloads the file, and works out its type (the #6598 sniffing). It then appends the file's text to what the provider checks.
So at LLM pre the guardrail sees every file the model could have in its context at that point: the input files plus whatever earlier tools returned. It doesn't read the last message for mentions, because a file attached at the start of the run is still in the model's context many turns later. This part is unchanged from main.
At LLM post, the source is the tool-call arguments, so only the files those arguments mention are sent. They are still looked up in the same registry, so an id the run never held is dropped.
One limit: the registry only knows files that came in through schema-typed job-attachment fields. A file id that appears only in a message's free text, with no schema-typed field behind it, is never registered, so no guardrail sees it at LLM pre.
… [AL-611] The payload scan stopped at any mapping with a UUID ID. A queue item or other record carrying its own UUID ID therefore hid every file nested under it, so a post-execution guardrail was never sent that file. The scan now yields the mention and still visits its children; an ID the run does not hold is dropped at the registry lookup, and a file mentioned twice is still sent once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
…ow-code does [AL-599] After the rebase onto #1144 (AL-611), the low-code resolvers answer in three ways: None says nothing about files, [] means files apply and none were found, a list names the files. The attachment_refs shim keeps that contract on top of the shared uipath-platform module, so guardrail_nodes and its tests behave exactly as on main. The coded flavors now follow the same contract. A state, output or tool result that cannot be read yields None instead of [], so a Files-scoped guardrail evaluates the text rather than passing on an explicit empty list. _evaluate_guardrail sends a bare call for None and [] only when the guardrail asks for files. The decorator adapter passes an explicit [] only to evaluators that declare forwards_attachments, so hand-written four-argument evaluators keep their call shape. Version 0.18.36 (main is at 0.18.35); uipath-platform floor stays 0.2.37. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ow-code does [AL-599] After the rebase onto #1144 (AL-611), the low-code resolvers answer in three ways: None says nothing about files, [] means files apply and none were found, a list names the files. The attachment_refs shim keeps that contract on top of the shared uipath-platform module, so guardrail_nodes and its tests behave exactly as on main. The coded flavors now follow the same contract. A state, output or tool result that cannot be read yields None instead of [], so a Files-scoped guardrail evaluates the text rather than passing on an explicit empty list. _evaluate_guardrail sends a bare call for None and [] only when the guardrail asks for files. The decorator adapter passes an explicit [] only to evaluators that declare forwards_attachments, so hand-written four-argument evaluators keep their call shape. Version 0.18.36 (main is at 0.18.35); uipath-platform floor stays 0.2.37. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…h detection_scope [AL-599]
Coded-agent guardrails read the files a run holds, as low-code guardrails do,
in both flavors.
- Middleware: every built-in UiPath*Middleware takes detection_scope=
(DetectionScope.TEXT / FILES / BOTH or "Text" / "Files" / "Both", sent as the
`appliesTo` validator parameter; None leaves the backend's per-validator
default). Hooks derive the run's attachment registry from the message history
(additional_kwargs["attachments"], <uip:attachments> blocks, ToolMessages,
SDK-tagged file/image blocks, uipath__job_attachments) and forward references
{id, fileName, mimeType}: the whole registry at agent/LLM pre-checks, only
registry-held mentions in 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. An explicit [] is sent when files were requested but
none found, nothing is said about files when a state or payload cannot be
read, and a 400 on the attachment list re-evaluates text-only once.
Text-only payloads stay byte-identical.
- Decorator adapter: tool, LLM and graph wraps forward the attachments each can
see; decorated tools are now guarded on ainvoke too (a ContextVar token
prevents double checks for sync tools routed through an executor).
- agent/guardrails/attachment_refs.py becomes a shim over
uipath.platform.guardrails.attachments that keeps #1144's None / [] / list
contract; low-code behaviour is unchanged. agent/multimodal/invoke.py tags
SDK-built blocks with the attachment id in extras.
- Exports DetectionScope and render_attachments_block from
uipath_langchain.guardrails.
- samples/cv-screener-agent: a coded port of the e2e CV screener mixing both
flavors, with PARITY_CHECK and GUARDRAILS_DETECTION_SCOPE switches and an
upload_attachment.py helper.
- Docs: "Files (attachments)" sections for both flavors.
- Version 0.18.36; requires uipath-platform 0.2.37 (UiPath/uipath-python#1940).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…h detection_scope [AL-599]
Coded-agent guardrails read the files a run holds, as low-code guardrails do,
in both flavors.
- Middleware: every built-in UiPath*Middleware takes detection_scope=
(DetectionScope.TEXT / FILES / BOTH or "Text" / "Files" / "Both", sent as the
`detectionScope` validator parameter; None leaves the backend's per-validator
default). Hooks derive the run's attachment registry from the message history
(additional_kwargs["attachments"], <uip:attachments> blocks, ToolMessages,
SDK-tagged file/image blocks, uipath__job_attachments) and forward references
{id, fileName, mimeType}: the whole registry at agent/LLM pre-checks, only
registry-held mentions in 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. An explicit [] is sent when files were requested but
none found, nothing is said about files when a state or payload cannot be
read, and a 400 on the attachment list re-evaluates text-only once.
Text-only payloads stay byte-identical.
- Decorator adapter: tool, LLM and graph wraps forward the attachments each can
see; decorated tools are now guarded on ainvoke too (a ContextVar token
prevents double checks for sync tools routed through an executor).
- agent/guardrails/attachment_refs.py becomes a shim over
uipath.platform.guardrails.attachments that keeps #1144's None / [] / list
contract; low-code behaviour is unchanged. agent/multimodal/invoke.py tags
SDK-built blocks with the attachment id in extras.
- Exports DetectionScope and render_attachments_block from
uipath_langchain.guardrails.
- samples/cv-screener-agent: a coded port of the e2e CV screener mixing both
flavors, with PARITY_CHECK and GUARDRAILS_DETECTION_SCOPE switches and an
upload_attachment.py helper.
- Docs: "Files (attachments)" sections for both flavors.
- Reads and sends the detectionScope validator parameter (AL-606 rename,
stacked on #1150).
- Version 0.18.38; requires uipath-platform 0.2.37 (UiPath/uipath-python#1940).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…h detection_scope [AL-599]
Coded-agent guardrails read the files a run holds, as low-code guardrails do,
in both flavors.
- Middleware: every built-in UiPath*Middleware takes detection_scope=
(DetectionScope.TEXT / FILES / BOTH or "Text" / "Files" / "Both", sent as the
`detectionScope` validator parameter; None leaves the backend's per-validator
default). Hooks derive the run's attachment registry from the message history
(additional_kwargs["attachments"], <uip:attachments> blocks, ToolMessages,
SDK-tagged file/image blocks, uipath__job_attachments) and forward references
{id, fileName, mimeType}: the whole registry at agent/LLM pre-checks, only
registry-held mentions in 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. An explicit [] is sent when files were requested but
none found, nothing is said about files when a state or payload cannot be
read, and a 400 on the attachment list re-evaluates text-only once.
Text-only payloads stay byte-identical.
- Decorator adapter: tool, LLM and graph wraps forward the attachments each can
see; decorated tools are now guarded on ainvoke too (a ContextVar token
prevents double checks for sync tools routed through an executor).
- agent/guardrails/attachment_refs.py becomes a shim over
uipath.platform.guardrails.attachments that keeps #1144's None / [] / list
contract; low-code behaviour is unchanged. agent/multimodal/invoke.py tags
SDK-built blocks with the attachment id in extras.
- Exports DetectionScope and render_attachments_block from
uipath_langchain.guardrails.
- samples/cv-screener-agent: a coded port of the e2e CV screener mixing both
flavors, with PARITY_CHECK and GUARDRAILS_DETECTION_SCOPE switches and an
upload_attachment.py helper.
- Docs: "Files (attachments)" sections for both flavors.
- Reads and sends the detectionScope validator parameter (AL-606 rename,
after #1150).
- Version 0.18.37; requires uipath-platform 0.2.37 (UiPath/uipath-python#1940).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…h detection_scope [AL-599]
Coded-agent guardrails read the files a run holds, as low-code guardrails do,
in both flavors.
- Middleware: every built-in UiPath*Middleware takes detection_scope=
(DetectionScope.TEXT / FILES / BOTH or "Text" / "Files" / "Both", sent as the
`detectionScope` validator parameter; None leaves the backend's per-validator
default). Hooks derive the run's attachment registry from the message history
(additional_kwargs["attachments"], <uip:attachments> blocks, ToolMessages,
SDK-tagged file/image blocks, uipath__job_attachments) and forward references
{id, fileName, mimeType}: the whole registry at agent/LLM pre-checks, only
registry-held mentions in 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. An explicit [] is sent when files were requested but
none found, nothing is said about files when a state or payload cannot be
read, and a 400 on the attachment list re-evaluates text-only once.
Text-only payloads stay byte-identical.
- Decorator adapter: tool, LLM and graph wraps forward the attachments each can
see; decorated tools are now guarded on ainvoke too (a ContextVar token
prevents double checks for sync tools routed through an executor).
- agent/guardrails/attachment_refs.py becomes a shim over
uipath.platform.guardrails.attachments that keeps #1144's None / [] / list
contract; low-code behaviour is unchanged. agent/multimodal/invoke.py tags
SDK-built blocks with the attachment id in extras.
- Exports DetectionScope and render_attachments_block from
uipath_langchain.guardrails.
- samples/cv-screener-agent: a coded port of the e2e CV screener mixing both
flavors, with PARITY_CHECK and GUARDRAILS_DETECTION_SCOPE switches and an
upload_attachment.py helper.
- Docs: "Files (attachments)" sections for both flavors.
- Reads and sends the detectionScope validator parameter (AL-606 rename,
after #1150).
- Version 0.18.45; requires uipath-platform 0.2.41 (UiPath/uipath-python#1940).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…h detection_scope [AL-599]
Coded-agent guardrails read the files a run holds, as low-code guardrails do,
in both flavors.
- Middleware: every built-in UiPath*Middleware takes detection_scope=
(DetectionScope.TEXT / FILES / BOTH or "Text" / "Files" / "Both", sent as the
`detectionScope` validator parameter; None sends none, which the backend
reads as Text for every validator, so files are forwarded only when a
guardrail asks for Files or Both). Hooks derive the run's attachment registry from the message history
(additional_kwargs["attachments"], <uip:attachments> blocks, ToolMessages,
SDK-tagged file/image blocks, uipath__job_attachments) and forward references
{id, fileName, mimeType}: the whole registry at agent/LLM pre-checks, only
registry-held mentions in 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. An explicit [] is sent when files were requested but
none found, nothing is said about files when a state or payload cannot be
read, and a 400 on the attachment list re-evaluates text-only once.
Text-only payloads stay byte-identical.
- Decorator adapter: tool, LLM and graph wraps forward the attachments each can
see; decorated tools are now guarded on ainvoke too (a ContextVar token
prevents double checks for sync tools routed through an executor).
- agent/guardrails/attachment_refs.py becomes a shim over
uipath.platform.guardrails.attachments that keeps #1144's None / [] / list
contract; low-code behaviour is unchanged. agent/multimodal/invoke.py tags
SDK-built blocks with the attachment id in extras.
- Exports DetectionScope and render_attachments_block from
uipath_langchain.guardrails.
- samples/cv-screener-agent: a coded port of the e2e CV screener mixing both
flavors, with PARITY_CHECK and GUARDRAILS_DETECTION_SCOPE switches and an
upload_attachment.py helper.
- Docs: "Files (attachments)" sections for both flavors.
- Reads and sends the detectionScope validator parameter (AL-606 rename,
after #1150).
- Version 0.18.50; requires uipath-platform 0.2.41 (UiPath/uipath-python#1940).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>




What changed?
Post-execution guardrails get only the files the output references (AL-611).
agent_resultand sends only the registered files it references.end_execution.Three-way resolution:
None: text only, or the payload could not be scanned. The backend falls back to text.[]: files apply and there are none. A Files-only guardrail passes without judging the text.The 400 retry still drops the references, and a 400 on
[]propagates.Output-file verification now registers the references Orchestrator verified, so the agent-output check can resolve files from tools that don't register attachments. It does nothing without a job key.
Known limit: that verification runs only when output files are enabled for the agent, i.e. it has a Create File tool and is not conversational. Without it, a file from a tool that doesn't register attachments isn't sent at agent output.
Version 0.18.34. Floor
uipath-platform>=0.2.36, the first release that sends an empty attachments list.How has this been tested?
attachment_refsreturns.end_execution): turn 1 sends[], turn 2 sends the file, and agent output sends only the created file, not the input."attachments": []./validate), with an IP guardrail at Agent scope, Files, Text and an uploaded protected input file:[]and passed with no provider call, and theend_executionturn sent the file and was blocked with "Flagged files: code_sample.txt".Related (AL-611)
🤖 Generated with Claude Code