Repository navigation
Conversation
added 16 commits
October 23, 2026 23:16
Add TransformTarget.TOOL_RESULT, whose text replaces the whole content of the tool message under check, and map the tool_input direction to it in the compile gate. Any other target on a tool-result surface, and TOOL_RESULT on any other direction, stays refused. Signed-off-by: Justin Brooks <jbrooks@hiddenlayer.com>
Accept the tool_call, tool_result and tool_definition keywords a per-tool rail is executed with, record the calls and results each stub was handed, map the tool_input direction to TOOL_RESULT in the rewriting helpers, and add a tool_result_rewrite verdict helper. Signed-off-by: Justin Brooks <jbrooks@hiddenlayer.com>
Add RailResult.messages, the conversation as tool rails rewrote it, which takes part in equality so a lost rewrite fails a comparison, and messages_before_block, its capture-only counterpart on a block that follows a rewrite, excluded from equality like rewrite_before_block. Signed-off-by: Justin Brooks <jbrooks@hiddenlayer.com>
Add ModelEngine.rewrite_tool_results and its EngineRegistry delegator, the inverse of tool-exchange extraction: each key is the message_index extraction assigned, the tool message there takes the rewritten text as its content, and the caller's list and messages are copied rather than edited. A key that is not a tool message raises ValueError. Signed-off-by: Justin Brooks <jbrooks@hiddenlayer.com>
Per-tool rails on tool results may now return a TOOL_RESULT transform. Rewriting flows run first for each tool, each later check is handed the result as the checks ahead of it left it, and a rewrite a later rail undid counts as none. The rewritten contents are written back through the engine registry: an allow carries the conversation as RailResult.messages, and a block that follows a rewrite keeps it as capture-only messages_before_block. A rewrite that cannot be written back blocks, a tool-call rail's rewrite still raises, and parallel rails and the global validators keep refusing rewrites. Signed-off-by: Justin Brooks <jbrooks@hiddenlayer.com>
When the tool-result rails rewrite a result, generate and stream rebind the conversation before the input rails run, so the input and output rails, the main model and the generation record all read the rewrite. A tool-result block that follows a rewrite records the rewritten conversation on the request span, as an input block behind a mask already does. Signed-off-by: Justin Brooks <jbrooks@hiddenlayer.com>
A check whose tool-result rails rewrote a result threads the rewritten conversation into the input, tool-call and output rails behind them and onto the request span, and returns MODIFIED with RailsResult.messages: the checked conversation as the rails left it, index-aligned with the messages passed in. /v1/checks returns the same messages with the prepended context message stripped. Both stay unset unless a tool rail rewrote something. Signed-off-by: Justin Brooks <jbrooks@hiddenlayer.com>
Document TransformTarget.TOOL_RESULT, the transform_target pairings IORails accepts, how a per-tool rail's tool-result rewrite is applied (ordering, history recomputation, text-part collapse, the sequential fallback, capture on a block), and the MODIFIED status and messages field a check returns for it. Signed-off-by: Justin Brooks <jbrooks@hiddenlayer.com>
The request span recorded the tool results a conversation carries as the caller sent them whenever a per-tool masking rail never decided on them: the mask raised, the generation options turned the tool rails off, a check's rail_types left tool_result out, or the manager blocked before the per-tool rails ran. The withholding machinery only knew the input and output directions, so nothing seeded or cleared a pending tool mask. are_tool_results_safe now reports a configured mask that did not decide, IORails seeds the tool-result direction from per_tool_transform_flows and clears it from the rails' verdict, and a response to a tool result no mask decided on is withheld too, since the model may repeat it. Signed-off-by: Justin Brooks <jbrooks@hiddenlayer.com>
…once A stream's request span records the conversation when the stream ends, which a consumer can bring about at any point. The tool-result rewrite was held in a local until the input rails returned, so a stream closed while those rails waited on their model call recorded the tool result as the caller sent it, undoing the mask. The block branch alongside it already published the rewrite for this reason. Signed-off-by: Justin Brooks <jbrooks@hiddenlayer.com>
Add TransformTarget.TOOL_CALL, whose text is the JSON object of the complete arguments of the tool call under check, and map the tool_output direction to it in the compile gate. Any other target on a tool-call surface, and TOOL_CALL on any other direction, stays refused. Signed-off-by: Justin Brooks <jbrooks@hiddenlayer.com>
…ersation Add ModelEngine.rewrite_latest_tool_calls and its EngineRegistry delegator, the inverse of extract_latest_tool_calls. Each rewritten call on the last assistant message gets the new arguments as the JSON string the wire carries; only the list, that message, its tool_calls and the rewritten calls are copied, and a position with no call raises ValueError. Signed-off-by: Justin Brooks <jbrooks@hiddenlayer.com>
Per-tool rails on tool calls may now return a TOOL_CALL transform whose text is the JSON object of the complete arguments. Rewriting flows run first for each tool and each later check is handed the call as the checks ahead of it left it. Only function.arguments is replaced, so id, type and name cannot change; text that is not a JSON object blocks with malformed_arguments. Because rails ahead of a rewrite judged the original arguments, the final arguments are validated against the tool again and a failure blocks with the validator's own violation, or tool_not_allowed when the tool is undeclared. An allow carries the final calls as RailResult.tool_calls; a block behind a rewrite keeps them as capture-only tool_calls_before_block. are_latest_tool_calls_safe writes the rewritten arguments back onto the last assistant message and carries the conversation as messages, or messages_before_block on a block. A rewrite a later rail undid counts as none, compared as serialized JSON so 1 and true differ. Signed-off-by: Justin Brooks <jbrooks@hiddenlayer.com>
…check Generate swaps the rewritten calls into the model response, so the bare assistant message (JSON-string arguments) and GenerationResponse (dict arguments) both carry them. Stream sends them on the terminal tool-call chunk through a new local, which content capture then records. A check that runs tool-call rails rebinds the conversation for the output rails and the request span, reports MODIFIED with messages when a call was rewritten, and records the masked calls when a later rail blocks. Signed-off-by: Justin Brooks <jbrooks@hiddenlayer.com>
Document TransformTarget.TOOL_CALL and the tool_output pairing IORails accepts, how a per-tool rail's tool-call rewrite is applied (arguments only, the JSON-object rule, ordering, schema re-validation, both generate return shapes and the terminal stream chunk, capture), and the MODIFIED status and messages a check returns for it. Signed-off-by: Justin Brooks <jbrooks@hiddenlayer.com>
A per-tool masking rail that never decided on a tool call still let the raw arguments reach the request span: a stream whose generation options turned the tool-call rails off recorded the terminal tool-call chunk as delivered, and a check recorded the conversation whenever its rail_types left tool_call out or the mask failed. are_tool_calls_safe and are_latest_tool_calls_safe now report a configured mask that did not decide, a check seeds the tool-call direction from the latest assistant turn's calls and clears it from the rails' verdict, and a stream withholds its output when the calls it delivers carry arguments no mask decided on. Signed-off-by: Justin Brooks <jbrooks@hiddenlayer.com>
6 of 11 tasks
Nurozen
commented
Oct 24, 2026
Author
There was a problem hiding this comment.
Self-review pass turned up one bug, now fixed in 9f42179.
A whole-message tool-call extraction failure, such as a legacy function_call beside tool_calls, returned _blocked_before_rails(...) with the default rewrite_pending=False, so the block reported every configured call mask as decided and the request span recorded the raw arguments. Every other early return in that file already computes a masks_unrun flag; this one did not. masks_unrun is now hoisted above the try and passed to both extraction-failure branches.
A whole-message tool call extraction failure, such as a legacy function_call beside tool_calls, blocked without reporting the configured call masks as still owed a decision. The request span then recorded the raw tool call arguments the mask never saw. Report the masks as pending on both whole-message extraction failure paths, as the per-call parse failure already does. Signed-off-by: Justin Brooks <jbrooks@hiddenlayer.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Per-tool rails can only allow or block, so a rail whose policy is a mask must drop the whole turn.
This adds
tool_callandtool_resulttransform targets reusingRailOutcome.transform, fillingin the
NotImplementedErrorguard left by #2393. Rewriting rails run before judging rails, andchanged arguments are schema-validated. No library rail emits a tool rewrite — the in-tree users are
the engine and
tests/guardrails/rail_stubs.py. Design and open questions are in the linked issue.Related Issue(s)
Verification
make test: 10950 passed, 220 skipped. No live path or docs build was run. Rebase note:developnow reads a check with
model_type=None, so the write-back falls back to the module-levelrewriters, not the engine registry.
AI Assistance
Checklist