Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci-lite.yml
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,7 @@ jobs:
#
# This asserts the calibration still holds. If it fails, every
# projection built on the simulator is suspect until it is fixed.
run: python3 scripts/dep-sim.py --cut-nothing --expect-names 286
run: python3 scripts/dep-sim.py --cut-nothing --expect-names 282

- name: Guard — new feature-gated test modules must be acknowledged
# Self-maintaining coverage: the set of source files that #[cfg]-gate a test on
Expand Down
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,7 @@ whole cohort or expect a delta of 0.
| `tui` | ON | `openhuman::tui` — the tabbed ratatui/crossterm CLI UI (Logs, Chat, Config, Settings), auto-opened by bare `openhuman` on interactive non-container hosts and forced with `openhuman tui` (alias `chat`). Runs the core in-process. No controllers, no agent tools. **Intentionally NOT forwarded to the desktop shell** (allowlisted in `check-feature-forwarding.mjs`). | `ratatui`, `crossterm` |
| `channels` | ON | `openhuman::channels` (external-messaging providers — Telegram/Discord/Slack/Signal/WhatsApp/iMessage/IRC/… — plus the channel runtime, controllers, host, proactive messaging + inbound dispatch) and the `channels::webview_accounts` / `webview_apis` / `webview_notifications` / `channels::whatsapp_data` webview-bridge domains (incl. the 3 `whatsapp_data_*` agent tools). **Carve-outs `channels::{traits, cli}` stay ungated.** | **28** via `tinychannels/{email,lark}` — the crate itself stays (load-bearing), its two heavy providers do not |
| `contacts` | ON | `memory::people::address_book`'s macOS CNContactStore reader — the address-book seeding path for the people domain. Leaf gate over a **pre-existing** off-state: the module already shipped a non-macOS `imp` stub returning an empty contact list, so the gate only widens that stub's cfg. `read`/`read_with`/`AddressBookError`/`SystemContactsSource` and the whole `people` RPC surface stay compiled in every build; off ⇒ a refresh seeds nothing instead of failing. | **6** on macOS (`objc2`, `objc2-foundation`, `objc2-contacts`, `block2` + 2 transitive). **No-op on Linux/Windows** — never in those graphs, so the kernel-floor ratchet does not move. Verify cross-target: `cargo tree --target aarch64-apple-darwin -e normal -i objc2-contacts --no-default-features` (294 → 288 packages). |
| `runtime-node` | ON | `runtime::node` (download / verify / extract / install a pinned Node.js toolchain), the `runtime::javascript` language slot, `runtime::pool::node`, the `node_exec` / `npm_exec` agent tools, and the `node_runtime` harness-init step. **Facade + stub** — `ShellTool` holds `Option<Arc<NodeBootstrap>>` and `shell.rs` is kernel, so the module cannot simply vanish; `runtime/node/stub.rs` carries the `NodeBootstrap` type surface while registration sites are leaf-gated. **The generic native-tool dispatcher (`runtime::node::ops` / `runtime::node::types`) is NOT gated** — it backs both the gated `javascript.*` controllers and the ungated `flows` `oh:` `NativeToolBackend`, so native flow tools (`memory_search`, file, shell, …) keep working when the managed Node runtime is off. Off ⇒ `try_cached`/`probe_installed` return `None` and the shell never prepends a managed bin dir, identical to today's `node.enabled = false` path. | **`xz2` + its static liblzma C build.** First gate to remove a NATIVE toolchain build: `lzma-sys` leaves the list, 6 → 5. `tar`/`zip` are NOT shed — shared with `inference` (install_piper), `runtime::python`, and the document tools. |

