Skip to content

feat(guardrails): give post-execution guardrails only the files the output references [AL-611] - #1144

Merged
apetraru-uipath merged 2 commits into
mainfrom
feat/al-611-output-file-guardrails
Oct 13, 2026
Merged

apetraru-uipath merged 2 commits into
mainfrom
feat/al-611-output-file-guardrails

Conversation

@apetraru-uipath

@apetraru-uipath apetraru-uipath commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

What changed?

Post-execution guardrails get only the files the output references (AL-611).

  • Agent output scans agent_result and sends only the registered files it references.
  • After each LLM call the check scans the tool-call arguments of the last message, e.g. the file passed to end_execution.
  • Both apply to every validator. Before, both sent the run's whole attachment registry:
    • Input uploads were judged again as if the agent had produced them, which gave IP false positives.
    • With inputs first under the 5-file cap, the produced files could be dropped.
  • Agent and LLM pre-execution still send the whole registry. Tool checks keep their behaviour on the same mechanism.

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.
  • A list: the files to inspect.

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?

  • New tests:
    • Terminate and LLM-post attachment cases, plus a terminate subgraph run test where Block names the flagged file.
    • The three-way attachment_refs returns.
    • The output-file registry.
    • A full-graph scripted run (create-file then end_execution): turn 1 sends [], turn 2 sends the file, and agent output sends only the created file, not the input.
    • A wire test for "attachments": [].
  • Full suite 3741 passed. ruff, ruff format and mypy are clean.
  • Local end to end (Studio Web Debug, local robot with this branch and the SDK branch, local Agents with its PR, a logging proxy on /validate), with an IP guardrail at Agent scope, Files, Text and an uploaded protected input file:
    • On main, agent output sent the input and the output, and the run was blocked: a false positive.
    • On this branch, only the output file was sent and the run passed.
    • At LLM scope, the create-file turn sent [] and passed with no provider call, and the end_execution turn sent the file and was blocked with "Flagged files: code_sample.txt".

Related (AL-611)

🤖 Generated with Claude Code

…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>
@apetraru-uipath
apetraru-uipath force-pushed the feat/al-611-output-file-guardrails branch from df71bc4 to 0da42bd Compare October 12, 2026 17:06
@apetraru-uipath
apetraru-uipath marked this pull request as ready for review October 12, 2026 17:07
Copilot AI balanced review requested due to automatic review settings October 12, 2026 17:07

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Comment on lines +158 to +163
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why for LLM guardrail node we pass None for PRE exec?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

  • None if the guardrail's appliesTo is 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>
@sonarqubecloud

Copy link
Copy Markdown

@apetraru-uipath
apetraru-uipath merged commit 9d70391 into main Oct 13, 2026
23 of 47 checks passed
@apetraru-uipath
apetraru-uipath deleted the feat/al-611-output-file-guardrails branch October 13, 2026 08:54
apetraru-uipath added a commit that referenced this pull request Oct 13, 2026
…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>
apetraru-uipath added a commit that referenced this pull request Oct 13, 2026
…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>
apetraru-uipath added a commit that referenced this pull request Oct 13, 2026
…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>
apetraru-uipath added a commit that referenced this pull request Oct 14, 2026
…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>
apetraru-uipath added a commit that referenced this pull request Oct 14, 2026
…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>
apetraru-uipath added a commit that referenced this pull request Oct 18, 2026
…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>
apetraru-uipath added a commit that referenced this pull request Oct 21, 2026
…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>
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.

3 participants