Skip to content

feat(guardrails): attach guardrails to agent tool calls via control plane - #1793

Open
madhumitha-chandrasekaran-1 wants to merge 5 commits into
rocketride-org:developfrom
madhumitha-chandrasekaran-1:feat/RR-1792-guard-tool-control-plane
Open

madhumitha-chandrasekaran-1 wants to merge 5 commits into
rocketride-org:developfrom
madhumitha-chandrasekaran-1:feat/RR-1792-guard-tool-control-plane

Conversation

@madhumitha-chandrasekaran-1

@madhumitha-chandrasekaran-1 madhumitha-chandrasekaran-1 commented Aug 3, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Guardrails could only ever check one place in a pipeline: whatever feeds the final response node, via lane connections (questions/answers/documents). Every tool node (tool_slack, tool_gmail, tool_github, DB-as-tool, tool_filesystem, etc.) and persistent-memory read/write runs entirely on the control plane inside an agent's own turn, so guardrails never saw either — no matter how many tools an agent held, or how the pipeline was wired.

This adds a second attachment mode: a guard node can now attach via control directly to an agent node (classType: "guard"), and every tool call and memory read/write the agent makes gets checked automatically:

  • Before a tool runs / a value is written to memory, on the args/value (mode='output') — the agent-generated content is checked the same way an answer would be (PII, content-safety, hallucination).
  • After a tool returns / a value is read from memory, on the result (mode='input') — checked the same way a question would be (prompt-injection defense against a poisoned webhook/API/DB response, or content another run stored earlier — the standard indirect-prompt-injection-into-tool-use case).

policy_mode: block raises before the call executes or before its result reaches the agent; the framework driver surfaces this as a normal tool/memory error (not a pipeline crash). warn/log behave the same as on the existing lanes — log stays silent, matching writeQuestions/writeAnswers. A guard node that returns anything other than a recognized pass/warn/log/block action (missing, malformed, wrong type) is treated as a block — fails closed instead of silently disabling the check. One guard node covers every tool and memory channel the agent holds — no per-tool wiring needed. Memory.clear is left unguarded since deletion carries no content to evaluate.

Closes the general case in #1792 for any lane-connected agent holding tools/memory directly (covers all framework drivers — LangChain, deepagents, llamaindex, plain CrewAI — since they all route through the same AgentBase.call_tool choke point). The CrewAI hierarchical-subagent case (agent_crewai_subagent, itself control-plane-only) needs a follow-up fix to the pipeline-graph visited-propagation walk in pipeline_config.cpp and is intentionally out of scope here.

Changes

  • rocketlib/types.py, rocketlib/__init__.py — new IInvokeGuard.Check op, following the existing IInvokeTool/IInvokeMemory pattern.
  • nodes/src/nodes/guardrails/IInstance.py — new check() control-plane handler. Reports the verdict on param.result rather than enforcing it directly, since only the caller knows whether this is a pre- or post-call check. Stays silent for log mode, matching writeQuestions/writeAnswers.
  • packages/ai/src/ai/common/agent/_internal/host.py — both AgentHostServices.Tools and AgentHostServices.Memory discover attached guard nodes (getControllerNodeIds('guard')) and run them around every invoke()/put/get/list. Shared block/warn/log + fail-closed logic lives in one module-level _check_guard() rather than duplicated per channel. Validates the guard's result is actually a mapping before reading it, so a malformed result (list/string/None) fails closed instead of crashing with AttributeError.
  • examples/slack-agent.pipe — the pipeline that originally surfaced this gap, now with a guard wired to the agent.
  • nodes/src/nodes/guardrails/README.md — new section documenting the control-plane attachment mode, for both tools and memory.
  • Tests: fixed the existing guardrails stub harness (needed invoke_function stubbed), added coverage for check() (pass/block/log-silent/warn-logs/no-engine/empty-text), and a new test_host_guard.py covering tool block-on-args, block-on-result-after-ran, pass-through, no-guard-attached, malformed/non-mapping guard results, and the equivalent memory put/get/list/clear cases.

Test plan

  • pytest nodes/test/guardrails/test_all.py packages/ai/tests/ai/common/agent/_internal/test_host_guard.py packages/ai/tests/ai/common/test_config_shapes.py --noconftest — 133 passed (stub-based, no compiled engine required; --noconftest matches the documented pattern already used by test_config_shapes.py for engine-free files)
  • ruff check / ruff format --check clean on all touched files
  • Not yet run against the compiled engine (./builder build server / ./builder test) — needs Xcode Command Line Tools on the dev machine this was built on; the C++ side (pipeline_config.cpp control validation, getControllerNodeIds) is unmodified and reuses the exact dispatch path the existing tool/llm/memory control edges already use in production.

Closes #1792