**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`) is the **only** thing that catches drift, so run it before pushing any change to the voice surface.

Expand Down
30 changes: 28 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ hmac = "0.12"
# distributions ship as .tar.xz, Windows as .zip. `xz2` with `static`
# bundles liblzma so we don't need it as a system dependency.
tar = "0.4"
xz2 = { version = "0.1", features = ["static"] }
xz2 = { version = "0.1", features = ["static"], optional = true }
zip = { version = "2", default-features = false, features = ["deflate"] }
# gzip decoder for the Piper tar.gz binary releases on macOS / Linux. Already
# pulled in transitively by zip's `deflate` feature; declared directly so
Expand Down Expand Up @@ -429,7 +429,7 @@ tokio = { version = "1", features = ["test-util"] }
proptest = "1"

[features]
default = ["inference", "voice", "web3", "media", "documents", "meet", "skills", "flows", "mcp", "crash-reporting", "http-server", "channels", "tui", "medulla", "scheduler-gate", "file-logging", "contacts"]
default = ["inference", "voice", "web3", "media", "documents", "meet", "skills", "flows", "mcp", "crash-reporting", "http-server", "channels", "tui", "medulla", "scheduler-gate", "file-logging", "contacts", "runtime-node"]
# HTTP + Socket.IO server transport (#5048): the `/rpc` JSON-RPC endpoint and
# its auth middleware/CORS layer (`core::jsonrpc`, `core::auth`), the `/v1`
# OpenAI-compatible router (`inference::http`), the ad-hoc static-dir file
Expand Down Expand Up @@ -530,6 +530,32 @@ web3 = [
"dep:coins-bip39",
]

# Managed Node.js runtime: `runtime::node` (download / verify / extract / install
# a pinned toolchain), the `runtime::javascript` language slot over it,
# `runtime::pool::node`, the `node_exec` / `npm_exec` agent tools, and the

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Gate the Node pool behind runtime-node

In a --no-default-features --features flows kernel build, src/openhuman/runtime/pool/mod.rs still declares pub mod node unconditionally, so embedders retain the public run_inline Node execution API and the embedded pool_worker.js even though this feature's declared scope includes runtime::pool::node. Gate that module and its Node-only tests behind runtime-node so the off build actually removes the promised compile-time surface.

AGENTS.md reference: AGENTS.md:L338-L338

Useful? React with 👍 / 👎.

# `node_runtime` harness-init step. Default-ON. Slim builds opt out via
# `--no-default-features --features "<list without runtime-node>"`, which drops
# the exclusive `xz2` AND its static liblzma C build — the FIRST native
# toolchain build this gating program removes (6 native -> 5).
#
# FACADE + STUB, not a leaf gate. `ShellTool` holds `Option<Arc<NodeBootstrap>>`
# for managed-Node PATH injection and `tools/impl/system/shell.rs` is kernel, so
# deleting the module would take the shell tool with it. `runtime/node/stub.rs`
# carries the `NodeBootstrap` type surface; registration sites (`node_runtime_step`,
# the `javascript` controllers, `node_exec`/`npm_exec`) are leaf-gated at their call
# sites, because registration sites want absence.
#
# The generic native-tool dispatcher (`runtime::node::ops` / `runtime::node::types`)
# is deliberately NOT gated: it backs both the gated `javascript.*` controllers and
# the ungated `flows` `oh:` `NativeToolBackend`, so native flow tools keep working
# when the managed Node runtime is compiled out.
#
# Off-state: `try_cached`/`probe_installed` return `None`, so the shell never
# prepends a managed bin dir — identical to today's `node.enabled = false` path.
# `tar` and `zip` are NOT shed: `tar` is shared with `inference` (install_piper)
# and `runtime::python`, `zip` with `inference` and the document tools.
runtime-node = ["dep:xz2"]

# macOS Contacts seeding for the people domain: `memory::people::address_book`
# reads CNContactStore to seed handles. Default-ON. Slim / headless builds opt
# out via `--no-default-features --features "<list without contacts>"`, which
Expand Down
1 change: 1 addition & 0 deletions app/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ openhuman_core = { path = "../..", package = "openhuman", default-features = fal
# comes back with nothing to attach — and the absence is silent.
"file-logging",
"contacts",
"runtime-node",
] }
tinyjuice = { version = "0.2.1", default-features = false }

Expand Down
1 change: 1 addition & 0 deletions app/src-tauri/vendor/tauri-cef
Submodule tauri-cef added at 455b47
1 change: 1 addition & 0 deletions app/src-tauri/vendor/tauri-plugin-notification
Submodule tauri-plugin-notification added at 5f6196
13 changes: 12 additions & 1 deletion scripts/kernel-floor.limits
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@
# Simulate with: scripts/dep-sim.py --cut <crates>
#
# History
# 305/282/4 2026-08-09 runtime-node gate, measured on top of the
# upstream/main merge below (307/284/5 -> 305/282/4).
# `xz2` + its static liblzma C build are exclusive to
# runtime::node::extractor's .tar.xz path, so gating the
# managed Node toolchain removes them: -2 packages, -2
# names, and lzma-sys leaves the NATIVE list (5 -> 4) —
# the first native toolchain build removed by the gating
# program. Remaining native: libgit2-sys, libsqlite3-sys,
# libz-sys, ring — the target keeps the last two.
# Measured with `scripts/kernel-floor.sh flows`, and
# `scripts/dep-sim.py --cut-nothing` agrees (282 names).
# 307/284/5 2026-08-09 merge upstream/main + PR #5446 memory-subsystem work.
# Measured, not derived: `scripts/kernel-floor.sh flows`
# reports 307/284/5 on this merge. +3 names over the
Expand Down Expand Up @@ -147,4 +158,4 @@
# Native: aws-lc-sys libgit2-sys libsqlite3-sys libz-sys
# lzma-sys ring. Target after gating is 222 names / 2 native
# (libsqlite3-sys, ring) — see docs/plans MIGRATION-PLAN G6.
flows:307:284:5
flows:305:282:4
5 changes: 4 additions & 1 deletion src/core/all.rs
Original file line number Diff line number Diff line change
Expand Up @@ -675,7 +675,10 @@ fn build_registered_controllers() -> Vec<GroupedController> {
DomainGroup::Platform,
crate::openhuman::platform::socket::all_socket_registered_controllers(),
);
// Managed Node.js runtime bridge (tool listing + dispatch)
// Managed Node.js runtime bridge (tool listing + dispatch). Registration-site
// gate: with `runtime-node` off the `javascript.*` namespace is absent from
// `/schema` and unknown-method over `/rpc`, rather than registered+failing.
#[cfg(feature = "runtime-node")]
push(
&mut controllers,
DomainGroup::Runtimes,
Expand Down
30 changes: 30 additions & 0 deletions src/core/all_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2408,3 +2408,33 @@ fn sole_capability_for_namespace_is_none_for_mixed_and_unknown_namespaces() {
assert_eq!(sole_capability_for_namespace("people"), None);
assert_eq!(sole_capability_for_namespace("not_a_namespace"), None);
}

// ---- runtime-node gate -----------------------------------------------------

#[test]
#[cfg(feature = "runtime-node")]
fn javascript_controllers_registered_when_feature_on() {
let ns: Vec<&str> = all_controller_schemas()
.iter()
.map(|s| s.namespace)
.collect();
assert!(
ns.contains(&"javascript"),
"runtime-node ON must register the `javascript` namespace"
);
}

/// The half that proves the gate removes anything: absent, not
/// registered-and-failing.
#[test]
#[cfg(not(feature = "runtime-node"))]
fn javascript_controllers_absent_when_feature_off() {
let ns: Vec<&str> = all_controller_schemas()
.iter()
.map(|s| s.namespace)
.collect();
assert!(
!ns.contains(&"javascript"),
"runtime-node OFF must not register the `javascript` namespace"
);
}
31 changes: 21 additions & 10 deletions src/openhuman/agent/harness_init/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,9 @@ pub fn all_steps() -> Vec<HarnessInitStep> {
spacy_step(),
kompress_step(),
runtime_python_server_step(),
// Registration-site gate: no managed toolchain to provision when
// `runtime-node` is compiled out, so the step is absent.
#[cfg(feature = "runtime-node")]
node_runtime_step(),
Comment thread
senamakel marked this conversation as resolved.
]
}
Expand Down Expand Up @@ -232,6 +235,7 @@ async fn kompress_run(config: &Config) -> Result<(), String> {
.map_err(|e| format!("{e:#}"))
}

#[cfg(feature = "runtime-node")]
fn node_runtime_step() -> HarnessInitStep {
HarnessInitStep {
id: "node_runtime",
Expand All @@ -243,6 +247,7 @@ fn node_runtime_step() -> HarnessInitStep {
}
}

#[cfg(feature = "runtime-node")]
fn build_node_bootstrap(config: &Config) -> crate::openhuman::runtime::node::NodeBootstrap {
crate::openhuman::runtime::node::NodeBootstrap::new(
config.node.clone(),
Expand All @@ -251,6 +256,7 @@ fn build_node_bootstrap(config: &Config) -> crate::openhuman::runtime::node::Nod
)
}

#[cfg(feature = "runtime-node")]
async fn node_is_done(config: &Config) -> bool {
if !config.node.enabled {
return true;
Expand All @@ -264,6 +270,7 @@ async fn node_is_done(config: &Config) -> bool {
.is_some()
}

#[cfg(feature = "runtime-node")]
async fn node_run(config: &Config) -> Result<(), String> {
if !config.node.enabled {
return Ok(());
Expand All @@ -289,16 +296,20 @@ mod tests {
fn all_steps_have_stable_ids_and_are_non_required() {
let steps = all_steps();
let ids: Vec<_> = steps.iter().map(|s| s.id).collect();
assert_eq!(
ids,
vec![
"python_runtime",
"spacy",
"kompress",
"runtime_python_server",
"node_runtime"
]
);
let mut expected = vec![
"python_runtime",
"spacy",
"kompress",
"runtime_python_server",
];
// `node_runtime` is a registration-site gate: it is absent (not
// dead-but-listed) when the managed Node runtime is compiled out. `cfg!`
// (not `#[cfg]`) keeps `expected` mutable-and-used in both builds — same
// idiom as `tools/ops_tests.rs`.
if cfg!(feature = "runtime-node") {
expected.push("node_runtime");
}
assert_eq!(ids, expected);
assert!(steps.iter().all(|s| !s.required));
assert!(steps.iter().all(|s| !s.label.is_empty()));
}
Expand Down
20 changes: 17 additions & 3 deletions src/openhuman/runtime/javascript/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,27 @@
//! specific backend. That keeps the door open for future sibling modules like
//! `python`, `ruby`, or a different JavaScript backend.

