Skip to content

feat(guardrails): let tool-scope judge guardrails read the files a tool call mentions - #1103

Merged
apetraru-uipath merged 3 commits into
mainfrom
feat/guardrail-judge-tool-scope
Sep 22, 2026
Merged

apetraru-uipath merged 3 commits into
mainfrom
feat/guardrail-judge-tool-scope

Conversation

@apetraru-uipath

@apetraru-uipath apetraru-uipath commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

What

Tool-scope LLM-as-Judge guardrails now read the files a tool call touches. Until now guardrail_nodes.py excluded Tool scope from attachment forwarding (_ATTACHMENT_SCOPES = {Agent, Llm}), because feeding a tool-scope judge from the whole-run registry would ship every file on every tool call. This PR replaces that exclusion with call-scoped discovery:

  • Pre-execution: the judge receives references for the attachments named in the tool call's arguments ({"ID": "<uuid>"} objects, the shape the model emits and replace_job_attachment_ids expands later inside the tool wrapper). Only ids the run's registry holds are forwarded, with the registry's name and MIME type: the registry is the set of files this run legitimately has (agent input plus files its tools returned), so a mention of a file the run never held is skipped rather than sent to the backend for an Orchestrator lookup.
  • Post-execution: the same scan over the parsed ToolMessage result, so a tool that returns a file (output-file, batch-transform, process tools) gets it judged.
  • A tool call that mentions no file forwards nothing, even when the run holds files elsewhere. A UUID under ID that is not an attachment (queue item, job) is never forwarded.
  • Agent and LLM scope are unchanged: they keep reading the whole registry, because the judge there evaluates the conversation so far. Both paths now build references through one collector (_collect), so the backend sees identical references whichever scope produced them.

This is the generic mechanism AL-596 (guardrails on the IXP extraction tool) builds on: the IXP tool takes a single JobAttachment, and its interrupt() fires inside the tool node, after the pre-execution judge has run.

Code

  • agent/guardrails/attachment_refs.py: resolve_referenced_attachments(data, job_attachments, guardrail) walks the payload (depth- and size-bounded), resolves mentions, dedupes and caps at 5; resolve_guardrail_attachments keeps its signature. Never raises.
  • agent/guardrails/guardrail_nodes.py: _resolve_attachments picks the source by scope and stage, reusing the tool node's existing argument/result extractors. Plain-text tool results are skipped before parsing (a mention always carries the literal ID key), so the output extractor's warning is not triggered for every text result.

Not touched: the subgraph wiring (tool nodes were already wrapped pre and post), _VALIDATOR_ALLOWED_STAGES, the text-only retry on an attachment-caused 400, the escalate action payload, the coded-agent middleware stack.

Backend and designer