Summary by CodeRabbit

  • New Features
    • Added guardrail checks for agent tool calls and memory reads, writes, and listings.
    • Supports content safety and PII detection with blocking, warning, and logging modes.
    • Blocked or invalid guardrail responses prevent protected operations from proceeding.
    • Added an example showing how to connect guardrails to an agent.
  • Documentation
    • Added guidance for configuring guardrails, execution checks, failure handling, and current limitations.

…lane

Lane connections only ever see what feeds a pipeline's final response, so
guardrails could never check what an agent does with a tool (Slack, Gmail,
GitHub, DB-as-tool, etc.) since tool calls happen entirely inside the
agent's own turn. A guard node can now attach via `control` to an agent
node (classType: "guard"), and every tool call gets checked before it
runs (args) and after it returns (result) using the same policy_mode
semantics as the questions/answers lanes.

Closes the general case from rocketride-org#1792 for any lane-connected agent holding
tools directly; the CrewAI hierarchical-subagent case is a narrower
follow-up.
@github-actions github-actions Bot added docs Documentation module:server C++ engine and server components module:nodes Python pipeline nodes module:ai AI/ML modules labels Aug 3, 2026
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor
🤖 Internal: Discord sync marker

Auto-managed by the Discord notification workflow. Stores the linked Discord message ID and forum thread ID. Do not edit or delete.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds a public control-plane guard check contract. Agent tools and memory operations evaluate connected guardrails before crossing the control-plane boundary. The example and documentation show the new wiring.

Changes

Guardrails control plane

Layer / File(s) Summary
Guard invocation contract and evaluation
packages/server/engine-lib/rocketlib-python/lib/rocketlib/types.py, packages/server/engine-lib/rocketlib-python/lib/rocketlib/__init__.py, nodes/src/nodes/guardrails/IInstance.py, nodes/test/guardrails/test_all.py
IInvokeGuard.Check carries the guard mode, text, and result. IInstance.check() evaluates available text and returns the parameter. Tests cover pass, block, bypass, and warning behavior.
Tool and memory guard enforcement
packages/ai/src/ai/common/agent/_internal/host.py
Tools and Memory discover connected guards. They check tool arguments, tool results, memory writes, and memory reads or lists. Invalid guard responses fail closed. Memory.clear() remains unchecked.
Guard enforcement validation
packages/ai/tests/ai/common/agent/_internal/test_host_guard.py
Tests cover guarded and unguarded tools, warning results, blocked operations, malformed responses, memory operations, and unguarded clearing.
Guardrail wiring and documentation
examples/slack-agent.pipe, nodes/src/nodes/guardrails/README.md
The example adds a strict blocking guardrail with content-safety and PII checks. Documentation describes control-plane wiring, policy modes, memory checks, fail-closed behavior, and the control-only sub-agent limitation.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Agent
  participant Tools
  participant Guardrails
  participant Tool
  Agent->>Tools: Request tool invocation
  Tools->>Guardrails: Check tool arguments
  Guardrails-->>Tools: Return pass or block verdict
  Tools->>Tool: Invoke when arguments pass
  Tool-->>Tools: Return tool output
  Tools->>Guardrails: Check tool output
  Guardrails-->>Tools: Return pass or block verdict
  Tools-->>Agent: Return output or raise ValueError
Loading

Possibly related PRs