pub use crate::openhuman::runtime::node::types::{ExecuteToolOutcome, RuntimeToolSummary};
//! ## Gating (`runtime-node`)
//!
//! The facade itself is always compiled — `ShellTool` imports `NodeBootstrap`
//! through it — but the re-exports split. The bootstrap type surface comes from
//! `node`'s stub when the feature is off; the download/extract/dispatch
//! machinery and the controller pair are gated, because their only consumers
//! are themselves gated off.

pub use crate::openhuman::runtime::node::{
ExecuteToolOutcome, NodeBootstrap, NodeSource, ResolvedNode,
};

#[cfg(feature = "runtime-node")]
pub use crate::openhuman::runtime::node::types::RuntimeToolSummary;
#[cfg(feature = "runtime-node")]
pub use crate::openhuman::runtime::node::{
all_runtime_node_controller_schemas as all_javascript_controller_schemas,
all_runtime_node_registered_controllers as all_javascript_registered_controllers,
};
#[cfg(feature = "runtime-node")]
pub use crate::openhuman::runtime::node::{
atomic_install, detect_system_node, download_distribution, execute_tool, extract_distribution,
fetch_shasums, list_tools, parse_node_version, NodeBootstrap, NodeDistribution, NodeSource,
ResolvedNode, SystemNode,
fetch_shasums, list_tools, parse_node_version, NodeDistribution, SystemNode,
};
44 changes: 41 additions & 3 deletions src/openhuman/runtime/node/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,62 @@
//! matching toolchain already exists on the host.
//! * [`bootstrap`] / [`downloader`] / [`extractor`] — resolve or install the
//! managed Node.js toolchain shipped with the core.
//! * [`ops`] / [`schemas`] — expose a minimal top-level runtime surface for
//! listing agent-callable tools and dispatching a tool by name.
//! * [`ops`] / [`types`] — the generic runtime tool bridge: build / list /
//! classify / execute against the native agent tool registry.
//! * [`schemas`] / [`rpc`] — the gated `javascript.*` controller pair.