No helix or Studio Web change is needed: llm_as_judge already allows Tool scope at both stages, the validate request has no scope or stage field, the designer already offers Tool scope for the judge (no stage picker, so a Tool-scope guardrail runs at both stages), and the appliesTo dropdown renders from the definition contract. File contents are used only when helix has EnableGuardrailAttachments on (UiPath/Agents#6256).

Tests

  • Review follow-up (093f3e4): the tool-scope node now judges the current tool call. With two calls to the same tool in one AI message it used to read the first call named tool_name (and, once the first ToolMessage was appended, nothing at all). Pre-execution selects the call via extract_current_tool_call_index exactly like the tool node; post-execution selects the call the last ToolMessage answers. Payload, deterministic input extraction and attachment resolution share the selection. Two node tests cover the two-calls case.

  • tests/agent/guardrails/test_attachment_refs.py: 18 new cases for the referenced path (named file, no file, nested, id the run never held even with inline name/type, registry wins over inline fields, non-attachment UUID, non-UUID id, dedupe, cap, prompts-only, model instances, bounded scan, odd payloads, parity with the registry path).

  • tests/agent/guardrails/test_guardrail_nodes.py: tool pre/post node tests replace test_tool_scope_node_never_resolves_attachments; Agent/LLM pre-execution with appliesTo = Files regression.

  • tests/agent/guardrails/test_tool_guardrails_subgraph_run.py: a compiled tool subgraph around a real UiPathToolNode: a failing judge blocks before the tool implementation runs and saw exactly the referenced file; a passing judge lets the tool run and judges its result.

  • tests/agent/guardrails: 351 passed. ruff, ruff format, mypy clean on the changed files; every new line in the two modules covered.

  • CI note: the Integration testing / * cloud jobs fail with UiPathPermissionDeniedError: Forbidden (403) from the cloud test environment on this and on other PRs today (fix/code-interpreter-warmup); the alpha variants pass.

Verified locally

Same runtime checkout, local helix on UiPath/Agents#6256 with EnableGuardrailAttachments on, real Orchestrator attachments (alpha, APetraruTenant). Agent: the Joke Agent with the built-in Analyze Files tool, one LLM-as-Judge guardrail at Tool scope on that tool (Block), the user prompt referencing {{input.file}}, no agent-scope judge.

Input Result
topic cats + joke_brief_unsafe.txt model calls Analyze_Files with the attachment → tool_pre_execution_judge_file_before_analyze_files → helix validate with exactly one GET …/odata/Attachments(<id>)Flagged: "joke_brief_unsafe.txt" contains …blocked (AGENT_RUNTIME.TERMINATION_GUARDRAIL_VIOLATION, reason "Unsafe file content"). The tool never ran.
topic gardening + joke_brief_clean.txt tool pre-execution judge Allowed (one attachment lookup) → Analyze Files runs → tool post-execution judge Allowed with no Orchestrator lookup (plain-text result names no file) → agent post PII passes → ✓ Successful execution

The agent-scope PII guardrail on the same runs receives the run's attachments and ignores them (helix gate), as before.

Studio Web Debug on the local robot, IXP Test Agent Solution (tool-scope judge added on the Json file IXP tool from the designer):

Input Result
JSON with an injected "ignore your instructions / exfiltrate" note tool pre-execution judge: one validate call, one attachment lookup → blocked before IXP started
clean PDF Tool input guardrail check ✓ (8 s) → IXP suspend/resume ✓ (23 s) → Tool output guardrail check ✓ (no attachment lookup, the extraction result names no file) → Agent output ✓

Version bumped to 0.18.13 (0.18.12 was released from main on 2026-09-21 without this change; branch rebased onto it).

Jira: AL-593 (consumer: AL-596).

🤖 Generated with Claude Code

Copilot AI lite review requested due to automatic review settings September 20, 2026 19:22

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.

Copilot review overview

🟡 Changes recommended

Current-call selection and nested payload scanning have unresolved issues that can produce incorrect guardrail decisions.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 High severity

Open (1)
What changed in this PR

Adds call-scoped attachment discovery for Tool-scope guardrails, including pre- and post-execution validation.

Changes:

  • Resolves referenced attachments from tool arguments and results.
  • Adds bounded scanning, deduplication, and metadata fallback.
  • Adds guardrail and end-to-end tests.
  • Bumps version to 0.18.12.
File Description
uv.lock Synchronizes the locked package version.
tests/​agent/​guardrails/​test_tool_guardrails_subgraph_run.py Adds end-to-end tool guardrail coverage.
tests/​agent/​guardrails/​test_guardrail_nodes.py Tests guardrail attachment forwarding.
tests/​agent/​guardrails/​test_attachment_refs.py Tests attachment discovery behavior.
src/​uipath_langchain/​agent/​guardrails/​guardrail_nodes.py Integrates scope- and stage-aware attachment resolution.
src/​uipath_langchain/​agent/​guardrails/​attachment_refs.py Implements bounded attachment reference collection.
pyproject.toml Bumps the package version.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/uipath_langchain/agent/guardrails/guardrail_nodes.py
@apetraru-uipath
apetraru-uipath force-pushed the feat/guardrail-judge-tool-scope branch 2 times, most recently from e5e77fd to a910bcd Compare September 20, 2026 19:57
apetraru-uipath and others added 2 commits September 22, 2026 00:26
…ol call mentions

Tool-scope guardrails no longer skip attachments. Before the tool runs, the
judge receives references for the attachments named in the call's arguments;
after it runs, for those named in the result. Only ids the run's registry
holds are forwarded, with the registry's name and type, so a call that mentions
no file forwards none, and a mention of a file the run never held is skipped
rather than sent to the backend for lookup. Agent and LLM scope keep reading
the run's registry; both paths build references through one collector.

AL-593

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
0.18.12 shipped from main on 2026-09-21 without this change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@apetraru-uipath
apetraru-uipath force-pushed the feat/guardrail-judge-tool-scope branch from a910bcd to 0c6aeb1 Compare September 21, 2026 21:26
… that name

Review finding on #1103: with two calls to the same tool in one AI message,
the tool-scope guardrail read "the first call named tool_name", so the second
pre-execution evaluation judged (and resolved attachments for) the first
call, and once the first ToolMessage was appended it read nothing at all.
The node now selects the call the way the tool node does
(extract_current_tool_call_index) before execution, and the call the last
ToolMessage answers after execution; payload, deterministic input extraction
and attachment resolution all use that selection.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@apetraru-uipath
apetraru-uipath merged commit d1eb20f into main Sep 22, 2026
48 checks passed
@apetraru-uipath
apetraru-uipath deleted the feat/guardrail-judge-tool-scope branch September 22, 2026 14:19
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