Suggested reviewers: jmaionchi, rod-christensen, stepmikhaylov

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The implementation adds control-plane checks for tool arguments and results, supports shared guards, and preserves block, warn, and log behavior [#1792].
Out of Scope Changes check ✅ Passed The changes remain within the guardrail attachment objective and include related memory coverage, documentation, exports, and tests.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: attaching guardrails to agent tool calls through the control plane.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@nodes/src/nodes/guardrails/IInstance.py`:
- Around line 176-183: Update the violation loop in check() to call warning()
only when result['action'] is 'block' or 'warn', matching writeQuestions() and
writeAnswers(). Preserve result assignment and return behavior, and keep action
'log' silent.

In `@packages/ai/src/ai/common/agent/_internal/host.py`:
- Around line 177-183: Update the guard evaluation in _run_guard so a missing,
malformed, or unrecognized param.result/action fails closed instead of allowing
the tool call to proceed. Validate the result after invoking each guard node,
and preserve the existing violation reporting for an explicit 'block' action
while treating any action other than an accepted pass value as a guard failure.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 36732e5c-7dc1-473b-a069-346c75defa78

📥 Commits

Reviewing files that changed from the base of the PR and between 685508d and 040371e.

📒 Files selected for processing (9)
  • examples/slack-agent.pipe
  • nodes/src/nodes/guardrails/IInstance.py
  • nodes/src/nodes/guardrails/README.md
  • nodes/test/guardrails/test_all.py
  • packages/ai/src/ai/common/agent/_internal/host.py
  • packages/ai/tests/ai/common/agent/_internal/__init__.py
  • packages/ai/tests/ai/common/agent/_internal/test_host_guard.py
  • packages/server/engine-lib/rocketlib-python/lib/rocketlib/__init__.py
  • packages/server/engine-lib/rocketlib-python/lib/rocketlib/types.py

Comment thread nodes/src/nodes/guardrails/IInstance.py
Comment thread packages/ai/src/ai/common/agent/_internal/host.py Outdated
…ard result

Address coderabbit review on rocketride-org#1793:
- check() was warning() on every violation regardless of policy_mode,
  logging even in `log` mode, unlike writeQuestions/writeAnswers and
  contrary to the documented behavior.
- _run_guard() treated a missing/malformed `action` from a guard node as
  an implicit pass. Since this is a security control, a broken or
  misconfigured guard should fail closed instead of silently disabling
  enforcement.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/ai/src/ai/common/agent/_internal/host.py`:
- Around line 184-192: Update the guard-result handling around the action
validation to first require that result is a mapping, raising ValueError for any
other type, and validate that violations is an iterable of mappings with usable
rule values before constructing the blocked-tool message. Ensure all malformed
results, including non-mapping param.result values, consistently raise
ValueError rather than AttributeError or iteration errors, and add coverage for
a non-mapping result.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4b543f1e-419e-48e9-8877-3b3253812f0e

📥 Commits

Reviewing files that changed from the base of the PR and between 040371e and 07c940f.

📒 Files selected for processing (4)
  • nodes/src/nodes/guardrails/IInstance.py
  • nodes/test/guardrails/test_all.py
  • packages/ai/src/ai/common/agent/_internal/host.py
  • packages/ai/tests/ai/common/agent/_internal/test_host_guard.py

Comment thread packages/ai/src/ai/common/agent/_internal/host.py Outdated
Address follow-up coderabbit review on rocketride-org#1793: getattr(param, 'result', None)
or {} only replaces falsy non-dicts, so a non-empty list or string from a
malformed guard node passed through and crashed result.get() with
AttributeError instead of the intended fail-closed ValueError. Now
validates result is a dict (falls through to the "no recognized action"
branch otherwise) and defends the violations-formatting path against a
non-list violations value or non-dict violation items.
Memory.put/get/list bypassed the guard entirely -- a separate host channel
from Tools, discovered and invoked independently, so an agent could write
PII/injected content into persistent memory or read back poisoned stored
content with a guard fully configured and attached, and it would never see
either. Memory is architecturally the same kind of control-plane side
effect this PR already covers for tool calls (the issue itself calls out
db-as-tool writes as in-scope), so it gets the same treatment:

- Memory.put is guarded pre-write (mode='output') on the value being stored.
- Memory.get/list are guarded post-read (mode='input') on the value/keys
  read back -- the indirect-prompt-injection case, but via memory another
  run wrote instead of a tool response.
- Memory.clear is left unguarded -- deletion carries no content to check.

Extracted the shared block/warn/log + fail-closed logic out of
Tools._run_guard into a module-level _check_guard() used by both Tools and
Memory, rather than duplicating it.
…e lookup

/simplify pass on PR rocketride-org#1793:
- _guard_text() reimplemented what ai.common.utils.safe_str already does
  (explicitly documented as "the single home of safe_str... no duplicate
  copy anywhere else"). Drop it and call safe_str() directly -- also fixes
  a latent bug where the except-fallback's own str(value) call wasn't
  itself guarded.
- Tools and Memory each called getControllerNodeIds('guard') independently
  even though both are constructed together from AgentHostServices.__init__.
  Resolve guard_nodes once there and pass it to both constructors.
@madhumitha-chandrasekaran-1

madhumitha-chandrasekaran-1 commented Aug 3, 2026 •

Copy link
Copy Markdown
Contributor Author

@coderabbitai resume

@coderabbitai

coderabbitai Bot commented Aug 3, 2026 •

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Reviews resumed.

@nihalnihalani

Copy link
Copy Markdown
Collaborator

The conflict here comes from your own merged #1861. guardrails/IInstance.py on develop is now 178 lines with _forward_question / _forward_answer helpers, and nodes/test/guardrails/test_all.py was rewritten in the same change — this branch's hunks are against the pre-#1861 file (the check() addition appends at @@ -152,3).

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

docs Documentation module:ai AI/ML modules module:nodes Python pipeline nodes module:server C++ engine and server components

Projects

None yet

Development

Successfully merging this pull request may close these issues.

guardrails' protection surface is capped at lane-connected nodes — coverage shrinks with every new tool node added

3 participants