//! ## Gating (`runtime-node`)
//!
//! Facade: this module is always declared, but only the *managed-Node*
//! machinery (`bootstrap` / `downloader` / `extractor` / `resolver` / `rpc` /
//! `schemas`) is `#[cfg(feature = "runtime-node")]`; a `stub` carries
//! `NodeBootstrap`'s type surface when the feature is off. The forcing
//! constraint is `ShellTool`, which holds `Option<Arc<NodeBootstrap>>` as a
//! field and is kernel — always compiled.
//!
//! [`ops`] and [`types`] are deliberately **not** gated. They are the generic
//! native-tool dispatcher over the agent tool registry (`oh:*` tools such as
//! `memory_search`, file, and shell tools) — they back both the gated
//! `javascript.*` controllers *and* the ungated `flows` `oh:` `NativeToolBackend`,
//! which must keep dispatching native tools even when the managed Node runtime
//! itself is compiled out. Only the JavaScript RPC and the Node-specific
//! `node_exec` / `npm_exec` tools are gated.

#[cfg(feature = "runtime-node")]
pub mod bootstrap;
#[cfg(feature = "runtime-node")]
pub mod downloader;
#[cfg(feature = "runtime-node")]
pub mod extractor;
pub mod ops;
#[cfg(feature = "runtime-node")]
pub mod resolver;
#[cfg(feature = "runtime-node")]
pub mod rpc;
#[cfg(feature = "runtime-node")]
mod schemas;

/// Generic runtime tool bridge. Always compiled — see module docs.
pub mod ops;
/// Inert serde types shared by [`ops`] and the gated JS RPC. Always compiled.
pub mod types;

#[cfg(not(feature = "runtime-node"))]
mod stub;
#[cfg(not(feature = "runtime-node"))]
pub use stub::{NodeBootstrap, NodeSource, ResolvedNode, RUNTIME_NODE_DISABLED_MESSAGE};

#[cfg(feature = "runtime-node")]
pub use bootstrap::{NodeBootstrap, NodeSource, ResolvedNode};
#[cfg(feature = "runtime-node")]
pub use downloader::{download_distribution, fetch_shasums, NodeDistribution};
#[cfg(feature = "runtime-node")]
pub use extractor::{atomic_install, extract_distribution};
pub use ops::{execute_tool, list_tools};
#[cfg(feature = "runtime-node")]
pub use resolver::{detect_system_node, parse_node_version, SystemNode};
#[cfg(feature = "runtime-node")]
pub use schemas::{
all_controller_schemas as all_runtime_node_controller_schemas,
all_registered_controllers as all_runtime_node_registered_controllers,
};
pub use types::ExecuteToolOutcome;
Loading
Loading