From c5940bc90d3c8476da9ca1de8ff5a3e876f54d85 Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Wed, 15 Jul 2026 17:54:43 +0530 Subject: [PATCH 01/10] build(cargo): define the default-ON `flows` feature (#4797) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gates the flows automation domains at compile time, composing with the runtime DomainSet::flows axis from #4796. Default-ON, so the desktop build is unchanged. Makes `tinyflows` optional and moves `tinyagents`'s `repl` feature behind the gate: `repl` is the only thing pulling `rhai`, and it is consumed solely by `rhai_workflows`. A slim build therefore sheds tinyflows + jaq-core/jaq-std/jaq-json + rhai. `tinyagents` itself cannot be shed — 26+ domains consume it. --- Cargo.toml | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2b6bff7f03..c69c54b75b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,7 +61,10 @@ tinyplace = "2.0" # deterministic in-memory capability bundle the flows `dry_run_workflow` agent # tool (Phase 5b) runs a *draft* graph against so the workflow-builder agent can # self-verify a proposal without any real side effects (no real LLM/tool/HTTP/code). -tinyflows = { version = "0.5", features = ["mock"] } +# +# Optional: exclusive to the default-ON `flows` feature (#4797). A slim build +# without `flows` drops this crate and its `jaq-*` JSON-query stack entirely. +tinyflows = { version = "0.5", features = ["mock"], optional = true } # TinyJuice — host-agnostic TokenJuice compression engine. OpenHuman keeps # config/RPC/tool/runtime adapters in `src/openhuman/tokenjuice/` and patches # this dependency to the vendored submodule below. @@ -77,9 +80,12 @@ tinyjuice = { version = "0.2.1", default-features = false } # aligned to 0.40, avoiding duplicate `links = "sqlite3"` native bindings. # Durable graph checkpoints still use `SqlRunLedgerCheckpointer` until the # migration re-points those rows to the crate checkpointer. -# The `repl` feature adds the embedded Rhai `.ragsh` session runtime powering -# the `rhai_workflows` language-workflow tool (`src/openhuman/rhai_workflows/`). -tinyagents = { version = "1.7", features = ["sqlite", "repl"] } +# The `repl` feature (embedded Rhai `.ragsh` session runtime powering the +# `rhai_workflows` language-workflow tool, `src/openhuman/rhai_workflows/`) is +# NOT enabled here — the default-ON `flows` feature turns it on via +# `tinyagents/repl` (#4797), so a slim build without `flows` sheds `rhai`. +# The crate itself can never be dropped: 26+ domains consume tinyagents. +tinyagents = { version = "1.7", features = ["sqlite"] } # TinyCortex — Rust core for the memory engine (store/chunks/tree/retrieval/ # queue/ingest/score + long tail), vendored as a git submodule and patched # below to `vendor/tinycortex`. OpenHuman's memory subsystem migrates onto this @@ -334,7 +340,7 @@ tokio = { version = "1", features = ["test-util"] } proptest = "1" [features] -default = ["tokenjuice-treesitter", "voice"] +default = ["tokenjuice-treesitter", "voice", "flows"] # AST-aware code compression (tree-sitter Rust/TS/Python grammars; C build). # On by default; disable to fall back to the brace-depth heuristic. tokenjuice-treesitter = [ @@ -351,6 +357,18 @@ tokenjuice-treesitter = [ # inference domain (shared with accessibility for cpal) and await a separate # `inference` gate. voice = ["dep:hound", "dep:lettre"] +# Flows domains: the `flows::` automation surface (saved tinyflows graphs — +# create/run/schedule + the workflow_builder / flow_discovery agents), the +# `tinyflows::` adapter seam, and the `rhai_workflows::` language-workflow tool. +# Default-ON — the desktop app always ships with Workflows. Slim / headless +# builds opt out via `--no-default-features --features ""`, +# which drops `tinyflows` + its `jaq-core`/`jaq-std`/`jaq-json` JSON-query stack +# and, via `tinyagents/repl`, the `rhai` scripting engine. Composes with the +# runtime `DomainSet::flows` flag (#4796): the feature narrows the compile-time +# surface, `DomainSet` gates it at runtime. +# NOTE: this gate does NOT drop `tinyagents` itself — 26+ domains consume it. +# Only its `repl` feature (⇒ `rhai`) is exclusive to flows. +flows = ["dep:tinyflows", "tinyagents/repl"] sandbox-landlock = ["dep:landlock"] sandbox-bubblewrap = [] peripheral-rpi = ["dep:rppal"] From 1271a41749ae7a2cf3b2683d69aef453af632f2b Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Wed, 15 Jul 2026 17:54:53 +0530 Subject: [PATCH 02/10] refactor(flows): gate the flows/tinyflows/rhai_workflows module decls (#4797) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Leaf-gate: no stub facade. Every symbol reached from outside these three modules is a registration site, and registration sites want ABSENCE — a stub returning `Err("flows disabled")` would register a controller that then fails at runtime, the opposite of the intended unknown-method behaviour. Call sites carry their own #[cfg] instead. --- src/openhuman/mod.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/openhuman/mod.rs b/src/openhuman/mod.rs index 74f8442046..cdf3f04101 100644 --- a/src/openhuman/mod.rs +++ b/src/openhuman/mod.rs @@ -52,6 +52,7 @@ pub mod embeddings; pub mod encryption; pub mod file_state; pub mod file_storage; +#[cfg(feature = "flows")] pub mod flows; pub mod harness_init; pub mod health; @@ -98,6 +99,7 @@ pub mod provider_surfaces; pub mod recall_calendar; pub mod redirect_links; pub mod referral; +#[cfg(feature = "flows")] pub mod rhai_workflows; pub mod routing; pub mod runtime_node; @@ -127,6 +129,7 @@ pub mod thread_goals; pub mod threads; pub mod tinyagents; pub mod tinycortex; +#[cfg(feature = "flows")] pub mod tinyflows; pub mod tinyplace; pub mod tls; From 1f5bb0f502a9d5e97bcca6c3caba6198c8397573 Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Wed, 15 Jul 2026 17:54:53 +0530 Subject: [PATCH 03/10] refactor(core): gate flows controller, subscriber, and boot reconcile (#4797) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three core-side registration sites for the flows domain: - all.rs: the flows controller push (keeps its DomainGroup::Flows tag — that is the orthogonal runtime axis). - jsonrpc.rs: the FlowTriggerSubscriber registration. The existing `if plan.flows` runtime guard does not help here — the type path must still resolve for the block to compile. - services.rs: reconcile_schedule_triggers_on_boot. Each disabled path keeps a debug log, per the logging convention. --- src/core/all.rs | 2 ++ src/core/jsonrpc.rs | 8 ++++++++ src/core/runtime/services.rs | 2 ++ 3 files changed, 12 insertions(+) diff --git a/src/core/all.rs b/src/core/all.rs index e7697117be..d1d73f6bef 100644 --- a/src/core/all.rs +++ b/src/core/all.rs @@ -232,6 +232,8 @@ fn build_registered_controllers() -> Vec { crate::openhuman::cron::all_cron_registered_controllers(), ); // Saved automation workflows (tinyflows graphs): create/get/list/update/delete/run + // (gated with flows). + #[cfg(feature = "flows")] push( &mut controllers, DomainGroup::Flows, diff --git a/src/core/jsonrpc.rs b/src/core/jsonrpc.rs index ed242d4176..07ab12e174 100644 --- a/src/core/jsonrpc.rs +++ b/src/core/jsonrpc.rs @@ -2024,6 +2024,10 @@ fn register_domain_subscribers( // runs `flows::ops::flows_run`, so schedule/app-event workflows still // dispatch when no realtime channel is configured or // `OPENHUMAN_DISABLE_CHANNEL_LISTENERS` short-circuits `start_channels`. + // The `plan.flows` runtime guard cannot stand in for the compile-time gate: + // the `flows::bus::FlowTriggerSubscriber` type path below must still resolve + // for this to compile, so the whole block is `#[cfg]`-gated too. + #[cfg(feature = "flows")] if plan.flows { if group_first_time(DomainGroup::Flows) { if let Some(handle) = crate::core::event_bus::subscribe_global(Arc::new( @@ -2039,6 +2043,10 @@ fn register_domain_subscribers( } else { log::debug!("[event_bus] flows trigger subscriber SKIPPED — Flows domain disabled"); } + #[cfg(not(feature = "flows"))] + log::debug!( + "[event_bus] flows trigger subscriber SKIPPED — flows feature disabled at compile time" + ); // Memory: conversation-persistence + sync-stage bridge. if plan.memory { diff --git a/src/core/runtime/services.rs b/src/core/runtime/services.rs index 89b36521e1..3b814b1d37 100644 --- a/src/core/runtime/services.rs +++ b/src/core/runtime/services.rs @@ -127,6 +127,8 @@ pub fn spawn_cron_service() { // flow (issue B2) — idempotent, so a flow whose binding // predates this feature (or was otherwise lost) gets its // schedule re-registered without the user re-toggling it. + // Gated with flows — absent entirely from a slim build. + #[cfg(feature = "flows")] if let Err(e) = crate::openhuman::flows::ops::reconcile_schedule_triggers_on_boot(&config).await { From 99da254b759037647b05f3c6a58e231a0f418f6c Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Wed, 15 Jul 2026 17:55:05 +0530 Subject: [PATCH 04/10] refactor(tools): gate the flows + rhai_workflows agent tools (#4797) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gates the four glob re-exports and, per element, the 25 flows-owned tools in the default registry — the same construct the voice gate already uses on elements of this vec!. Also gates the rhai_workflows tool, whose engine the gate sheds via tinyagents/repl. `const FLOWS: &[&str]` in tool_group() stays UNgated: it is an inert &str table referencing no flows types, so the tool-group classification keeps working (and its tests keep passing) in a slim build. --- src/openhuman/tools/mod.rs | 4 ++++ src/openhuman/tools/ops.rs | 34 +++++++++++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/openhuman/tools/mod.rs b/src/openhuman/tools/mod.rs index 58aef6e4a6..3eb777dd34 100644 --- a/src/openhuman/tools/mod.rs +++ b/src/openhuman/tools/mod.rs @@ -26,8 +26,11 @@ pub use crate::openhuman::credentials::tools::*; pub use crate::openhuman::cron::tools::*; pub use crate::openhuman::dashboard::tools::*; pub use crate::openhuman::doctor::tools::*; +#[cfg(feature = "flows")] pub use crate::openhuman::flows::builder_tools::*; +#[cfg(feature = "flows")] pub use crate::openhuman::flows::discovery_tools::*; +#[cfg(feature = "flows")] pub use crate::openhuman::flows::tools::*; pub use crate::openhuman::health::tools::*; pub use crate::openhuman::integrations::tools::*; @@ -41,6 +44,7 @@ pub use crate::openhuman::monitor::tools::*; pub use crate::openhuman::orchestration::tools::*; pub use crate::openhuman::people::tools::*; pub use crate::openhuman::referral::tools::*; +#[cfg(feature = "flows")] pub use crate::openhuman::rhai_workflows::tools::*; pub use crate::openhuman::screen_intelligence::tools::*; pub use crate::openhuman::search::tools::*; diff --git a/src/openhuman/tools/ops.rs b/src/openhuman/tools/ops.rs index 95fb2736bb..43bf19a595 100644 --- a/src/openhuman/tools/ops.rs +++ b/src/openhuman/tools/ops.rs @@ -277,6 +277,7 @@ pub fn all_tools_with_runtime( // graph and returns a proposal summary — never creates/enables a // flow itself. Only the chat UI's WorkflowProposalCard "Save & // enable" action calls `flows_create`. + #[cfg(feature = "flows")] Box::new(ProposeWorkflowTool::new(config.clone())), // workflow-builder agent tool belt (Phase 5b). A deliberately narrow, // propose-or-read surface: revise a draft (validate-only), read saved @@ -285,38 +286,53 @@ pub fn all_tools_with_runtime( // or enable a flow (only the user's own `flows_create` click does); the // read tools are `PermissionLevel::None`, and `dry_run_workflow` is // autonomy-tier gated + wired to deterministic mock capabilities. + #[cfg(feature = "flows")] Box::new(ReviseWorkflowTool::new(config.clone())), // Structured incremental edits (F1): apply a small ops[] list to a base // graph (saved flow or inline) instead of re-emitting the whole graph, // then validate + gate + return a proposal (same contract as revise). // Proposal-only — never persists. + #[cfg(feature = "flows")] Box::new(EditWorkflowTool::new(config.clone())), // Standalone validate (F3): run the SAME structural + hard-gate stack // the propose/save tools use, without emitting a proposal — a pure // check so the agent can self-verify a draft mid-build. Read-only. + #[cfg(feature = "flows")] Box::new(ValidateWorkflowTool::new(config.clone())), // Read a saved flow's revision history (F6) — prior graph snapshots the // agent can inspect / pick a rollback target from. Read-only. + #[cfg(feature = "flows")] Box::new(GetFlowHistoryTool::new(config.clone())), // Phase 4 self-debug loop (F4): find a failing run, resume a parked // run (approval-gated), or cancel a runaway one. + #[cfg(feature = "flows")] Box::new(ListFlowRunsTool::new(config.clone())), + #[cfg(feature = "flows")] Box::new(ResumeFlowRunTool::new(config.clone())), + #[cfg(feature = "flows")] Box::new(CancelFlowRunTool::new(config.clone())), // Gated create (F4/F12): create a NEW flow — born disabled, approval // gated — and duplicate an existing one (disabled copy) for // clone-then-edit. Behind the Phase 3 safety rails. + #[cfg(feature = "flows")] Box::new(CreateWorkflowTool::new(config.clone())), + #[cfg(feature = "flows")] Box::new(DuplicateFlowTool::new(config.clone())), + #[cfg(feature = "flows")] Box::new(ListFlowsTool::new(config.clone())), + #[cfg(feature = "flows")] Box::new(GetFlowTool::new(config.clone())), + #[cfg(feature = "flows")] Box::new(GetFlowRunTool::new(config.clone())), + #[cfg(feature = "flows")] Box::new(ListFlowConnectionsTool::new(config.clone())), + #[cfg(feature = "flows")] Box::new(SearchToolCatalogTool::new(config.clone())), // Full live contract (schemas, real required_args/output_fields, // primary_array_path) for one action slug found via // search_tool_catalog — the grounding step before WIRING a node's // args/downstream bindings (systemic tool-contract fix, Part 1). + #[cfg(feature = "flows")] Box::new(GetToolContractTool::new(config.clone())), // B12: ONE bounded, READ-ONLY, REAL Composio call to derive the real // primary_array_path/output_fields when the live listing publishes no @@ -326,36 +342,45 @@ pub fn all_tools_with_runtime( // toolkits only — see builder_tools.rs's module doc for the carve-out // this makes in the workflow-builder agent's "no composio_execute" // invariant. + #[cfg(feature = "flows")] Box::new(GetToolOutputSampleTool::new(config.clone())), // Ground an `agent` node's `agent_ref` in real registered agent-kind ids // (researcher / code_executor / …) — the agent analogue of // search_tool_catalog. Read-only. + #[cfg(feature = "flows")] Box::new(ListAgentProfilesTool::new()), // Steer toolkit choice toward what's already connected + surface which // toolkits a flow still needs (Phase 5, item 19). Read-only. + #[cfg(feature = "flows")] Box::new(ListConnectableToolkitsTool::new(config.clone())), // Queryable DSL schema (F2): enumerate the 12 node kinds and fetch one // kind's full config-field/port/example/gotcha contract — the DSL // analogue of search_tool_catalog + get_tool_contract, so an agent need // not rely on prompt prose or memory for node config shapes. Read-only. + #[cfg(feature = "flows")] Box::new(ListNodeKindsTool::new()), + #[cfg(feature = "flows")] Box::new(GetNodeKindContractTool::new()), + #[cfg(feature = "flows")] Box::new(DryRunWorkflowTool::new(security.clone(), config.clone())), // Real end-to-end test run of a SAVED flow (Write / external-effect). The // workflow-builder prompt requires it to ask the user for confirmation // first, and the flow's own approval gate still pauses outbound nodes. + #[cfg(feature = "flows")] Box::new(RunFlowTool::new(config.clone())), // Persist a built graph onto an EXISTING saved flow (Write). Used only // when the USER explicitly asks the agent to save; the seeded build // turn from the Flows prompt bar is propose-only (see #4596) — Accept // + the canvas's own Save persist the graph. The tool itself can // never create a flow or change enabled/require_approval. + #[cfg(feature = "flows")] Box::new(SaveWorkflowTool::new(config.clone())), // Flow Scout discovery: the `flow_discovery` agent's terminal emit // sink. Read-only reasoning over the user's data ends by calling // `suggest_workflows`, which persists workflow ideas for the Flows page // "Suggested for you" section. `PermissionLevel::None`, no external // effect — writes only to the agent's own suggestions store. + #[cfg(feature = "flows")] Box::new(SuggestWorkflowsTool::new(config.clone())), // Wallet tools — expose wallet operations to the agent tool-call pipeline // so the crypto sub-agent can prepare transfers, check status, etc. @@ -1069,12 +1094,15 @@ pub fn all_tools_with_runtime( // tiers only — dark on `readonly` (it can drive effectful tools/sub-agents) // and behind the `OPENHUMAN_RHAI_WORKFLOWS=0` kill switch. Every effectful inner call // still re-gates itself in the Rhai bridge, so this surface adds no new - // ungated capability. + // ungated capability. Gated with `flows` — the whole tool (and the `rhai` + // engine behind it, via `tinyagents/repl`) is absent from a slim build. + #[cfg(feature = "flows")] let rhai_workflows_enabled = std::env::var("OPENHUMAN_RHAI_WORKFLOWS") .or_else(|_| std::env::var("OPENHUMAN_RHAI")) .or_else(|_| std::env::var("OPENHUMAN_RLM")) .map(|v| v != "0") .unwrap_or(true); + #[cfg(feature = "flows")] if rhai_workflows_enabled && security.autonomy != crate::openhuman::security::policy::AutonomyLevel::ReadOnly { @@ -1087,6 +1115,10 @@ pub fn all_tools_with_runtime( "[rhai_workflows] rhai_workflows tool not registered (readonly tier or OPENHUMAN_RHAI_WORKFLOWS=0)" ); } + #[cfg(not(feature = "flows"))] + tracing::debug!( + "[rhai_workflows] rhai_workflows tool not registered — flows feature disabled at compile time" + ); // DomainSet post-filter (#4796): drop tools whose DomainGroup is disabled // under the ambient CoreContext. With no active context, or under From f7d557baedebf44de628aa1f681f2e7b181c4d7d Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Wed, 15 Jul 2026 17:55:05 +0530 Subject: [PATCH 05/10] refactor(agent_registry): gate the workflow_builder + flow_discovery built-ins (#4797) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This module is always compiled, so the two BuiltinAgent entries' prompt_fn paths break without a cfg. Gating the element also drops its include_str! of the agent TOML — correct: a slim build must not advertise an agent whose entire tool belt is absent. #[cfg] on const-slice elements is stable (attributes on array-literal elements) and verified in both directions here. --- src/openhuman/agent_registry/agents/loader.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/openhuman/agent_registry/agents/loader.rs b/src/openhuman/agent_registry/agents/loader.rs index bcaa34a0cf..2dd60aca93 100644 --- a/src/openhuman/agent_registry/agents/loader.rs +++ b/src/openhuman/agent_registry/agents/loader.rs @@ -290,6 +290,9 @@ pub const BUILTINS: &[BuiltinAgent] = &[ // Workflow-authoring specialist (Phase 5a): builds tinyflows automation // graphs from natural language and returns a validated PROPOSAL — it never // persists or enables a flow. Deliberately narrow propose-or-read tool belt. + // Gated with `flows`: a slim build must not advertise an agent whose entire + // tool belt is absent, so the entry (and its `include_str!`) is stripped. + #[cfg(feature = "flows")] BuiltinAgent { id: "workflow_builder", toml: include_str!("../../flows/agents/workflow_builder/agent.toml"), @@ -301,7 +304,9 @@ pub const BUILTINS: &[BuiltinAgent] = &[ // `suggest_workflows` to record concrete, buildable automation ideas for // the Flows page "Suggested for you" section. It never persists or enables // a flow — the read-only counterpart to `workflow_builder`, which turns a - // picked suggestion into a real graph proposal. + // picked suggestion into a real graph proposal. Gated with `flows` (same + // reasoning as `workflow_builder` above). + #[cfg(feature = "flows")] BuiltinAgent { id: "flow_discovery", toml: include_str!("../../flows/agents/flow_discovery/agent.toml"), @@ -1000,6 +1005,9 @@ mod tests { assert!(matches!(def.tools, ToolScope::Wildcard)); } + // Both flows agents are `#[cfg(feature = "flows")]` entries in `BUILTINS` + // (#4797), so these tests only apply when the gate is on. + #[cfg(feature = "flows")] #[test] fn workflow_builder_is_registered_worker_with_narrow_propose_or_read_scope() { // Phase 5a/5b: the workflow-builder must be a Worker-tier leaf whose @@ -1130,6 +1138,7 @@ mod tests { ); } + #[cfg(feature = "flows")] #[test] fn flow_discovery_is_registered_readonly_reasoning_scout() { // The Flow Scout must be a read-only reasoning leaf: it reads the From 1970c9bba19ea872cf81437a0990d9f0531031ad Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Wed, 15 Jul 2026 17:55:16 +0530 Subject: [PATCH 06/10] test(flows): gate flows-asserting tests + add directional coverage (#4797) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The CI smoke lane runs `cargo check` only, never `cargo test --no-default-features` — so CI stays green while the disabled-build test suite breaks. Six pre-existing sites hard-assert that flows exists and fail (or panic) once the gate lands: - all_tests.rs: `full_ns.contains("flows")`, `group_for_namespace`, and two `.expect("a flows.* method exists")` vehicles. - jsonrpc_tests.rs: the transport-layer gated-method vehicle. - builtin_definitions.rs + definition_tests.rs: built-in agent id and max-iterations tables listing the two flows agents. Adds directional tests: controllers registered when the feature is on, absent when off (one namespace — tinyflows registers no controllers and rhai_workflows is AgentOnly), plus a tools test proving all 25 flow tools and rhai_workflows leave the registry when the gate is off. SecurityPolicy::default() is Supervised, so the rhai assertion is real. --- src/core/all_tests.rs | 47 ++++++++++++++++- src/core/jsonrpc_tests.rs | 5 ++ .../agent/harness/builtin_definitions.rs | 3 ++ .../agent/harness/definition_tests.rs | 3 ++ src/openhuman/tools/ops_tests.rs | 51 +++++++++++++++++++ 5 files changed, 108 insertions(+), 1 deletion(-) diff --git a/src/core/all_tests.rs b/src/core/all_tests.rs index 08412d4d26..7a6fcd4d4e 100644 --- a/src/core/all_tests.rs +++ b/src/core/all_tests.rs @@ -738,6 +738,7 @@ async fn harness_excludes_gated_namespaces() { .iter() .map(|s| s.namespace) .collect(); + #[cfg(feature = "flows")] assert!(full_ns.contains("flows"), "full() must expose flows"); assert!(full_ns.contains("voice"), "full() must expose voice"); @@ -777,6 +778,12 @@ async fn harness_excludes_gated_namespaces() { ); } +// Uses a `flows.*` method as its gated-family vehicle, so the whole test is +// `#[cfg(feature = "flows")]`: without the feature there is no flows controller +// in the registry at all and the `.expect()` below would panic. The runtime +// gating this proves is orthogonal to the compile-time gate, and CI runs the +// test suite on default features (flows ON), so no coverage is lost there. +#[cfg(feature = "flows")] #[tokio::test] async fn dispatch_returns_none_for_gated_method() { // A method whose group is gated OFF under the ambient DomainSet must @@ -808,6 +815,8 @@ async fn dispatch_returns_none_for_gated_method() { ); } +// Same flows-vehicle reasoning as `dispatch_returns_none_for_gated_method`. +#[cfg(feature = "flows")] #[tokio::test] async fn schema_lookup_is_gated_in_lockstep_with_dispatch() { // #4808 review: `schema_for_rpc_method` must gate identically to @@ -857,7 +866,10 @@ fn group_mapping_smoke() { assert_eq!(group_for_namespace("config"), Some(DomainGroup::Config)); assert_eq!(group_for_namespace("security"), Some(DomainGroup::Security)); assert_eq!(group_for_namespace("agent"), Some(DomainGroup::Agent)); - // …and a representative gated one maps to its gate group. + // …and a representative gated one maps to its gate group. `group_for_namespace` + // reads the real controller registry, so a compile-time-gated family has no + // entry to map when its feature is off. + #[cfg(feature = "flows")] assert_eq!(group_for_namespace("flows"), Some(DomainGroup::Flows)); assert_eq!(group_for_namespace("skills"), Some(DomainGroup::Skills)); assert_eq!(group_for_namespace("voice"), Some(DomainGroup::Voice)); @@ -866,3 +878,36 @@ fn group_mapping_smoke() { // Internal-only registry is grouped too (mcp_audit → Mcp). assert_eq!(group_for_namespace("mcp_audit"), Some(DomainGroup::Mcp)); } + +// --- #4797: `flows` compile-time gate (directional proof) ------------------- +// +// One namespace, not three: `tinyflows` registers no controllers, and +// `rhai_workflows` is `scope() = AgentOnly` (no controller schemas in v1), so +// `flows` is the gate's entire controller surface. + +#[cfg(feature = "flows")] +#[test] +fn flows_controllers_registered_when_feature_on() { + let namespaces: Vec<&str> = all_controller_schemas() + .iter() + .map(|s| s.namespace) + .collect(); + assert!( + namespaces.contains(&"flows"), + "with the `flows` feature ON the flows controllers must be registered" + ); +} + +#[cfg(not(feature = "flows"))] +#[test] +fn flows_controllers_absent_when_feature_off() { + let namespaces: Vec<&str> = all_controller_schemas() + .iter() + .map(|s| s.namespace) + .collect(); + assert!( + !namespaces.contains(&"flows"), + "with the `flows` feature OFF the flows controllers must be absent \ + (unknown-method over /rpc, omitted from /schema)" + ); +} diff --git a/src/core/jsonrpc_tests.rs b/src/core/jsonrpc_tests.rs index 11b18a99cd..b4774d2d82 100644 --- a/src/core/jsonrpc_tests.rs +++ b/src/core/jsonrpc_tests.rs @@ -243,6 +243,11 @@ async fn invoke_doctor_models_rejects_unknown_param() { assert!(err.contains("unknown param 'invalid'")); } +// Uses a `flows.*` method as its gated-family vehicle: without the `flows` +// feature there is no flows controller in the registry and the `.expect()` +// below would panic. The transport-layer gating it proves is orthogonal to the +// compile-time gate (#4797). +#[cfg(feature = "flows")] #[tokio::test] async fn gated_method_is_unknown_at_transport_even_with_malformed_params() { // #4808 review (CodeRabbit): prove the schema-gate fix at the JSON-RPC diff --git a/src/openhuman/agent/harness/builtin_definitions.rs b/src/openhuman/agent/harness/builtin_definitions.rs index 8764788832..f1cd08eeda 100644 --- a/src/openhuman/agent/harness/builtin_definitions.rs +++ b/src/openhuman/agent/harness/builtin_definitions.rs @@ -266,7 +266,10 @@ mod tests { "critic", "archivist", "summarizer", + // Gated with `flows` (#4797) — absent from a slim build. + #[cfg(feature = "flows")] "workflow_builder", + #[cfg(feature = "flows")] "flow_discovery", ] { assert!(ids.contains(&expected.to_string()), "missing {expected}"); diff --git a/src/openhuman/agent/harness/definition_tests.rs b/src/openhuman/agent/harness/definition_tests.rs index ed905d26dc..70fe297ca9 100644 --- a/src/openhuman/agent/harness/definition_tests.rs +++ b/src/openhuman/agent/harness/definition_tests.rs @@ -374,7 +374,10 @@ fn all_builtin_agent_definitions_have_expected_effective_max_iterations() { ("skill_creator", 50), ("task_manager_agent", 50), ("tools_agent", 50), + // Gated with `flows` (#4797) — absent from a slim build. + #[cfg(feature = "flows")] ("flow_discovery", 50), + #[cfg(feature = "flows")] ("workflow_builder", 50), ("skill_executor", 50), ("tinyplace_agent", 50), diff --git a/src/openhuman/tools/ops_tests.rs b/src/openhuman/tools/ops_tests.rs index 3e1e0d5b7e..bf126c0edc 100644 --- a/src/openhuman/tools/ops_tests.rs +++ b/src/openhuman/tools/ops_tests.rs @@ -2269,3 +2269,54 @@ fn no_gate_family_tool_silently_defaults_to_platform() { ); } } + +// --- #4797: `flows` compile-time gate --------------------------------------- + +/// With the `flows` feature off, every flows-owned agent tool — and the +/// `rhai_workflows` tool whose engine the gate sheds via `tinyagents/repl` — is +/// compiled out of the default registry entirely. +/// +/// `SecurityPolicy::default()` is `Supervised` (not `ReadOnly`), so the +/// `rhai_workflows` assertion is a real one: that tool *would* be registered at +/// this tier if the feature were on. +#[test] +#[cfg(not(feature = "flows"))] +fn default_tools_omits_flows_tools_when_feature_off() { + let security = Arc::new(SecurityPolicy::default()); + let tools = default_tools(security); + let names = tool_names(&tools); + + for absent in [ + "propose_workflow", + "revise_workflow", + "edit_workflow", + "validate_workflow", + "get_flow_history", + "list_flow_runs", + "resume_flow_run", + "cancel_flow_run", + "create_workflow", + "duplicate_flow", + "list_flows", + "get_flow", + "get_flow_run", + "list_flow_connections", + "search_tool_catalog", + "get_tool_contract", + "get_tool_output_sample", + "list_agent_profiles", + "list_connectable_toolkits", + "list_node_kinds", + "get_node_kind_contract", + "dry_run_workflow", + "run_flow", + "save_workflow", + "suggest_workflows", + "rhai_workflows", + ] { + assert!( + !names.iter().any(|n| n == absent), + "tool `{absent}` must be compiled out when the `flows` feature is off; got: {names:?}" + ); + } +} From 8997359fada7b2f3362021698319e6653d43c7f6 Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Wed, 15 Jul 2026 17:55:26 +0530 Subject: [PATCH 07/10] build(tauri): enable the flows feature in the desktop shell (#4797) The shell sets default-features = false, so without this the shipped app would silently lose the entire Flows surface. Multi-line array so the sibling gates append cleanly. --- app/src-tauri/Cargo.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/src-tauri/Cargo.toml b/app/src-tauri/Cargo.toml index 177210c7e1..31c8b507d6 100644 --- a/app/src-tauri/Cargo.toml +++ b/app/src-tauri/Cargo.toml @@ -132,7 +132,9 @@ cef = { version = "=146.4.1", default-features = false } # by tying the core's lifetime to the GUI process. The existing port-7788 # probe in `core_process::ensure_running` still attaches to a running # `openhuman-core run` harness when one is already listening. -openhuman_core = { path = "../..", package = "openhuman", default-features = false } +openhuman_core = { path = "../..", package = "openhuman", default-features = false, features = [ + "flows", +] } tinyjuice = { version = "0.2.1", default-features = false } [target.'cfg(unix)'.dependencies] From b25121bb3d3d59d7c6e37107da9861a87c7fce82 Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Wed, 15 Jul 2026 17:55:26 +0530 Subject: [PATCH 08/10] docs(agents): document the flows leaf-gate + dep-shed scope (#4797) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds the gate-table row and the leaf-gate rationale (why registration sites want absence, not a stub). Corrects the issue's dep claim: the gate does NOT shed tinyagents (26+ domains consume it). "Sheds the rhai scripting engine" is true only at the feature level — rhai arrives via tinyagents/repl, which flows now owns. Also notes that compiling clean is not proof of a dep shed, and records the disabled-build test gap for future gates. The ci-lite smoke lane needs no functional change: its `--features tokenjuice-treesitter` is an explicit allow-list, so a new default-ON gate is excluded automatically. Only the stale step name ("the voice gate disabled") is corrected. --- .github/workflows/ci-lite.yml | 2 +- AGENTS.md | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-lite.yml b/.github/workflows/ci-lite.yml index 870276ae6d..7f00e2a770 100644 --- a/.github/workflows/ci-lite.yml +++ b/.github/workflows/ci-lite.yml @@ -382,7 +382,7 @@ jobs: cache-on-failure: true shared-key: pr-rust-feature-gate-smoke - - name: Check core builds with the voice gate disabled + - name: Check core builds with the default domain gates disabled run: bash scripts/ci-cancel-aware.sh cargo check --manifest-path Cargo.toml --no-default-features --features tokenjuice-treesitter rust-core-coverage: diff --git a/AGENTS.md b/AGENTS.md index 03ecc7e77c..ef3ad25631 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -233,11 +233,18 @@ GGML_NATIVE=OFF cargo check --manifest-path Cargo.toml \ | Feature | Default | Gates | Drops deps | | ------- | ------- | ----- | ---------- | | `voice` | ON | `openhuman::voice` + `openhuman::audio_toolkit` domains — STT/TTS providers, dictation server, always-on listening, podcast audio + email | `hound`, `lettre` | +| `flows` | ON | `openhuman::flows` (saved automation graphs — create/run/schedule, the `workflow_builder` + `flow_discovery` agents), `openhuman::tinyflows` (engine seam), `openhuman::rhai_workflows` (`.ragsh` language-workflow tool) | `tinyflows`, `jaq-core`, `jaq-std`, `jaq-json`, `rhai` | **Facade pattern (pathfinder for the other gates).** `pub mod voice;` is **always compiled** as a facade: the real submodules are `#[cfg(feature = "voice")]`, and a `#[cfg(not(feature = "voice"))] mod stub;` (`src/openhuman/voice/stub.rs`) re-exposes the same public surface that always-on / other-gated callers use (`server`, `dictation_listener`, `streaming`, `reply_speech`, `cloud_transcribe`, `cli`, `create_stt_provider`, `effective_stt_provider`, `publish_ptt_transcript_committed`) with no-op / `None` / disabled-error bodies. Callers therefore do **not** need per-call `#[cfg]`. When voice is off: the voice/audio controllers are unregistered (unknown-method over `/rpc`, absent from `/schema`), the `audio_generate_podcast` agent tools are absent, and `openhuman voice` returns a "voice disabled" error. Stub signatures must match the real ones exactly — the disabled build (`--no-default-features --features tokenjuice-treesitter`) is the **only** thing that catches drift, so run it before pushing any change to the voice surface. **Scope note:** the `voice` gate does **not** drop `whisper-rs` / `llama` / `cpal`. Those live in the inference domain (`src/openhuman/inference/local/service/whisper_engine.rs`; `cpal` is shared with accessibility) and await a separate future `inference` gate. The issue-level DoD line claiming whisper is dropped is superseded by this scope correction. +**Leaf-gate pattern (`flows`).** Where `voice` needs a stub facade, `flows` needs **none** — and deliberately so. Every symbol reached from outside the gate is a *registration site* (controller push in `src/core/all.rs`, the `FlowTriggerSubscriber` in `src/core/jsonrpc.rs`, boot reconcile in `src/core/runtime/services.rs`, agent-tool `vec!` elements in `src/openhuman/tools/ops.rs`, `BuiltinAgent` entries in `agent_registry/agents/loader.rs`). Registration sites want **absence**: a stub that registered a controller returning `Err("flows disabled")` would make `flows.*` a *known* method that fails at runtime — the opposite of the intended "unknown method / omitted tool". So the three modules are `#[cfg(feature = "flows")]` at their `pub mod` declaration and each call site carries its own `#[cfg]`. Nothing inside `flows/`, `tinyflows/`, or `rhai_workflows/` is modified. There is no `openhuman flows` CLI subcommand, so no CLI stub is needed either. When flows is off: the `flows.*` controllers are unregistered (unknown-method over `/rpc`, absent from `/schema`), all 25 flow agent tools + `rhai_workflows` are absent, and the `workflow_builder` / `flow_discovery` built-in agents are not advertised. + +**Scope note (`flows` deps):** the gate sheds `tinyflows` + its `jaq-core` / `jaq-std` / `jaq-json` JSON-query stack, and `rhai`. It does **not** shed `tinyagents` — 26+ domains consume that crate. The issue-level DoD line reading "sheds the rhai scripting engine" is therefore true only at the **feature** level: `rhai` arrives via `tinyagents/repl`, which the root `Cargo.toml` no longer enables directly — the `flows` feature turns it on. Dropping `flows` drops `repl`, which drops `rhai`; `tinyagents` itself stays. Verify a claimed shed with `cargo tree -i --no-default-features --features tokenjuice-treesitter` (must return nothing) — compiling clean is **not** proof that a dep was dropped. + +**Testing gotcha (applies to every gate).** The CI smoke lane runs `cargo check` only — it never runs `cargo test --no-default-features`, so CI stays green while the disabled-build **test** suite is broken. Tests that hard-assert a gated family (`.expect("a flows.* method exists")`, `assert!(full_ns.contains("flows"))`, `group_for_namespace("flows")`, built-in-agent id lists) must be `#[cfg]`-gated in lockstep with the feature. Run `GGML_NATIVE=OFF cargo test --lib --no-default-features --features tokenjuice-treesitter core::` locally before pushing any gate change. + ### Event bus (`src/core/event_bus/`) Typed pub/sub + native request/response. Both singletons — use module-level functions. From f14c34b71352b40b79e9915620d549a1c6ffaee0 Mon Sep 17 00:00:00 2001 From: oxoxDev Date: Wed, 15 Jul 2026 20:34:43 +0530 Subject: [PATCH 09/10] test(flows): gate flows JSON-RPC E2E tests behind the flows feature (#4797) The flows controllers are cfg-gated out of the slim build (src/core/all.rs), so openhuman.flows_* become unknown JSON-RPC methods there. The eight flows cases in tests/json_rpc_e2e.rs called them unconditionally and asserted success, which would fail once the target compiles in the slim direction. Gate the eight tests plus their four flows-only helpers so the disabled-build suite skips them. Verified with voice on / flows off (isolating the pre-existing voice test_seam gap tracked in #4916): cargo check --no-default-features --features tokenjuice-treesitter,voice --test json_rpc_e2e -> 0 errors, no unused-helper warnings. Default build unchanged. --- tests/json_rpc_e2e.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/json_rpc_e2e.rs b/tests/json_rpc_e2e.rs index 90668c3263..e47687e5f9 100644 --- a/tests/json_rpc_e2e.rs +++ b/tests/json_rpc_e2e.rs @@ -12477,6 +12477,7 @@ async fn json_rpc_workflows_lifecycle_round_trip() { /// Shared boot for a flows E2E: isolates `HOME`, seeds a minimal config against /// a mock upstream, and stands up the core HTTP router. Returns the rpc base /// URL plus the join handles + tempdir the caller must keep alive/abort. +#[cfg(feature = "flows")] async fn boot_flows_rpc_env() -> ( String, tempfile::TempDir, @@ -12509,6 +12510,7 @@ async fn boot_flows_rpc_env() -> ( /// The smallest valid graph with a human-in-the-loop approval gate: /// `trigger → gate(requires_approval) → downstream`. A run pauses at `gate`; /// approving it via `flows_resume` runs `downstream`. +#[cfg(feature = "flows")] fn approval_gated_graph_json() -> Value { json!({ "name": "approval-gated", @@ -12531,6 +12533,7 @@ fn approval_gated_graph_json() -> Value { /// cancel operates on a run id (checkpoint thread id), so we cancel a *fresh* /// parked run rather than the already-completed one (cancelling a terminal run /// is an error). +#[cfg(feature = "flows")] #[tokio::test] async fn json_rpc_flows_lifecycle_round_trip() { let _env_lock = json_rpc_e2e_env_lock(); @@ -12747,6 +12750,7 @@ async fn json_rpc_flows_lifecycle_round_trip() { /// `false`. This pins that the four new controllers are registered and dispatch /// end-to-end (schema + handler wiring), independent of the agent-backed /// `flows_discover`, which needs a provider. +#[cfg(feature = "flows")] #[tokio::test] async fn json_rpc_flows_suggestion_lifecycle_methods_are_wired() { let _env_lock = json_rpc_e2e_env_lock(); @@ -12817,6 +12821,7 @@ async fn json_rpc_flows_suggestion_lifecycle_methods_are_wired() { /// resolves to `chat-v1` on the managed backend while a **reasoning**-tier node /// resolves to `reasoning-v1` — letting the full-arc test assert the two nodes /// routed to distinct managed tiers. +#[cfg(feature = "flows")] fn write_flows_tier_config(openhuman_dir: &Path, api_origin: &str) { let cfg = format!( r#"api_url = "{api_origin}" @@ -12857,6 +12862,7 @@ compaction_enabled = false /// `trigger` feeds a reasoning-tier `planner` (structured `{plan, angle}`) into a /// chat-tier `drafter` that references `nodes.planner.item.json.plan`, then a /// `transform` shapes `{topic, plan, draft}`. +#[cfg(feature = "flows")] fn opus_sonnet_demo_graph() -> Value { json!({ "schema_version": 1, @@ -12931,6 +12937,7 @@ fn opus_sonnet_demo_graph() -> Value { /// /// Runs on the agent-sized worker stack because the builder/scout turns and the /// agent-node run drive the full harness (deep async stacks). +#[cfg(feature = "flows")] #[test] fn json_rpc_flows_full_arc_discover_build_create_run() { run_json_rpc_e2e_on_agent_stack( @@ -12939,6 +12946,7 @@ fn json_rpc_flows_full_arc_discover_build_create_run() { ); } +#[cfg(feature = "flows")] async fn json_rpc_flows_full_arc_discover_build_create_run_inner() { let _env_lock = json_rpc_e2e_env_lock(); // Drain the scripted-completion FIFO even if an assertion below panics, so a @@ -13182,6 +13190,7 @@ async fn json_rpc_flows_full_arc_discover_build_create_run_inner() { /// edge (→ `downstream`) and an `error` edge (→ `recover`). Resuming with the /// gate in `rejections` routes the denied gate's error item to `recover`, and /// `downstream` must not run. +#[cfg(feature = "flows")] #[tokio::test] async fn json_rpc_flows_resume_deny_routes_to_error_port() { let _env_lock = json_rpc_e2e_env_lock(); @@ -13270,6 +13279,7 @@ async fn json_rpc_flows_resume_deny_routes_to_error_port() { /// clean but returns a loud, non-fatal warning; a `schedule` trigger (which /// does fire) warns nothing; a graph with no trigger is `valid: false` with a /// structural error and no warnings. +#[cfg(feature = "flows")] #[tokio::test] async fn json_rpc_flows_validate_reports_warnings_and_errors() { let _env_lock = json_rpc_e2e_env_lock(); @@ -13380,6 +13390,7 @@ async fn json_rpc_flows_validate_reports_warnings_and_errors() { /// becomes an annotated placeholder, and the approximations come back as /// warnings — all WITHOUT persisting (the returned payload is a graph, not a /// saved Flow row; `flows_list` stays empty afterwards). +#[cfg(feature = "flows")] #[tokio::test] async fn json_rpc_flows_import_native_and_n8n() { let _env_lock = json_rpc_e2e_env_lock(); @@ -13491,6 +13502,7 @@ async fn json_rpc_flows_import_native_and_n8n() { /// fault-tolerance: the mock upstream has no connected-accounts route, so the /// Composio source fails and is tolerated (the RPC still returns the HTTP half /// rather than erroring). +#[cfg(feature = "flows")] #[tokio::test] async fn json_rpc_flows_list_connections_aggregates_secret_free() { let _env_lock = json_rpc_e2e_env_lock(); From 3717f309d9f22496ecf75581cfd9a47b0c51cde7 Mon Sep 17 00:00:00 2001 From: M3gA-Mind Date: Thu, 16 Jul 2026 13:58:31 +0530 Subject: [PATCH 10/10] fix(flows): classify all flow-owned tools as DomainGroup::Flows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The runtime DomainSet post-filter (#4796) uses tool_group() to drop tools whose DomainGroup is disabled. The FLOWS classification list was missing 12 flow-owned tools (edit_workflow, validate_workflow, get_flow_history, list_flow_runs, resume_flow_run, cancel_flow_run, create_workflow, duplicate_flow, list_connectable_toolkits, list_node_kinds, get_node_kind_contract, rhai_workflows), so with the flows feature compiled in but DomainSet::flows disabled at runtime they fell through to Platform and stayed callable — leaking the flows surface past the runtime gate. List every flow-owned tool explicitly (now the same 26 names as the compile-time #[cfg(feature = "flows")] registrations and default_tools_omits_flows_tools_when_feature_off) and add a regression loop to tool_group_classifies_gate_and_harness_families. --- src/openhuman/tools/ops.rs | 21 ++++++++++++++++ src/openhuman/tools/ops_tests.rs | 41 ++++++++++++++++++++++++++++++-- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/openhuman/tools/ops.rs b/src/openhuman/tools/ops.rs index 8cdb81848b..55196835ee 100644 --- a/src/openhuman/tools/ops.rs +++ b/src/openhuman/tools/ops.rs @@ -1207,13 +1207,28 @@ fn tool_group(name: &str) -> crate::core::all::DomainGroup { "skill_registry_uninstall", "skill_runtime_resolve_runtimes", ]; + // Flows has no clean tool-name prefix, so it MUST list every flow-owned + // tool explicitly — a missing name falls through to `Platform` below and + // stays callable under a custom `DomainSet { platform: true, flows: false }`, + // leaking the flows surface past the runtime gate (#4808 review; #4797 + // maintainer review). Keep this in lockstep with the `#[cfg(feature = + // "flows")]` registrations in `all_tools_with_runtime` above — the same 26 + // names asserted by `default_tools_omits_flows_tools_when_feature_off`. const FLOWS: &[&str] = &[ "propose_workflow", "revise_workflow", + "edit_workflow", + "validate_workflow", + "get_flow_history", "dry_run_workflow", "save_workflow", "suggest_workflows", "run_flow", + "list_flow_runs", + "resume_flow_run", + "cancel_flow_run", + "create_workflow", + "duplicate_flow", "list_flows", "get_flow", "get_flow_run", @@ -1222,6 +1237,12 @@ fn tool_group(name: &str) -> crate::core::all::DomainGroup { "get_tool_contract", "get_tool_output_sample", "list_agent_profiles", + "list_connectable_toolkits", + "list_node_kinds", + "get_node_kind_contract", + // The `rhai_workflows` (.ragsh) tool is compile-gated with `flows` and + // belongs to the same runtime domain — drop it when Flows is off too. + "rhai_workflows", ]; // Voice family agent tools (audio_toolkit) — no `voice_`/`tts_`/`stt_` // prefix, so they must be listed explicitly or they fall through to diff --git a/src/openhuman/tools/ops_tests.rs b/src/openhuman/tools/ops_tests.rs index 801b207992..96d74a2363 100644 --- a/src/openhuman/tools/ops_tests.rs +++ b/src/openhuman/tools/ops_tests.rs @@ -2224,8 +2224,45 @@ fn tool_group_classifies_gate_and_harness_families() { assert_eq!(tool_group("run_workflow"), DomainGroup::Skills); assert_eq!(tool_group("skill_registry_browse"), DomainGroup::Skills); assert_eq!(tool_group("list_workflows"), DomainGroup::Skills); - assert_eq!(tool_group("propose_workflow"), DomainGroup::Flows); - assert_eq!(tool_group("list_flows"), DomainGroup::Flows); + // Flows has no name prefix, so EVERY flow-owned tool must be classified + // explicitly — a missing one falls through to Platform and stays callable + // when the Flows domain is runtime-gated off (#4797 maintainer review). + // This list mirrors the compile-time `#[cfg(feature = "flows")]` + // registrations and `default_tools_omits_flows_tools_when_feature_off`. + for flow_tool in [ + "propose_workflow", + "revise_workflow", + "edit_workflow", + "validate_workflow", + "get_flow_history", + "dry_run_workflow", + "save_workflow", + "suggest_workflows", + "run_flow", + "list_flow_runs", + "resume_flow_run", + "cancel_flow_run", + "create_workflow", + "duplicate_flow", + "list_flows", + "get_flow", + "get_flow_run", + "list_flow_connections", + "search_tool_catalog", + "get_tool_contract", + "get_tool_output_sample", + "list_agent_profiles", + "list_connectable_toolkits", + "list_node_kinds", + "get_node_kind_contract", + "rhai_workflows", + ] { + assert_eq!( + tool_group(flow_tool), + DomainGroup::Flows, + "flow-owned tool `{flow_tool}` must classify as Flows, not fall through to Platform" + ); + } assert_eq!(tool_group("media_generate_image"), DomainGroup::Media); // Voice audio_* tools have no voice_/tts_/stt_ prefix — must be classified // explicitly, not fall through to Platform (#4808 review).