Skip to content

fix(flows): gate live-run tool on flows_build builder path (#4593) - #4606

Merged
senamakel merged 1 commit into
tinyhumansai:mainfrom
CodeGhost21:fix/4593-flows-build-approval-gate
Jul 6, 2026
Merged

senamakel merged 1 commit into
tinyhumansai:mainfrom
CodeGhost21:fix/4593-flows-build-approval-gate

Conversation

@CodeGhost21

@CodeGhost21 CodeGhost21 commented Jul 6, 2026 •

Copy link
Copy Markdown
Member

Summary

  • flows_build ran the workflow_builder turn under AgentTurnOrigin::Cli, whose ApprovalGate branch auto-allows every external-effect tool — so a stray run_flow call during an authoring turn could execute a live saved flow (real Slack/Gmail/HTTP/code) with no HITL confirmation (issue flows_build builder turn can run a live flow without approval gating #4593).
  • Adds a reusable Agent::hide_tools(&[&str]) that drops names from the session's visible belt and rebuilds the ToolPolicySession so they resolve to Deny at the tool-call boundary (a hard execution guarantee, not just prompt-hiding).
  • flows_build now strips the live-run tool from the builder belt on this direct RPC path; authoring/read tools are untouched, so the turn still runs headless without fail-closing.
  • run_flow stays available and properly gated (real WebChat approval card) when workflow_builder runs as the build_workflow chat delegate — only the direct flows_build path is restricted.

Problem

flows_build deliberately uses AgentTurnOrigin::Cli so the approval gate does not fail-closed in a headless/streamed run. But ApprovalGate::intercept_audited's Cli arm returns Allow for every external_effect tool. The workflow_builder belt includes run_flow (flows::tools::RunFlowTool, permission_level = Write, external_effect = true), whose execute() calls flows_run(...) — a real run. Invoked from the copilot/prompt-bar with a saved flow_id, a mistaken run_flow fires the flow with no gate. The only remaining safety was the flow's own require_approval (only pauses outbound nodes if set) plus a soft "ask first" prompt rule — neither a real guarantee. This contradicts flows_build's contract: "This op never enables or runs a flow." Flagged P1 by chatgpt-codex-connector on #4578.

Note: the run_workflow → run_flow rename referenced in the issue thread has already landed on main; the belt lists run_flow today. The duplicate-tool-name collision (harness run_workflow spawn tool vs. flows runner) is therefore already resolved and is out of scope here.

Solution

  • Agent::hide_tools (agent/harness/session/runtime.rs): removes names from visible_tool_names and calls rebuild_tool_policy_session(), so hidden names become Deny in the enforced ToolPolicySession (consumed by ToolPolicyEnforcement on the tinyagents turn path). If the belt was unfiltered (empty = "all visible"), it first seeds from every spec so the hide actually restricts.
  • flows_build (flows/ops.rs): restrict_builder_toolset(&mut agent) hides FLOWS_BUILD_HIDDEN_TOOLS = ["run_workflow", "run_flow"] right after the agent is built. run_flow is the live-runner today; run_workflow is listed belt-and-braces (no-ops if absent).
  • Why not change the origin? There is no routable approval surface on this path yet (the copilot stream carries only a broadcast thread_id, no per-user client_id; the dedicated flow-review surface "B3" isn't built), so a gated origin would park-then-TTL-deny (a 10-min hang). Making the tool unreachable matches the "never runs a flow" contract and keeps the gated chat-delegate path unchanged.

Submission Checklist

If a section does not apply to this change, mark the item as N/A with a one-line reason. Do not delete items.

  • Tests added or updated (happy path + at least one failure / edge case) per Testing Strategy
  • Diff coverage ≥ 80% — changed lines meet the gate. New logic is exercised by flows_build_hides_the_live_run_tool_from_the_builder_belt and both hide_tools_* unit tests.
  • Coverage matrix updated — N/A: behaviour-hardening change to an existing feature (no new feature row)
  • All affected feature IDs from the matrix are listed under ## Related — N/A: no matrix feature IDs affected
  • No new external network dependencies introduced (mock backend used per Testing Strategy)
  • Manual smoke checklist updated if this touches release-cut surfaces — N/A: internal approval-gating hardening, no release-cut surface change
  • Linked issue closed via Closes #NNN in the ## Related section

Impact

  • Security (desktop/CLI): closes an approval-gate bypass where the flows copilot builder could run a live saved flow (outbound Slack/Gmail/HTTP/code) with no human-in-the-loop confirmation.
  • No API/schema/migration changes. Behaviour change is scoped to the flows_build RPC path; the build_workflow chat-delegate path (properly gated) is unchanged.

Related


AI Authored PR Metadata (required for Codex/Linear PRs)

Keep this section for AI-authored PRs. For human-only PRs, mark each field N/A.

Linear Issue

Commit & Branch

  • Branch: fix/4593-flows-build-approval-gate
  • Commit SHA: b7648c7

Validation Run

  • pnpm --filter openhuman-app format:check — N/A: no frontend files changed
  • pnpm typecheck — N/A: no TypeScript changed
  • Focused tests: cargo test --lib flows_build_hides_the_live_run_tool_from_the_builder_belt, hide_tools_* — all pass
  • Rust fmt/check (if changed): cargo fmt --check clean; cargo check clean
  • Tauri fmt/check (if changed): N/A: no app/src-tauri changes

Validation Blocked

  • command: pnpm compile / pre-push hook (frontend steps)
  • error: tsc fails in app/src/components/flows/canvas/* and app/src/pages/conversations/* with Cannot find module '@xyflow/react' | 'rehype-highlight' | 'remark-gfm' — these deps are declared in app/package.json but were not installed in the local worktree's stale node_modules. Pre-existing, in files this PR does not touch.
  • impact: None on this change. This is a Rust-only PR; cargo fmt --check, cargo check (core + Tauri via pnpm rust:check), and all focused Rust tests pass. The pre-push hook was bypassed with --no-verify for this unrelated pre-existing frontend/tooling breakage per repo policy; CI (which installs fresh deps) validates the frontend.

Behavior Changes

  • Intended behavior change: the direct flows_build builder turn can no longer call the live-run flow tool.
  • User-visible effect: none in normal use (authoring/proposal unchanged); a copilot builder turn can no longer execute a saved flow without going through a gated surface.

Parity Contract

  • Legacy behavior preserved: run_flow stays available and gated on the build_workflow chat-delegate path (WebChat origin → approval card).
  • Guard/fallback/dispatch parity checks: hidden names enforced as Deny via ToolPolicySession; authoring tools (external_effect == false) unaffected, so the turn never fail-closes.

Duplicate / Superseded PR Handling

  • Duplicate PR(s): none
  • Canonical PR: this PR
  • Resolution: N/A

Summary by CodeRabbit

  • New Features

    • Added the ability to hide selected tools from an agent’s visible tool set during a session.
    • Direct flow-building now hides live run actions, keeping authoring tools available while preventing accidental execution.
  • Bug Fixes

    • Fixed cases where hiding tools had no effect when all tools were initially visible.
    • Added coverage to ensure hidden tools stay unavailable without affecting other allowed tools.

…ild path (tinyhumansai#4593)

The flows_build RPC runs the workflow_builder turn under
AgentTurnOrigin::Cli so the approval gate does not fail-closed in a
headless/streamed run. That same origin makes ApprovalGate auto-allow
every external_effect tool, so a stray run_flow call during an authoring
turn could execute a live saved flow (real Slack/Gmail/HTTP/code) with
no HITL confirmation.

Strip the live-run tool from the builder's callable belt on this direct
RPC path via a new Agent::hide_tools helper, which drops the names from
the (already narrow) visible belt and rebuilds the ToolPolicySession so
they resolve to Deny at the tool-call boundary — a hard execution
guarantee, not just prompt-hiding. Authoring/read tools are untouched so
the turn still runs headless without fail-closing. run_flow stays
available and properly gated (WebChat approval card) when
workflow_builder is invoked as the build_workflow chat delegate.

Covers both run_flow (current name) and legacy run_workflow for
robustness. Adds Rust regression coverage on the flows_build path plus
unit tests for both hide_tools branches.
@CodeGhost21
CodeGhost21 requested a review from a team July 6, 2026 21:19
@coderabbitai

coderabbitai Bot commented Jul 6, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a new Agent::hide_tools method to session runtime that removes named tools from an agent's visible-tool filter, seeding it from all tool specs if previously unset. Applies this to flows_build to hide live flow-run tools (run_workflow, run_flow) from the workflow_builder agent, with accompanying unit and regression tests.

Changes

Tool hiding feature

Layer / File(s) Summary
Agent.hide_tools API and tests
src/openhuman/agent/harness/session/runtime.rs, src/openhuman/agent/harness/session/tests.rs
Adds hide_tools(&mut self, names: &[&str]) which seeds visible_tool_names from all tool specs when empty, removes named tools, and rebuilds the tool policy session; tests cover both a pre-filtered and an unfiltered starting state.
flows_build builder toolset restriction
src/openhuman/flows/ops.rs, src/openhuman/flows/ops_tests.rs
Adds FLOWS_BUILD_HIDDEN_TOOLS and restrict_builder_toolset helper calling Agent::hide_tools, wires it into flows_build before the agent runs, and adds a regression test confirming run_workflow/run_flow are hidden while authoring tools remain visible.

Estimated code review effort: 2 (Simple) | ~15 minutes

Sequence Diagram(s)

sequenceDiagram
  participant flows_build
  participant workflow_builder as Agent(workflow_builder)
  participant restrict_builder_toolset

  flows_build->>workflow_builder: construct agent
  flows_build->>restrict_builder_toolset: restrict_builder_toolset(agent)
  restrict_builder_toolset->>workflow_builder: hide_tools([run_workflow, run_flow])
  workflow_builder->>workflow_builder: seed visible_tool_names if empty
  workflow_builder->>workflow_builder: remove hidden names, rebuild tool policy session
  flows_build->>workflow_builder: run agent (live-run tools denied)
Loading

Possibly related issues

Suggested labels: agent, bug

Suggested reviewers: M3gA-Mind

Poem

A rabbit hides carrots so none run astray,
No live flow shall launch until we say!
hide_tools() sweeps the belt so clean,
run_flow and run_workflow, nowhere seen. 🐰
Tests hop through each little case—
Safe authoring now has its place. 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: gating the live-run tool on the flows_build builder path.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

@coderabbitai coderabbitai Bot added agent Built-in agents, prompts, orchestration, and agent runtime in src/openhuman/agent/. bug labels Jul 6, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b7648c7576

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

.collect();
}
for name in names {
self.visible_tool_names.remove(*name);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve an explicit empty allowlist when hiding tools

If names covers the whole current belt — for example a custom workflow_builder override or any single-tool caller where only run_flow is visible — this removal leaves visible_tool_names empty. Elsewhere an empty set is the “no filter / all tools visible” sentinel (run_chat_turn_graph maps it to None, and ToolPolicyEngine only hides when the set is non-empty), so the rebuild immediately re-exposes the full registry instead of denying the hidden tools. That turns a safety restriction into a fail-open path; keep an explicit empty allowlist/sentinel instead of collapsing to the empty-set sentinel.

Useful? React with 👍 / 👎.

@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

🧹 Nitpick comments (1)
src/openhuman/agent/harness/session/tests.rs (1)

1259-1311: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Tests only assert on visible_tool_names_for_test, not the actual policy-enforcement outcome.

The doc comment for hide_tools promises hidden names resolve to Deny at the tool-call boundary (via the rebuilt [ToolPolicySession]), not merely absent from the prompt — a hard execution guarantee even if the model requests the tool anyway. Both new tests here stop at inspecting the intermediate visible_tool_names set and never assert that tool_policy_session (or an equivalent decision API) actually denies a call to the hidden tool. Consider adding one assertion per test that exercises the policy session directly, so the "hard execution guarantee" claim is what's actually pinned by the test, not just the input to it.

🤖 Prompt for 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.

In `@src/openhuman/agent/harness/session/tests.rs` around lines 1259 - 1311, The
new hide_tools tests only verify the visible tool allowlist and do not prove the
rebuilt ToolPolicySession enforces the promised deny-at-call boundary. Update
hide_tools_drops_named_from_existing_filter and
hide_tools_seeds_allowlist_when_no_filter_present to also exercise the actual
policy decision path on the agent’s ToolPolicySession (or equivalent decision
API) and assert the hidden tool is denied. Keep the existing
visible_tool_names_for_test checks, but add one policy-level assertion in each
test so the hard execution guarantee is covered, not just the intermediate
filter state.
🤖 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 `@src/openhuman/agent/harness/session/runtime.rs`:
- Around line 256-268: `hide_tools` in `SessionRuntime` can turn an empty
`visible_tool_names` back into the “all visible” sentinel, which silently undoes
hiding when the last visible tool is removed. Update `hide_tools` so it
preserves the intended “none visible” state after removals, instead of relying
on an empty `HashSet` meaning both “unset” and “fully hidden”; add an explicit
guard or separate state handling before calling `rebuild_tool_policy_session()`.
Refer to `hide_tools`, `visible_tool_names`, and `rebuild_tool_policy_session`
when implementing the fix.

---

Nitpick comments:
In `@src/openhuman/agent/harness/session/tests.rs`:
- Around line 1259-1311: The new hide_tools tests only verify the visible tool
allowlist and do not prove the rebuilt ToolPolicySession enforces the promised
deny-at-call boundary. Update hide_tools_drops_named_from_existing_filter and
hide_tools_seeds_allowlist_when_no_filter_present to also exercise the actual
policy decision path on the agent’s ToolPolicySession (or equivalent decision
API) and assert the hidden tool is denied. Keep the existing
visible_tool_names_for_test checks, but add one policy-level assertion in each
test so the hard execution guarantee is covered, not just the intermediate
filter state.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e1745f83-acc6-4e9a-b9cc-08efd5f9bbad

📥 Commits

Reviewing files that changed from the base of the PR and between 9b84f96 and b7648c7.

📒 Files selected for processing (4)
  • src/openhuman/agent/harness/session/runtime.rs
  • src/openhuman/agent/harness/session/tests.rs
  • src/openhuman/flows/ops.rs
  • src/openhuman/flows/ops_tests.rs

Comment on lines +256 to +268
pub fn hide_tools(&mut self, names: &[&str]) {
if self.visible_tool_names.is_empty() {
self.visible_tool_names = self
.tool_specs
.iter()
.map(|spec| spec.name.clone())
.collect();
}
for name in names {
self.visible_tool_names.remove(*name);
}
self.rebuild_tool_policy_session();
}

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

hide_tools can silently re-expose everything if a caller ever hides the full belt.

Because empty HashSet is overloaded as the "all visible" sentinel, if names ever covers every tool currently in visible_tool_names (e.g. seeded set had only a handful of entries and all get removed), the resulting empty set flips meaning back to "all visible" instead of "nothing visible" — silently un-hiding tools instead of hiding them. This is explicitly called out as a caveat in the doc comment but nothing guards against it. Today's only caller (flows_build's restrict_builder_toolset) hides 2 of a larger belt so it's safe in practice, but the API itself is a footgun for any future caller (or a future edit to FLOWS_BUILD_HIDDEN_TOOLS) that could silently defeat the exact protection this PR adds.

🛡️ Suggested guard
     pub fn hide_tools(&mut self, names: &[&str]) {
         if self.visible_tool_names.is_empty() {
             self.visible_tool_names = self
                 .tool_specs
                 .iter()
                 .map(|spec| spec.name.clone())
                 .collect();
         }
         for name in names {
             self.visible_tool_names.remove(*name);
         }
+        if self.visible_tool_names.is_empty() && !self.tool_specs.is_empty() {
+            log::warn!(
+                "[agent] hide_tools cleared the entire visible-tool set for {} — \
+                 falling back to the 'all visible' sentinel, which defeats the hide",
+                self.agent_definition_name
+            );
+        }
         self.rebuild_tool_policy_session();
     }
🤖 Prompt for 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.

In `@src/openhuman/agent/harness/session/runtime.rs` around lines 256 - 268,
`hide_tools` in `SessionRuntime` can turn an empty `visible_tool_names` back
into the “all visible” sentinel, which silently undoes hiding when the last
visible tool is removed. Update `hide_tools` so it preserves the intended “none
visible” state after removals, instead of relying on an empty `HashSet` meaning
both “unset” and “fully hidden”; add an explicit guard or separate state
handling before calling `rebuild_tool_policy_session()`. Refer to `hide_tools`,
`visible_tool_names`, and `rebuild_tool_policy_session` when implementing the
fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent Built-in agents, prompts, orchestration, and agent runtime in src/openhuman/agent/. bug

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

flows_build builder turn can run a live flow without approval gating

2 participants