mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-30 15:03:57 +00:00
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai> Co-authored-by: M3gA-Mind <elvin@tinyhumans.ai>
This commit is contained in:
co-authored by
Steven Enamakel
M3gA-Mind
parent
d09e8c1e5f
commit
b625d735d0
@@ -401,7 +401,7 @@ jobs:
|
||||
cache-on-failure: true
|
||||
shared-key: pr-rust-feature-gate-smoke
|
||||
|
||||
- name: Check core builds with the voice + media gates 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:
|
||||
|
||||
@@ -241,6 +241,7 @@ GGML_NATIVE=OFF cargo check --manifest-path Cargo.toml \
|
||||
| `media` | ON | `openhuman::media_generation` (the `media_generate_*` agent tools) + `openhuman::image` scaffold | none (surface-only) |
|
||||
| `meet` | ON | `openhuman::meet` (join-URL validation) + `openhuman::meet_agent` (live STT/LLM/TTS loop) + `openhuman::agent_meetings` (backend-delegated Meet bot over Socket.IO) | none — see note |
|
||||
| `skills` | ON | `openhuman::skills` + `openhuman::skill_runtime` + `openhuman::skill_registry` domains — SKILL.md discovery/parse/install, workflow execution + run logs, remote catalogs, the `skill_setup` / `skill_executor` builtin agents, and the 16 skill agent tools | none (see below) |
|
||||
| `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.
|
||||
|
||||
@@ -276,6 +277,12 @@ Two places the carve-out doesn't reach, and why they are `#[cfg]` at the call si
|
||||
|
||||
When skills are off: the `skills` / `skill_runtime` / `skill_registry` controllers are unregistered (unknown-method over `/rpc`, absent from `/schema`), the 16 skill agent tools (incl. `run_workflow` / `await_workflow`) are **absent** from the tool list rather than degraded to an error, the `skill_setup` / `skill_executor` builtin agents are gone, and the boot-time remote catalog refresh is skipped. Composes with the runtime `DomainSet::skills` flag (#4796) — that axis needed no change here; #4798 is compile-time only.
|
||||
|
||||
**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 <crate> --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.
|
||||
|
||||
+23
-5
@@ -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
|
||||
@@ -341,7 +347,7 @@ tokio = { version = "1", features = ["test-util"] }
|
||||
proptest = "1"
|
||||
|
||||
[features]
|
||||
default = ["tokenjuice-treesitter", "voice", "web3", "media", "meet", "skills"]
|
||||
default = ["tokenjuice-treesitter", "voice", "web3", "media", "meet", "skills", "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 = [
|
||||
@@ -384,6 +390,18 @@ web3 = ["dep:bitcoin", "dep:curve25519-dalek"]
|
||||
# controllers / stores / subscribers tagged `Media` (agent tools only), and
|
||||
# `openhuman::image` is currently unwired scaffold (added #2997).
|
||||
media = []
|
||||
# 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 "<list without flows>"`,
|
||||
# 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"]
|
||||
# Meet domains: `meet` (join-URL validation), `meet_agent` (live STT/LLM/TTS
|
||||
# loop over an open call), and `agent_meetings` (backend-delegated Meet bot via
|
||||
# Socket.IO). Default-ON — the desktop app always ships with Meet. Slim /
|
||||
|
||||
@@ -166,6 +166,7 @@ openhuman_core = { path = "../..", package = "openhuman", default-features = fal
|
||||
"voice",
|
||||
"tokenjuice-treesitter",
|
||||
"web3",
|
||||
"flows",
|
||||
"meet",
|
||||
"skills",
|
||||
] }
|
||||
|
||||
@@ -232,6 +232,8 @@ fn build_registered_controllers() -> Vec<GroupedController> {
|
||||
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,
|
||||
|
||||
+46
-1
@@ -820,6 +820,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");
|
||||
|
||||
@@ -859,6 +860,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
|
||||
@@ -890,6 +897,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
|
||||
@@ -939,7 +948,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));
|
||||
// `group_for_namespace` is registry-derived, so a compile-time-gated domain
|
||||
// has no controller to map. Skip when its Cargo feature is off.
|
||||
@@ -956,6 +968,39 @@ fn group_mapping_smoke() {
|
||||
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)"
|
||||
);
|
||||
}
|
||||
|
||||
/// All three Meet namespaces register when the `meet` feature is on (#4800).
|
||||
///
|
||||
/// Paired with `meet_controllers_absent_when_feature_off` below: together they
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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}");
|
||||
|
||||
@@ -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),
|
||||
// Compiled out with the `skills` gate — see `openhuman::skills::stub`.
|
||||
#[cfg(feature = "skills")]
|
||||
|
||||
@@ -295,6 +295,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"),
|
||||
@@ -306,7 +309,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"),
|
||||
@@ -1005,6 +1010,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_bounded_authoring_scope() {
|
||||
// Phase 5a/5b: the workflow-builder must be a Worker-tier leaf whose
|
||||
@@ -1135,6 +1143,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
|
||||
|
||||
@@ -53,6 +53,7 @@ pub mod emergency_stop;
|
||||
pub mod encryption;
|
||||
pub mod file_state;
|
||||
pub mod file_storage;
|
||||
#[cfg(feature = "flows")]
|
||||
pub mod flows;
|
||||
pub mod harness_init;
|
||||
pub mod health;
|
||||
@@ -102,6 +103,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;
|
||||
@@ -131,6 +133,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;
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -284,6 +284,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
|
||||
@@ -292,38 +293,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
|
||||
@@ -333,36 +349,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.
|
||||
@@ -1112,12 +1137,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
|
||||
{
|
||||
@@ -1130,6 +1158,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
|
||||
@@ -1196,13 +1228,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",
|
||||
@@ -1211,6 +1258,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
|
||||
|
||||
@@ -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).
|
||||
@@ -2303,3 +2340,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:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12595,6 +12595,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,
|
||||
@@ -12627,6 +12628,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",
|
||||
@@ -12649,6 +12651,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();
|
||||
@@ -12865,6 +12868,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();
|
||||
@@ -12935,6 +12939,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}"
|
||||
@@ -12975,6 +12980,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,
|
||||
@@ -13049,6 +13055,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(
|
||||
@@ -13057,6 +13064,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
|
||||
@@ -13300,6 +13308,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();
|
||||
@@ -13388,6 +13397,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();
|
||||
@@ -13498,6 +13508,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();
|
||||
@@ -13609,6 +13620,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();
|
||||
|
||||
Reference in New Issue
Block a user