diff --git a/AGENTS.md b/AGENTS.md index 21f0d8b797..0e93a7a51d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -251,7 +251,16 @@ Modules: `all`, `auth`, `cli`, `dispatch`, `event_bus/`, `jsonrpc`, `logging`, ` Two independent runtime axes on `CoreBuilder` (`src/core/runtime/builder.rs`): - **`ServiceSet`** selects which *background services / transports* run (`rpc_http`, `socketio`, `cron`, `channels`, `heartbeat`, …). Presets: `desktop()` / `headless_api()` / `none()`. -- **`DomainSet`** selects which *domain families* exist at runtime, one flag per `DomainGroup` (`src/core/all.rs`). Presets: `full()` (default — byte-identical to before #4796), `harness()` (agent + memory + threads + config + security only), `none()`. Every controller is tagged with its `DomainGroup` at the single registration site in `src/core/all.rs`; the live surface (controllers/`/schema`/dispatch, agent tools, stores, subscribers) is filtered by the ambient `CoreContext::domains()`. A gated domain's controllers become unknown-method, its agent tools absent, its stores/subscribers uninitialized. `examples/embed_headless.rs` uses `DomainSet::harness()`. Per-gate Cargo `[features]` (children #4797–#4804) narrow the compile-time surface further; `DomainSet` is the runtime axis they compose with. +- **`DomainSet`** selects which *domain families* exist at runtime, one flag per `DomainGroup` (`src/core/all.rs`). Presets: `full()` (default — byte-identical to before #4796), `harness()` (agent + memory + threads + config + security only), `none()`. Every controller is tagged with its `DomainGroup` at the single registration site in `src/core/all.rs`; the live surface (controllers/`/schema`/dispatch, agent tools, stores, subscribers) is filtered by the ambient `CoreContext::domains()`. A gated domain's controllers become unknown-method, its agent tools absent, its stores/subscribers uninitialized. `examples/embed_headless.rs` uses `DomainSet::harness()`; `examples/embed_kernel.rs` uses `DomainSet::kernel()` — the floor (threads + config + security, with `agent`/`memory` OFF) that a host opts subsystems back into by field assignment. Per-gate Cargo `[features]` (children #4797–#4804) narrow the compile-time surface further; `DomainSet` is the runtime axis they compose with. + +**`DomainGroup` tracks family directories 1:1.** After the domain reorg (#5328) each variant names a `src/openhuman/` family, so the runtime axis stopped sweeping half the surface into the `Platform` catch-all. Groups: the harness families (`Agent`, `Memory`, `Threads`, `Config`, `Security`), the compile-gate families (`Flows`, `Skills`, `Mcp`, `Meet`, `Channels`, `Web3`, `Voice`, `Media`, `Medulla`), the families carved out of `Platform` (`Inference`, `Integrations`, `Automation` = cron + subconscious, `Runtimes` = runtime + sandbox, `Desktop`, `Hosted`, `Relay` = tinyplace), and `Platform` itself — now only the kernel surfaces with no family of their own (`platform/`, `tools/`, `http_host/`, `test_support/`). + +That realignment fixed two real defects, both pinned by tests in `src/core/all_tests.rs`: + +- `harness()` claimed "agent + memory + threads + config + security" but silently dropped `agent::{agentbox, harness_init, artifacts, learning}`, `security::{credentials, devices}`, `config::{workspace, migration_helpers}`, `memory::people` and `skills::webhooks` into `Platform`. An agent harness that never registers `harness_init` is a latent bug. +- `embedded()` had to set `platform: true` purely to reach credentials and config, which dragged the desktop and hosted-backend surfaces along with it. Those are `Desktop` / `Hosted` now and stay off. + +**Adding a family directory means four edits, all compiler-enforced:** the `DomainGroup` variant (`src/core/all.rs`), the `DomainSet` field + `allows()` arm + every preset (`src/core/runtime/builder.rs`). Two more are *not* compiler-enforced and are the usual source of drift — `tool_group()` in `src/openhuman/tools/ops.rs` (a missing entry leaks a gated tool under a custom `DomainSet`; this is the #4808 review finding) and the `StoreInitPlan` / `DomainSubscriberPlan` keys (`runtime/context.rs`, `core/jsonrpc.rs`). Registering a controller whose store keys on a different group gives you a live RPC surface with no store behind it. ### Compile-time domain gates (Cargo `[features]`) diff --git a/docs/specs/2026-08-02-core-kernel-domain-reorg.md b/docs/specs/2026-08-02-core-kernel-domain-reorg.md index b1436bf5ef..c7e2dfc9b3 100644 --- a/docs/specs/2026-08-02-core-kernel-domain-reorg.md +++ b/docs/specs/2026-08-02-core-kernel-domain-reorg.md @@ -330,8 +330,17 @@ RPCs are unregistered and the `MemoryDiffTool` is absent. 2. Every family directory maps 1:1 to a gate or is declared kernel in this document. 3. `kernel-floor.limits` reaches 222 names / 2 native. 4. Each gate has both-ways tests in `src/core/all_tests.rs` and `tools/ops_tests.rs`. -5. `DomainGroup` gains at most four variants (`Integrations`, `Automation`, `Relay`, `Runtimes`); - the rest stay `Platform` at the runtime axis. `DomainSet::kernel()` exists, with - `examples/embed_kernel.rs`. +5. ✅ **Done.** `DomainGroup` gained seven variants, not four: `Inference`, `Integrations`, + `Automation` (cron + subconscious), `Runtimes` (runtime + sandbox), `Desktop`, `Hosted`, + `Relay` (tinyplace). The extra three over the original estimate are `Inference`, `Desktop` + and `Hosted` — carving those out is what lets `embedded()` stop setting `platform: true` + just to reach credentials and config. `Platform` now holds only `platform/`, `tools/`, + `http_host/`, `test_support/`. `DomainSet::kernel()` and `examples/embed_kernel.rs` exist; + the example runs and demonstrates memory-on / agent-unknown-method. + + The realignment also fixed two defects the flat tree had hidden: `harness()` was dropping + ten namespaces (including `harness_init`) into `Platform` despite claiming their families, + and `StoreInitPlan.people` keyed on `Platform` while its controllers moved to `Memory` — + which would have registered the people RPC surface with no store behind it. 6. Hand off to `kernel.md`'s subsystem registry (`src/core/subsystem/`, `Driver`, `Guard`, `subsystems_status`). diff --git a/examples/embed_kernel.rs b/examples/embed_kernel.rs new file mode 100644 index 0000000000..b36f959cd4 --- /dev/null +++ b/examples/embed_kernel.rs @@ -0,0 +1,81 @@ +//! Embed the OpenHuman core at its **kernel floor**, then opt one subsystem in. +//! +//! [`DomainSet::kernel`] is the smallest useful runtime surface: threads, +//! config, and security — the transport, dispatch, policy and identity a host +//! needs before it has decided what the core is *for*. Notably `agent` and +//! `memory` are OFF: they are the two largest subsystems and the ones an +//! alternative driver would replace, so a host that wants them says so. +//! +//! Contrast the two presets: +//! +//! - [`DomainSet::harness`] — kernel + `agent` + `memory`. The embeddable agent +//! core; see `examples/embed_headless.rs`. +//! - [`DomainSet::kernel`] — the floor. Start here when you want, say, memory +//! without the agent harness, or when you intend to bind your own driver to a +//! subsystem slot. +//! +//! Because `DomainSet` is a plain struct, opting a family back in is a field +//! assignment — no builder ceremony: +//! +//! ```ignore +//! let mut domains = DomainSet::kernel(); +//! domains.memory = true; // kernel + memory, nothing else +//! ``` +//! +//! What "off" means is uniform across every axis: the family's controllers are +//! unregistered (unknown-method over `/rpc`, absent from `/schema`), its agent +//! tools are absent from the tool list rather than present-and-failing, and its +//! stores and event-bus subscribers never initialize. +//! +//! Run with: +//! +//! ```bash +//! GGML_NATIVE=OFF cargo run --example embed_kernel +//! ``` + +use openhuman_core::{CoreBuilder, DomainSet, HostKind, ServiceSet}; + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + // Kernel floor plus exactly one subsystem: memory. The agent harness stays + // out, so this process can serve memory reads/writes without ever + // constructing an agent. + let mut domains = DomainSet::kernel(); + domains.memory = true; + + let runtime = CoreBuilder::new(HostKind::Cli) + .domains(domains) + .services(ServiceSet::none()) + .build() + .await?; + + // Always available — `core.*` is kernel transport, not a domain. + let version = runtime + .invoke("core.version", serde_json::json!({})) + .await + .map_err(|e| anyhow::anyhow!("core.version failed: {e}"))?; + println!("core.version -> {version}"); + + // Enabled: memory was opted in above. + match runtime + .invoke("openhuman.memory_list_namespaces", serde_json::json!({})) + .await + { + Ok(v) => println!("memory_list_namespaces -> {v}"), + Err(e) => println!("memory_list_namespaces -> error: {e:?}"), + } + + // Disabled: `agent` is off under kernel(), so this is an UNKNOWN METHOD — + // not a registered handler returning "agent disabled". Absence is the + // contract: a registered-but-failing method teaches a model the capability + // exists and makes it retry. + match runtime + .invoke("openhuman.agent_list_definitions", serde_json::json!({})) + .await + { + Ok(v) => println!("agent_list_definitions -> unexpectedly OK: {v}"), + Err(e) => println!("agent_list_definitions -> unknown method (expected): {e:?}"), + } + + Ok(()) +} diff --git a/src/core/all.rs b/src/core/all.rs index 2535b2fa12..e3234a67f1 100644 --- a/src/core/all.rs +++ b/src/core/all.rs @@ -64,6 +64,27 @@ impl RegisteredController { /// per-feature axes the child issues (#4797–#4804) additionally narrow at /// compile time. `Platform` is the catch-all for everything not in a named /// family — always on in `full()`, off in `harness()`/`none()`. +/// +/// **Groups track `src/openhuman/` family directories 1:1.** Before the domain +/// reorg (#5328) they could not: a capability lived across up to 13 sibling +/// top-level dirs, so half the controller surface was tagged `Platform` for want +/// of a family to name. That made two things wrong which are now fixed: +/// +/// - `harness()` claimed "agent + memory + threads + config + security" but +/// silently dropped `agent::{agentbox, harness_init, artifacts, learning}`, +/// `security::{credentials, devices}`, `config::{workspace, migration_helpers}`, +/// `memory::people` and `skills::webhooks`, all of which sat in `Platform`. +/// An agent harness that does not register `harness_init` is a latent bug. +/// - `embedded()` had to set `platform: true` purely to reach credentials and +/// config, which dragged in the desktop and hosted-backend surfaces it has no +/// use for. Those are now `Desktop` and `Hosted` and stay off. +/// +/// `Platform` is now what its name says: the kernel surfaces with no family of +/// their own (`platform/`, `tools/`, `http_host/`, `test_support/`). +/// +/// When adding a family directory, add the matching variant here, a field on +/// [`crate::core::runtime::DomainSet`], an arm in `allows()`, and an entry in +/// each preset — the compiler enforces all four. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum DomainGroup { // Harness families — on under `DomainSet::harness()`. @@ -91,6 +112,33 @@ pub enum DomainGroup { /// `medulla` (it folds that domain's envelopes). Splitting them would add /// drift surface for no reachable configuration. Medulla, + // Families carved out of the `Platform` catch-all once the domain reorg + // (#5328) gave each one a directory to be named after. Before that, half the + // controller surface was tagged `Platform` purely because there was no + // family to point at — which made `DomainSet::embedded()` set + // `platform: true` just to reach credentials/config/cron, dragging the + // desktop and hosted-backend surfaces along with it. + /// Model inference: providers, routing, local engines, embeddings, and the + /// token-compression surface (`inference/`). + Inference, + /// External connectors reached on the user's behalf — Composio, calendar, + /// file storage, task sources (`integrations/`). + Integrations, + /// Background initiative: scheduled jobs and the subconscious tick loop + /// (`cron/`, `subconscious/`). Pairs with `ServiceSet::{cron, heartbeat}`. + Automation, + /// Code-execution substrate: the managed Node/Python runtimes, the worker + /// pool, and the sandbox/CWD-jail confinement (`runtime/`, `sandbox/`). + Runtimes, + /// Desktop-shell-facing surfaces a headless or embedded host has no use for + /// (`desktop/`). + Desktop, + /// Clients of the hosted TinyHumans backend — billing, team, referral, + /// announcements, the hosted orchestration brain (`hosted/`). A self-hosted + /// build drops these as a unit. + Hosted, + /// The multi-agent relay surface (`tinyplace/`). + Relay, // Everything not in a named family — always on in `full()`, off otherwise. Platform, } @@ -206,13 +254,13 @@ fn build_registered_controllers() -> Vec { // AgentBox marketplace adapter status push( &mut controllers, - DomainGroup::Platform, + DomainGroup::Agent, crate::openhuman::agent::agentbox::all_agentbox_registered_controllers(), ); // Core application shell state push( &mut controllers, - DomainGroup::Platform, + DomainGroup::Desktop, crate::openhuman::desktop::app_state::all_app_state_registered_controllers(), ); // Audio generation + podcast-style email delivery (gated with voice). @@ -225,20 +273,20 @@ fn build_registered_controllers() -> Vec { // Composio integration controllers push( &mut controllers, - DomainGroup::Platform, + DomainGroup::Integrations, crate::openhuman::integrations::composio::all_composio_registered_controllers(), ); // Recall.ai Calendar V1 (backend-proxied) controllers push( &mut controllers, - DomainGroup::Platform, + DomainGroup::Integrations, crate::openhuman::integrations::recall_calendar::all_recall_calendar_registered_controllers( ), ); // Scheduled job management push( &mut controllers, - DomainGroup::Platform, + DomainGroup::Automation, crate::openhuman::cron::all_cron_registered_controllers(), ); // Saved automation workflows (tinyflows graphs): create/get/list/update/delete/run @@ -252,12 +300,12 @@ fn build_registered_controllers() -> Vec { // Proactive task ingestion from external tools (github/notion/linear/clickup) push( &mut controllers, - DomainGroup::Platform, + DomainGroup::Integrations, crate::openhuman::integrations::task_sources::all_task_sources_registered_controllers(), ); push( &mut controllers, - DomainGroup::Platform, + DomainGroup::Desktop, crate::openhuman::desktop::dashboard::all_dashboard_registered_controllers(), ); // MCP client subsystem: Smithery registry browser, local server install/connect, tool dispatch @@ -306,7 +354,7 @@ fn build_registered_controllers() -> Vec { // One-time first-run initialization (Python/spaCy/Node provisioning) push( &mut controllers, - DomainGroup::Platform, + DomainGroup::Agent, crate::openhuman::agent::harness_init::all_harness_init_registered_controllers(), ); // Diagnostic tools @@ -342,19 +390,19 @@ fn build_registered_controllers() -> Vec { // Interactive plan-review gate — parks a live turn on a thread-scoped plan push( &mut controllers, - DomainGroup::Security, + DomainGroup::Agent, crate::openhuman::agent::plan_review::all_plan_review_registered_controllers(), ); // Agent-generated artifact storage, retrieval, and lifecycle management push( &mut controllers, - DomainGroup::Platform, + DomainGroup::Agent, crate::openhuman::agent::artifacts::all_artifacts_registered_controllers(), ); // Background heartbeat loop controls push( &mut controllers, - DomainGroup::Platform, + DomainGroup::Automation, crate::openhuman::subconscious::heartbeat::all_heartbeat_registered_controllers(), ); // Ad-hoc static directory HTTP hosting for local file sharing / previews. @@ -409,7 +457,7 @@ fn build_registered_controllers() -> Vec { // User credentials and session management push( &mut controllers, - DomainGroup::Platform, + DomainGroup::Security, crate::openhuman::security::credentials::all_credentials_registered_controllers(), ); // Desktop service management @@ -421,43 +469,43 @@ fn build_registered_controllers() -> Vec { // Data migration utilities push( &mut controllers, - DomainGroup::Platform, + DomainGroup::Config, crate::openhuman::config::migration_helpers::all_migration_registered_controllers(), ); // Background command monitors for agent-scoped event sources push( &mut controllers, - DomainGroup::Platform, + DomainGroup::Automation, crate::openhuman::subconscious::monitors::all_monitor_registered_controllers(), ); // Unified inference domain: text / vision / local runtime / cloud providers. // (Formerly split across inference, local AI, and providers modules.) push( &mut controllers, - DomainGroup::Platform, + DomainGroup::Inference, crate::openhuman::inference::all_inference_registered_controllers(), ); push( &mut controllers, - DomainGroup::Platform, + DomainGroup::Inference, crate::openhuman::inference::all_local_inference_registered_controllers(), ); // Embedding provider configuration and embed RPC. push( &mut controllers, - DomainGroup::Platform, + DomainGroup::Inference, crate::openhuman::inference::embeddings::all_embeddings_registered_controllers(), ); // People resolution and interaction scoring push( &mut controllers, - DomainGroup::Platform, + DomainGroup::Memory, crate::openhuman::memory::people::all_people_registered_controllers(), ); // Sandbox execution backends (Docker, local jail, policy, cleanup) push( &mut controllers, - DomainGroup::Platform, + DomainGroup::Runtimes, crate::openhuman::sandbox::all_sandbox_registered_controllers(), ); // Backend Socket.IO bridge + related runtime plumbing @@ -469,7 +517,7 @@ fn build_registered_controllers() -> Vec { // Managed Node.js runtime bridge (tool listing + dispatch) push( &mut controllers, - DomainGroup::Platform, + DomainGroup::Runtimes, crate::openhuman::runtime::javascript::all_javascript_registered_controllers(), ); // Medulla integration: readiness, durable sessions, and the connected worker @@ -504,7 +552,7 @@ fn build_registered_controllers() -> Vec { // User workspace and file management push( &mut controllers, - DomainGroup::Platform, + DomainGroup::Config, crate::openhuman::config::workspace::all_workspace_registered_controllers(), ); // Workflow tool registry @@ -577,25 +625,25 @@ fn build_registered_controllers() -> Vec { // Referral and growth tracking push( &mut controllers, - DomainGroup::Platform, + DomainGroup::Hosted, crate::openhuman::hosted::referral::all_referral_registered_controllers(), ); // Billing and subscription management push( &mut controllers, - DomainGroup::Platform, + DomainGroup::Hosted, crate::openhuman::hosted::billing::all_billing_registered_controllers(), ); // Announcements surfaced on harness init push( &mut controllers, - DomainGroup::Platform, + DomainGroup::Hosted, crate::openhuman::hosted::announcements::all_announcements_registered_controllers(), ); // Team and role management push( &mut controllers, - DomainGroup::Platform, + DomainGroup::Hosted, crate::openhuman::hosted::team::all_team_registered_controllers(), ); // E2E test support — `openhuman.test_reset` wipes sidecar state in-place. @@ -623,7 +671,7 @@ fn build_registered_controllers() -> Vec { // Local assistive surfaces over third-party provider apps push( &mut controllers, - DomainGroup::Platform, + DomainGroup::Desktop, crate::openhuman::desktop::provider_surfaces::all_provider_surfaces_registered_controllers( ), ); @@ -637,19 +685,19 @@ fn build_registered_controllers() -> Vec { // Background awareness and autonomous tasks push( &mut controllers, - DomainGroup::Platform, + DomainGroup::Automation, crate::openhuman::subconscious::all_subconscious_registered_controllers(), ); push( &mut controllers, - DomainGroup::Platform, + DomainGroup::Automation, crate::openhuman::subconscious::triggers::all_subconscious_triggers_registered_controllers( ), ); // Webhook tunnel management push( &mut controllers, - DomainGroup::Platform, + DomainGroup::Skills, crate::openhuman::skills::webhooks::all_webhooks_registered_controllers(), ); // Core binary update management @@ -667,7 +715,7 @@ fn build_registered_controllers() -> Vec { // Self-learning and user context enrichment push( &mut controllers, - DomainGroup::Platform, + DomainGroup::Agent, crate::openhuman::agent::learning::all_learning_registered_controllers(), ); // Conversation thread and message management @@ -677,12 +725,13 @@ fn build_registered_controllers() -> Vec { crate::openhuman::threads::all_threads_registered_controllers(), ); // TokenJuice content-router debug controllers (detect / compress / cache_stats / retrieve). - // Classified Platform (always-on): TokenJuice is the token-compression content - // router that runs on every agent tool output, not a crypto surface — despite - // #4802 listing it under the web3 gate. Flagged for #4802 re-scope. + // Classified Inference: TokenJuice is the token-compression content router, + // not a crypto surface — despite #4802 listing it under the web3 gate. + // Only these debug/inspection controllers are gated; the content-router + // subscriber used on agent tool output remains always-on core infra. push( &mut controllers, - DomainGroup::Platform, + DomainGroup::Inference, crate::openhuman::inference::tokenjuice::all_tokenjuice_registered_controllers(), ); // Per-thread todo list (agent task board CRUD over RPC) @@ -694,7 +743,7 @@ fn build_registered_controllers() -> Vec { // Integration notification ingest, triage, and per-provider settings push( &mut controllers, - DomainGroup::Platform, + DomainGroup::Desktop, crate::openhuman::desktop::notifications::all_notifications_registered_controllers(), ); // Google Meet call-join request validation (shell handles the webview). @@ -729,7 +778,7 @@ fn build_registered_controllers() -> Vec { // Mobile device pairing and management push( &mut controllers, - DomainGroup::Platform, + DomainGroup::Security, crate::openhuman::security::devices::all_devices_registered_controllers(), ); // Durable agent session database — queryable index over transcripts, lineage, tool calls @@ -796,7 +845,7 @@ fn build_internal_only_controllers() -> Vec { // but NOT advertised to agents in tool listings or schema discovery. push( &mut controllers, - DomainGroup::Platform, + DomainGroup::Relay, crate::openhuman::tinyplace::all_tinyplace_registered_controllers(), ); // User-consented tiny.place pairing for wrapped agent sessions: UI-callable @@ -811,7 +860,7 @@ fn build_internal_only_controllers() -> Vec { // Renderer-only — not advertised to agents. push( &mut controllers, - DomainGroup::Agent, + DomainGroup::Hosted, crate::openhuman::hosted::orchestration::all_registered_controllers(), ); controllers diff --git a/src/core/all_tests.rs b/src/core/all_tests.rs index 175ea38998..98a3e5c9ec 100644 --- a/src/core/all_tests.rs +++ b/src/core/all_tests.rs @@ -986,6 +986,7 @@ 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)); + assert_eq!(group_for_namespace("plan_review"), Some(DomainGroup::Agent)); // …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. @@ -1258,3 +1259,162 @@ fn medulla_controllers_absent_when_feature_off() { "`medulla` must not register when the feature is off" ); } + +// ---- DomainGroup ↔ family-directory realignment ---------------------------- +// The reorg (#5328) made `src/openhuman/` one directory per family, so the +// runtime axis can finally name each one instead of sweeping half the surface +// into `Platform`. These pin that alignment in both directions. + +/// Every namespace whose family got carved out of `Platform` must now report its +/// own group. Before the realignment each of these answered `Platform`, so a +/// `DomainSet` that disabled the family still served its RPC surface. +#[test] +fn carved_out_families_report_their_own_group() { + let cases: &[(&str, DomainGroup)] = &[ + #[cfg(feature = "flows")] + ("flows", DomainGroup::Flows), + ("cron", DomainGroup::Automation), + ("heartbeat", DomainGroup::Automation), + ("composio", DomainGroup::Integrations), + ("task_sources", DomainGroup::Integrations), + ("billing", DomainGroup::Hosted), + ("team", DomainGroup::Hosted), + ("tinyplace", DomainGroup::Relay), + ("dashboard", DomainGroup::Desktop), + ("notification", DomainGroup::Desktop), + ("sandbox", DomainGroup::Runtimes), + // Mis-tagged before the realignment: these live inside a named family + // directory but answered `Platform`, so `harness()` registered nothing + // for them despite claiming to enable their family. + ("agentbox", DomainGroup::Agent), + ("harness_init", DomainGroup::Agent), + ("ai", DomainGroup::Agent), + ("auth", DomainGroup::Security), + ("devices", DomainGroup::Security), + ("workspace", DomainGroup::Config), + ("people", DomainGroup::Memory), + ]; + for (ns, want) in cases { + match group_for_namespace(ns) { + Some(got) => assert_eq!( + got, *want, + "namespace `{ns}` must be tagged {want:?}, got {got:?} — the DomainGroup \ + tag has drifted from the family directory it lives in" + ), + None => panic!("namespace `{ns}` is not registered; update this test if it moved"), + } + } +} + +/// `Platform` is now only the kernel surfaces with no family of their own. If a +/// namespace from a named family lands here, its `push(...)` tag was missed. +#[test] +fn platform_holds_only_kernel_surfaces() { + let platform: Vec<&str> = registry() + .iter() + .chain(internal_registry().iter()) + .filter(|g| g.group == DomainGroup::Platform) + .map(|g| g.controller.schema.namespace) + .collect(); + // Namespaces legitimately without a family: platform/, tools/, http_host/, + // test_support/. Anything else here is a missed tag. + for ns in &platform { + assert!( + !matches!( + *ns, + "cron" + | "heartbeat" + | "composio" + | "task_sources" + | "billing" + | "team" + | "referral" + | "announcements" + | "tinyplace" + | "dashboard" + | "notification" + | "sandbox" + | "agentbox" + | "harness_init" + | "ai" + | "auth" + | "devices" + | "workspace" + | "people" + ), + "namespace `{ns}` belongs to a named family but is still tagged Platform" + ); + } +} + +/// `harness()` claims agent + memory + threads + config + security. Before the +/// realignment it silently dropped several of their namespaces into `Platform`, +/// most damagingly `harness_init` — an agent harness that never runs harness +/// init. This asserts the claim is now true. +#[test] +fn harness_preset_registers_the_families_it_claims() { + let harness = crate::core::runtime::DomainSet::harness(); + for ns in [ + "agentbox", + "harness_init", + "ai", + "auth", + "devices", + "workspace", + "people", + ] { + let group = + group_for_namespace(ns).unwrap_or_else(|| panic!("namespace `{ns}` is not registered")); + assert!( + harness.allows(group), + "harness() must allow `{ns}` ({group:?}) — it is part of a harness family" + ); + } +} + +/// `kernel()` is the floor: threads/config/security only. It must NOT pull in +/// the two big replaceable subsystems, nor any carved-out family. +#[test] +fn kernel_preset_is_the_floor() { + let k = crate::core::runtime::DomainSet::kernel(); + assert!( + k.threads && k.config && k.security, + "kernel keeps the floor" + ); + assert!( + !k.agent && !k.memory, + "kernel() must not enable agent/memory — a host opts those in explicitly" + ); + for (name, on) in [ + ("inference", k.inference), + ("integrations", k.integrations), + ("automation", k.automation), + ("runtimes", k.runtimes), + ("desktop", k.desktop), + ("hosted", k.hosted), + ("relay", k.relay), + ("platform", k.platform), + ] { + assert!(!on, "kernel() must leave `{name}` off"); + } +} + +/// An embedded host supplies its own UI and never dials the hosted backend. +/// Before the realignment `embedded()` had to set `platform: true` to reach +/// credentials/config, which dragged both surfaces in. +#[test] +fn embedded_preset_excludes_desktop_and_hosted() { + let e = crate::core::runtime::DomainSet::embedded(); + assert!(!e.desktop, "embedded() must not enable desktop surfaces"); + assert!( + !e.hosted, + "embedded() must not enable hosted-backend clients" + ); + assert!(!e.relay, "embedded() must not enable the relay surface"); + // Still needs these: skills run on the managed runtimes, and the session + // loop is driven by cron/heartbeat. + assert!(e.runtimes, "embedded() needs the code-execution runtimes"); + assert!(e.automation, "embedded() needs cron + subconscious"); + assert!(e.inference, "embedded() needs inference"); + assert!(e.integrations, "embedded() needs external integrations"); +} diff --git a/src/core/jsonrpc.rs b/src/core/jsonrpc.rs index 43dd4ebd98..770889d410 100644 --- a/src/core/jsonrpc.rs +++ b/src/core/jsonrpc.rs @@ -1892,8 +1892,18 @@ async fn run_server_with_services( /// always registered as core/platform infra and intentionally absent here. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct DomainSubscriberPlan { - /// webhook + notification-bridge + composio trigger + task-sources + device-tunnel. + /// Reserved for subscribers with no family of their own. Currently none: + /// the reorg gave every subscriber that used to live here a real family + /// (see the fields below), so this stays for future kernel-level ones. pub platform: bool, + /// composio trigger archive + trigger subscriber + task-sources poller. + pub integrations: bool, + /// device tunnel handshake/peer-status subscriber. + pub security: bool, + /// notification bridge (desktop-shell delivery). + pub desktop: bool, + /// webhook request subscriber (skills own inbound webhook routing). + pub skills: bool, /// channel-inbound + web-only proactive. pub channels: bool, /// flows trigger dispatch. @@ -1904,6 +1914,8 @@ pub struct DomainSubscriberPlan { pub meet: bool, /// agent handlers + background delivery + run-ledger finalizer + orchestration ingest. pub agent: bool, + /// hosted orchestration ingest. + pub hosted: bool, /// mcp::registry lifecycle bus init. pub mcp: bool, } @@ -1914,11 +1926,16 @@ impl DomainSubscriberPlan { use crate::core::all::DomainGroup; Self { platform: domains.allows(DomainGroup::Platform), + integrations: domains.allows(DomainGroup::Integrations), + security: domains.allows(DomainGroup::Security), + desktop: domains.allows(DomainGroup::Desktop), + skills: domains.allows(DomainGroup::Skills), channels: domains.allows(DomainGroup::Channels), flows: domains.allows(DomainGroup::Flows), memory: domains.allows(DomainGroup::Memory), meet: domains.allows(DomainGroup::Meet), agent: domains.allows(DomainGroup::Agent), + hosted: domains.allows(DomainGroup::Hosted), mcp: domains.allows(DomainGroup::Mcp), } } @@ -1977,6 +1994,23 @@ fn register_domain_subscribers( .insert(group) } + /// Learning subscribers need their own idempotency token rather than + /// `group_first_time(DomainGroup::Agent)`: the Agent block below already + /// consumes that token, and whichever ran second would silently skip. + fn learning_first_time() -> bool { + static DONE: OnceLock> = OnceLock::new(); + let mut done = DONE + .get_or_init(|| Mutex::new(false)) + .lock() + .expect("learning-subscriber registry lock poisoned"); + if *done { + false + } else { + *done = true; + true + } + } + // Seed the live tool-execution timeout from the persisted `[agent]` config // so a user-configured value (Settings → Agent OS access → Action timeout) // is in effect from the first tool call. `OPENHUMAN_TOOL_TIMEOUT_SECS`, when @@ -2083,10 +2117,10 @@ fn register_domain_subscribers( // ---- Gated domain subscribers — each group installed at most once, the // first time its owning DomainGroup is enabled. ------------------------- - // Platform: webhook + notification bridge + composio trigger + task-sources - // proactive ingestion + device tunnel. - if plan.platform { - if group_first_time(DomainGroup::Platform) { + // Carved-out families: webhook (Skills), notification bridge (Desktop), + // composio + task-sources (Integrations), and device tunnel (Security). + if plan.skills { + if group_first_time(DomainGroup::Skills) { if let Some(handle) = crate::core::event_bus::subscribe_global(Arc::new( crate::openhuman::skills::webhooks::bus::WebhookRequestSubscriber::new(), )) { @@ -2096,9 +2130,23 @@ fn register_domain_subscribers( "[event_bus] failed to register webhook subscriber — bus not initialized" ); } + } + } else { + log::debug!("[event_bus] webhook subscriber SKIPPED — Skills domain disabled"); + } + + if plan.desktop { + if group_first_time(DomainGroup::Desktop) { crate::openhuman::desktop::notifications::register_notification_bridge_subscriber( config.clone(), ); + } + } else { + log::debug!("[event_bus] notification bridge SKIPPED — Desktop domain disabled"); + } + + if plan.integrations { + if group_first_time(DomainGroup::Integrations) { if let Err(error) = crate::openhuman::integrations::composio::init_composio_trigger_history( workspace_dir.clone(), @@ -2108,24 +2156,39 @@ fn register_domain_subscribers( } crate::openhuman::integrations::composio::register_composio_trigger_subscriber(); crate::openhuman::integrations::task_sources::bus::register_task_sources_subscriber(); + } + } else { + log::debug!( + "[event_bus] composio + task-sources subscribers SKIPPED — Integrations domain disabled" + ); + } + + if plan.security { + if group_first_time(DomainGroup::Security) { // Device tunnel subscriber: handles tunnel:frame handshakes, // peer-status events, and register acks. Must be live before any // tunnel:frame events can arrive. crate::openhuman::security::devices::bus::register_device_tunnel_subscriber(); + } + } else { + log::debug!("[event_bus] device-tunnel subscriber SKIPPED — Security domain disabled"); + } + + if plan.agent { + if learning_first_time() { // Always-on learning subscribers (email-signature producer, rebuild // trigger + periodic loop, ProfileMdRenderer). Previously wired only // in `channels::runtime::startup::start_channels`, which is skipped // when no channel is configured — silently dropping ALL learning for - // channel-less users (#5003). Registered here on the unconditional - // Platform boot path; idempotent, so it never double-registers. + // channel-less users (#5003). `agent::learning` is an Agent-family + // domain; it sat on the Platform boot path only because `learning` + // used to be a top-level directory. Idempotent. crate::openhuman::agent::learning::startup::register_learning_subscribers( workspace_dir.clone(), ); } } else { - log::debug!( - "[event_bus] Platform subscribers (webhook/notification/composio/task-sources/device-tunnel) SKIPPED — Platform domain disabled" - ); + log::debug!("[event_bus] learning subscribers SKIPPED — Agent domain disabled"); } // Channels: inbound dispatch + web-only proactive messaging. @@ -2252,12 +2315,19 @@ fn register_domain_subscribers( log::debug!("[event_bus] agent_meetings subscribers SKIPPED — Meet domain disabled"); } - // Agent: orchestration ingest + native agent handlers + background-completion - // delivery + run-ledger finalizer. + // Hosted: ingest tiny.place harness session DMs off the stream bus. + if plan.hosted { + if group_first_time(DomainGroup::Hosted) { + crate::openhuman::hosted::orchestration::register_orchestration_ingest_subscriber(); + } + } else { + log::debug!("[event_bus] orchestration ingest SKIPPED — Hosted domain disabled"); + } + + // Agent: native agent handlers + background-completion delivery + + // run-ledger finalizer. if plan.agent { if group_first_time(DomainGroup::Agent) { - // Orchestration: ingest tiny.place harness session DMs off the stream bus. - crate::openhuman::hosted::orchestration::register_orchestration_ingest_subscriber(); // Native request handlers — the agent `agent.run_turn` handler is // what channel dispatch calls instead of importing // `run_tool_call_loop` directly. @@ -2276,7 +2346,7 @@ fn register_domain_subscribers( } } else { log::debug!( - "[event_bus] agent handlers + background delivery + run-ledger finalizer + orchestration ingest SKIPPED — Agent domain disabled" + "[event_bus] agent handlers + background delivery + run-ledger finalizer SKIPPED — Agent domain disabled" ); } @@ -2325,7 +2395,12 @@ pub async fn bootstrap_core_runtime( // --- Event bus bootstrap --- // Ensure the global event bus is initialized (no-op if already done by start_channels). crate::core::event_bus::init_global(crate::core::event_bus::DEFAULT_CAPACITY); - crate::openhuman::agent::file_state::init_global(); + let agent_enabled = domains.allows(crate::core::all::DomainGroup::Agent); + if agent_enabled { + crate::openhuman::agent::file_state::init_global(); + } else { + log::debug!("[boot] agent file-state coordinator SKIPPED — Agent domain disabled"); + } // Register domain subscribers for cross-module event handling. Ungated infra // runs once (INFRA: Once) and each DomainGroup installs at most once via the // per-group `group_first_time` set, so repeated calls to @@ -2360,21 +2435,21 @@ pub async fn bootstrap_core_runtime( // at boot is orphaned — its driver died without firing a terminal event, so // the finalizer never settled it. Stamp such rows `interrupted` so they stop // rendering as perpetual "running" timeline entries on thread reopen. - match crate::openhuman::agent::session_db::run_ledger::interrupt_orphaned_agent_runs(&cfg) { - Ok(0) => {} - Ok(count) => log::info!("[runtime] settled {count} orphaned agent run(s) on startup"), - Err(err) => log::warn!("[runtime] failed to settle orphaned agent runs: {err}"), - } + if agent_enabled { + match crate::openhuman::agent::session_db::run_ledger::interrupt_orphaned_agent_runs(&cfg) { + Ok(0) => {} + Ok(count) => log::info!("[runtime] settled {count} orphaned agent run(s) on startup"), + Err(err) => log::warn!("[runtime] failed to settle orphaned agent runs: {err}"), + } - // --- Detached sub-agent TaskStore reconciliation ------------------- - // The durable orchestration TaskStore (`/.openhuman/ - // orchestration_tasks.jsonl`) can hold non-terminal sub-agent records left - // by a previous process — their detached executor (abort handle + - // cooperative CancellationToken) died with that process, so they cannot be - // re-attached. Reconcile each orphan to a terminal state and emit the typed - // terminal lifecycle event so the run ledger finalizes. Best-effort and - // non-fatal (issue #4249 / 07.2 steps 2 & 4). - { + // --- Detached sub-agent TaskStore reconciliation ------------------- + // The durable orchestration TaskStore (`/.openhuman/ + // orchestration_tasks.jsonl`) can hold non-terminal sub-agent records left + // by a previous process — their detached executor (abort handle + + // cooperative CancellationToken) died with that process, so they cannot be + // re-attached. Reconcile each orphan to a terminal state and emit the typed + // terminal lifecycle event so the run ledger finalizes. Best-effort and + // non-fatal (issue #4249 / 07.2 steps 2 & 4). let reconciled = crate::openhuman::agent::orchestration::running_subagents::reconcile_orphaned_tasks_on_boot( &workspace_dir, @@ -2384,6 +2459,10 @@ pub async fn bootstrap_core_runtime( "[runtime] reconciled {reconciled} orphaned detached sub-agent task(s) on startup" ); } + } else { + log::debug!( + "[boot] agent run-ledger + orchestration task reconciliation SKIPPED — Agent domain disabled" + ); } // --- Cost dashboard tracker --- @@ -2409,13 +2488,17 @@ pub async fn bootstrap_core_runtime( // Loads built-in archetype definitions plus any custom TOML files // under `/agents/*.toml`. Idempotent — safe to call // multiple times. Uses the per-user scoped workspace_dir. - if let Err(err) = - crate::openhuman::agent::harness::AgentDefinitionRegistry::init_global(&workspace_dir) - { - log::warn!( - "[runtime] AgentDefinitionRegistry::init_global failed: {err} — \ - spawn_subagent will be unavailable until restart" - ); + if agent_enabled { + if let Err(err) = + crate::openhuman::agent::harness::AgentDefinitionRegistry::init_global(&workspace_dir) + { + log::warn!( + "[runtime] AgentDefinitionRegistry::init_global failed: {err} — \ + spawn_subagent will be unavailable until restart" + ); + } + } else { + log::debug!("[boot] agent definition registry SKIPPED — Agent domain disabled"); } // --- Agent sandbox + projects dirs --- diff --git a/src/core/jsonrpc_tests.rs b/src/core/jsonrpc_tests.rs index 99b944d13b..9672129ab4 100644 --- a/src/core/jsonrpc_tests.rs +++ b/src/core/jsonrpc_tests.rs @@ -31,11 +31,16 @@ fn domain_subscriber_plan_full_registers_every_gated_subscriber() { plan, DomainSubscriberPlan { platform: true, + integrations: true, + security: true, + desktop: true, + skills: true, channels: true, flows: true, memory: true, meet: true, agent: true, + hosted: true, mcp: true, }, "full() must register every gated domain subscriber" @@ -49,11 +54,16 @@ fn domain_subscriber_plan_none_registers_no_gated_subscriber() { plan, DomainSubscriberPlan { platform: false, + integrations: false, + security: false, + desktop: false, + skills: false, channels: false, flows: false, memory: false, meet: false, agent: false, + hosted: false, mcp: false, }, "none() must register no gated domain subscriber (core infra still runs, ungated)" @@ -64,10 +74,7 @@ fn domain_subscriber_plan_none_registers_no_gated_subscriber() { fn domain_subscriber_plan_harness_gates_by_owning_group() { let plan = DomainSubscriberPlan::for_domains(crate::core::runtime::DomainSet::harness()); // harness() = agent + memory + threads + config + security. - assert!( - plan.agent, - "harness keeps agent + orchestration subscribers" - ); + assert!(plan.agent, "harness keeps agent subscribers"); assert!( plan.memory, "harness keeps memory conversation-persistence + sync bridge" @@ -83,6 +90,10 @@ fn domain_subscriber_plan_harness_gates_by_owning_group() { ); assert!(!plan.flows, "harness must skip flows trigger dispatch"); assert!(!plan.meet, "harness must skip agent_meetings subscribers"); + assert!( + !plan.hosted, + "harness must skip hosted orchestration ingest" + ); assert!(!plan.mcp, "harness must skip mcp_registry bus init"); } diff --git a/src/core/runtime/builder.rs b/src/core/runtime/builder.rs index 931aa47abd..312af93c5f 100644 --- a/src/core/runtime/builder.rs +++ b/src/core/runtime/builder.rs @@ -208,6 +208,20 @@ pub struct DomainSet { /// Medulla integration: cloud client, session runtime, chat store, and /// authored harness workflows. pub medulla: bool, + /// Model inference: providers, routing, local engines, embeddings. + pub inference: bool, + /// External connectors (Composio, calendar, file storage, task sources). + pub integrations: bool, + /// Background initiative: cron + the subconscious tick loop. + pub automation: bool, + /// Code-execution substrate: Node/Python runtimes, pool, sandbox. + pub runtimes: bool, + /// Desktop-shell-facing surfaces. + pub desktop: bool, + /// Clients of the hosted TinyHumans backend. + pub hosted: bool, + /// The multi-agent relay surface (tinyplace). + pub relay: bool, /// Everything not in a named family — always on in `full()`. pub platform: bool, } @@ -231,6 +245,13 @@ impl DomainSet { voice: true, media: true, medulla: true, + inference: true, + integrations: true, + automation: true, + runtimes: true, + desktop: true, + hosted: true, + relay: true, platform: true, } } @@ -254,13 +275,20 @@ impl DomainSet { voice: false, media: false, medulla: false, + inference: false, + integrations: false, + automation: false, + runtimes: false, + desktop: false, + hosted: false, + relay: false, platform: false, } } /// A long-lived embedded host: the harness core plus the Medulla - /// integration and the workflow engine it runs on, and `platform` for the - /// credentials / config / cron / task-source domains such a session needs. + /// integration and the workflow engine it runs on, and the supporting + /// runtime, automation, integration, and platform surfaces it needs. /// /// Named for the *shape* rather than any downstream consumer — the core /// does not know which host embeds it, and a preset naming one would invert @@ -294,10 +322,53 @@ impl DomainSet { voice: false, media: false, medulla: true, + inference: true, + integrations: true, + automation: true, + runtimes: true, + desktop: false, + hosted: false, + relay: false, platform: true, } } + /// The kernel floor: threads, config, security — and nothing else. + /// + /// Distinct from [`DomainSet::none`], which is "no domains at all". This is + /// "the minimum a host needs before opting a subsystem back in", so an + /// embedder can request kernel + exactly one family. `agent` and `memory` + /// are OFF on purpose: they are the two largest subsystems and the ones an + /// alternative driver would replace, so a host that wants them says so. + /// + /// See `examples/embed_kernel.rs`. + pub fn kernel() -> Self { + Self { + agent: false, + memory: false, + threads: true, + config: true, + security: true, + flows: false, + skills: false, + mcp: false, + meet: false, + channels: false, + web3: false, + voice: false, + media: false, + medulla: false, + inference: false, + integrations: false, + automation: false, + runtimes: false, + desktop: false, + hosted: false, + relay: false, + platform: false, + } + } + /// Nothing on — every family disabled. pub fn none() -> Self { Self { @@ -315,6 +386,13 @@ impl DomainSet { voice: false, media: false, medulla: false, + inference: false, + integrations: false, + automation: false, + runtimes: false, + desktop: false, + hosted: false, + relay: false, platform: false, } } @@ -336,6 +414,13 @@ impl DomainSet { DomainGroup::Voice => self.voice, DomainGroup::Media => self.media, DomainGroup::Medulla => self.medulla, + DomainGroup::Inference => self.inference, + DomainGroup::Integrations => self.integrations, + DomainGroup::Automation => self.automation, + DomainGroup::Runtimes => self.runtimes, + DomainGroup::Desktop => self.desktop, + DomainGroup::Hosted => self.hosted, + DomainGroup::Relay => self.relay, DomainGroup::Platform => self.platform, } } @@ -754,6 +839,7 @@ mod tests { DomainGroup::Voice, DomainGroup::Media, DomainGroup::Medulla, + DomainGroup::Integrations, DomainGroup::Platform, ] { assert!(full.allows(group), "full() must allow {group:?}"); @@ -859,8 +945,8 @@ mod tests { #[test] fn embedded_is_not_harness_plus_medulla() { // Guards the most tempting future "simplification": deriving this - // preset from harness(). harness() sets platform:false, which drops - // credentials/config/cron/task_sources/todos. + // preset from harness(), which leaves the supporting Platform, + // Channels, and Integrations families off. let harness = DomainSet::harness(); let tui = DomainSet::embedded(); @@ -868,6 +954,8 @@ mod tests { assert!(tui.allows(DomainGroup::Platform)); assert!(!harness.allows(DomainGroup::Channels)); assert!(tui.allows(DomainGroup::Channels)); + assert!(!harness.allows(DomainGroup::Integrations)); + assert!(tui.allows(DomainGroup::Integrations)); } #[test] diff --git a/src/core/runtime/context.rs b/src/core/runtime/context.rs index 4da37319c3..5931498aa0 100644 --- a/src/core/runtime/context.rs +++ b/src/core/runtime/context.rs @@ -309,7 +309,12 @@ pub struct StoreInitPlan { pub memory: bool, /// `agent::multimodal` attachments sidecar dir — gated on [`DomainGroup::Agent`]. pub agent_attachments: bool, - /// `people::store` — gated on [`DomainGroup::Platform`]. + /// `memory::people::store` — gated on [`DomainGroup::Memory`]. + /// + /// Was `Platform` while `people` was a top-level domain. The reorg moved it + /// to `memory/people` and its controllers are tagged `Memory`; leaving the + /// store on `Platform` would register those controllers under `harness()` + /// with no store behind them. pub people: bool, /// legacy-workflow prune under `skills::registry` — gated on [`DomainGroup::Skills`]. pub skills_prune: bool, @@ -322,7 +327,7 @@ impl StoreInitPlan { Self { memory: domains.allows(DomainGroup::Memory), agent_attachments: domains.allows(DomainGroup::Agent), - people: domains.allows(DomainGroup::Platform), + people: domains.allows(DomainGroup::Memory), skills_prune: domains.allows(DomainGroup::Skills), } } @@ -391,7 +396,7 @@ pub async fn init_stores( Err(e) => log::warn!("[boot] people::store init failed: {e}"), } } else { - log::debug!("[boot] people::store init SKIPPED — Platform domain disabled"); + log::debug!("[boot] people::store init SKIPPED — Memory domain disabled"); } // Prune legacy bundled skills (dev-workflow / github-issue-crusher // / pr-review-shepherd) that older builds seeded into @@ -482,8 +487,16 @@ mod tests { plan.agent_attachments, "harness keeps agent attachments sidecar (Agent)" ); - // Platform / Skills are NOT in harness → their stores stay off. - assert!(!plan.people, "harness must skip people::store (Platform)"); + // `people` moved to `memory/people` in the domain reorg (#5328) and its + // controllers are tagged `Memory`, so harness — which enables Memory — + // must now initialize its store too. Before the realignment it keyed on + // `Platform`, which meant harness registered the people controllers with + // no store behind them. + assert!( + plan.people, + "harness keeps memory::people::store (Memory) — it moved under memory/" + ); + // Skills is NOT in harness → its store work stays off. assert!( !plan.skills_prune, "harness must skip skills legacy-prune (Skills)" diff --git a/src/openhuman/tools/ops.rs b/src/openhuman/tools/ops.rs index 55735bc725..b42f4adaf9 100644 --- a/src/openhuman/tools/ops.rs +++ b/src/openhuman/tools/ops.rs @@ -1271,13 +1271,9 @@ pub fn all_tools_with_runtime( /// by its `name()`, so [`all_tools_with_runtime`] can drop tools whose family is /// disabled under the ambient [`DomainSet`](crate::core::runtime::DomainSet). /// -/// Only the gate families (Web3/Mcp/Skills/Flows/Media/Voice/Meet) and the two -/// mapped harness families (Memory/Threads) are matched; **everything else -/// defaults to `Platform`**. Consequence under `harness()` (platform off): the -/// gate-family tools drop AND the generic Platform tools (shell/file/grep/edit/ -/// screen/billing/team/cron/config/security/agent-orchestration/…) drop too — -/// only memory + thread/todo tools remain. This is the strict #4796 harness -/// surface; an embedder that wants a broader tool set can widen its DomainSet. +/// Named-family tools are matched here; everything without a domain family +/// defaults to `Platform`. Under `harness()`, the Agent/Memory/Threads/Config/ +/// Security tools remain while gate-family and generic Platform tools drop. /// (Names verified against each Tool impl's `fn name()` on 2026-07-13.) fn tool_group(name: &str) -> crate::core::all::DomainGroup { use crate::core::all::DomainGroup; @@ -1359,6 +1355,14 @@ fn tool_group(name: &str) -> crate::core::all::DomainGroup { "audio_generate_and_email_podcast", ]; // Threads: thread_* / todo_* handled by prefix below; these are the extras. + // Subconscious monitor + proactive-notify tools (Automation family). + const MONITORS: &[&str] = &[ + "monitor", + "monitor_list", + "monitor_read", + "monitor_stop", + "notify_user", + ]; const THREADS_EXTRA: &[&str] = &["transcript_search", "goal_get", "goal_set", "goal_complete"]; // Memory extras not covered by the `memory_`/`goals_` prefixes. const MEMORY_EXTRA: &[&str] = &[ @@ -1414,8 +1418,97 @@ fn tool_group(name: &str) -> crate::core::all::DomainGroup { if name.starts_with("thread_") || name.starts_with("todo_") || THREADS_EXTRA.contains(&name) { return DomainGroup::Threads; } - // Everything else — shell/file/config/security/agent/billing/… — is - // Platform: present under full(), absent under harness()/none(). + // Harness families realigned out of Platform. + if name.starts_with("artifact_") + || name.starts_with("learning_") + || name.contains("subagent") + || matches!( + name, + "ask_user_clarification" + | "agent_prepare_context" + | "delegate" + | "delegate_graph" + | "delegate_to_personality" + | "todo" + | "update_task" + | "wait" + | "wait_loop" + | "request_plan_review" + | "plan_exit" + | "spawn_parallel_agents" + ) + { + return DomainGroup::Agent; + } + if name.starts_with("config_") || name.starts_with("workspace_") { + return DomainGroup::Config; + } + if name.starts_with("people_") { + return DomainGroup::Memory; + } + if name.starts_with("security_") + || name.starts_with("credential_") + || name.starts_with("session_") + || name.starts_with("oauth_") + { + return DomainGroup::Security; + } + // ── Families carved out of Platform by the DomainGroup realignment ────── + // Each of these previously fell through to Platform, which meant the tool + // stayed callable when its family was gated off under a custom DomainSet — + // the leak the #4808 review flagged for whatsapp_data. Keep these in + // lockstep with the `push(...)` tags in `core::all`. + // + // Automation: scheduled jobs (`cron_*`) plus the subconscious monitor + + // proactive-notify surface. + if name.starts_with("cron_") || name == "schedule" || MONITORS.contains(&name) { + return DomainGroup::Automation; + } + // Integrations: every external connector reached on the user's behalf. + if name.starts_with("composio") + || name == "web_search_tool" + || name.starts_with("tinyfish_") + || name.starts_with("exa_") + || name.starts_with("brave_") + || name.starts_with("parallel_") + || name.starts_with("querit_") + || name.starts_with("apify_") + || name.starts_with("google_places_") + || name.starts_with("stock_") + || name == "polymarket" + || name.starts_with("storage_") + || name.starts_with("task_source_") + || name == "twilio_call" + { + return DomainGroup::Integrations; + } + // Hosted: clients of the TinyHumans backend. + if name.starts_with("billing_") + || name.starts_with("referral_") + || name.starts_with("team_") + || name.starts_with("orchestration_") + { + return DomainGroup::Hosted; + } + // Relay: the multi-agent relay surface. + if name.starts_with("tinyplace_") { + return DomainGroup::Relay; + } + // Desktop: shell-facing surfaces. + if name.starts_with("dashboard_") { + return DomainGroup::Desktop; + } + // Runtimes: the managed Node/Python execution tools. These live under + // `tools/impl/system/` rather than `runtime/`, so they are matched by name. + if name == "node_exec" || name == "npm_exec" || name == "python_exec" { + return DomainGroup::Runtimes; + } + // Inference: the token-compression retrieval surface. + if name.starts_with("tokenjuice_") { + return DomainGroup::Inference; + } + // Everything else — shell/file and other kernel utilities — is Platform: + // present under full(), absent under harness()/none(). DomainGroup::Platform } diff --git a/src/openhuman/tools/ops_tests.rs b/src/openhuman/tools/ops_tests.rs index 4cc9035e32..df5ec36e2e 100644 --- a/src/openhuman/tools/ops_tests.rs +++ b/src/openhuman/tools/ops_tests.rs @@ -2563,12 +2563,43 @@ fn tool_group_classifies_gate_and_harness_families() { assert_eq!(tool_group("thread_list"), DomainGroup::Threads); assert_eq!(tool_group("todo_add"), DomainGroup::Threads); assert_eq!(tool_group("goal_get"), DomainGroup::Threads); + assert_eq!(tool_group("artifact_list"), DomainGroup::Agent); + assert_eq!(tool_group("learning_list_facets"), DomainGroup::Agent); + assert_eq!(tool_group("spawn_subagent"), DomainGroup::Agent); + for name in [ + "ask_user_clarification", + "wait", + "wait_loop", + "delegate", + "todo", + "update_task", + "spawn_parallel_agents", + ] { + assert_eq!(tool_group(name), DomainGroup::Agent); + } + assert_eq!(tool_group("people_list"), DomainGroup::Memory); + assert_eq!(tool_group("config_snapshot"), DomainGroup::Config); + assert_eq!(tool_group("workspace_init"), DomainGroup::Config); + assert_eq!(tool_group("security_policy_info"), DomainGroup::Security); + assert_eq!(tool_group("credential_list"), DomainGroup::Security); + assert_eq!(tool_group("session_state"), DomainGroup::Security); + assert_eq!(tool_group("oauth_list"), DomainGroup::Security); + assert_eq!(tool_group("schedule"), DomainGroup::Automation); + assert_eq!(tool_group("polymarket"), DomainGroup::Integrations); + for name in [ + "web_search_tool", + "tinyfish_search", + "exa_get_contents", + "brave_news_search", + "parallel_search", + "querit_search", + ] { + assert_eq!(tool_group(name), DomainGroup::Integrations); + } // Everything else → Platform (dropped under harness()). assert_eq!(tool_group("shell"), DomainGroup::Platform); assert_eq!(tool_group("file_read"), DomainGroup::Platform); - assert_eq!(tool_group("config_snapshot"), DomainGroup::Platform); - assert_eq!(tool_group("spawn_subagent"), DomainGroup::Platform); } #[test] @@ -2584,6 +2615,9 @@ fn tool_group_gate_families_dropped_under_harness_not_full() { // Harness keeps memory/threads, drops gate families AND platform. assert!(harness.allows(tool_group("memory_store"))); assert!(harness.allows(tool_group("thread_list"))); + assert!(harness.allows(tool_group("artifact_list"))); + assert!(harness.allows(tool_group("config_snapshot"))); + assert!(harness.allows(tool_group("security_policy_info"))); assert!(!harness.allows(tool_group("wallet_status"))); assert!(!harness.allows(tool_group("run_workflow"))); assert!(!harness.allows(tool_group("shell")));