From 18ca68a56c0efbe546d8d4cd88a2b61d424b8827 Mon Sep 17 00:00:00 2001 From: Steven Enamakel <31011319+senamakel@users.noreply.github.com> Date: Sat, 16 May 2026 17:23:35 -0700 Subject: [PATCH] feat(runtime): add javascript facade and skill creator agent (#1971) --- app/test/e2e/helpers/skill-e2e-runtime.ts | 4 +- gitbooks/developing/architecture.md | 55 ++--- src/core/all.rs | 8 +- src/core/event_bus/events_tests.rs | 2 +- src/core/jsonrpc.rs | 10 +- src/core/jsonrpc_tests.rs | 2 +- src/openhuman/agent/agents/loader.rs | 41 +++- src/openhuman/agent/agents/mod.rs | 1 + .../agent/agents/orchestrator/agent.toml | 1 + .../agent/agents/skill_creator/agent.toml | 35 +++ .../agent/agents/skill_creator/mod.rs | 1 + .../agent/agents/skill_creator/prompt.md | 39 +++ .../agent/agents/skill_creator/prompt.rs | 120 ++++++++++ .../agent/harness/builtin_definitions.rs | 1 + src/openhuman/channels/proactive.rs | 2 +- src/openhuman/channels/runtime/startup.rs | 8 +- src/openhuman/composio/periodic.rs | 2 +- src/openhuman/composio/providers/registry.rs | 2 +- src/openhuman/config/schema/node.rs | 2 +- src/openhuman/javascript/mod.rs | 18 ++ src/openhuman/memory/global.rs | 2 +- src/openhuman/mod.rs | 3 +- .../bootstrap.rs | 0 .../downloader.rs | 0 .../extractor.rs | 0 .../{node_runtime => runtime_node}/mod.rs | 19 +- src/openhuman/runtime_node/ops.rs | 222 ++++++++++++++++++ .../resolver.rs | 0 src/openhuman/runtime_node/rpc.rs | 58 +++++ src/openhuman/runtime_node/schemas.rs | 142 +++++++++++ src/openhuman/runtime_node/types.rs | 23 ++ src/openhuman/scheduler_gate/gate.rs | 2 +- src/openhuman/screen_intelligence/cli/mod.rs | 4 +- src/openhuman/skills/README.md | 2 +- src/openhuman/skills/mod.rs | 2 +- src/openhuman/skills/types.rs | 2 +- src/openhuman/tool_timeout/mod.rs | 2 +- src/openhuman/tools/impl/system/node_exec.rs | 4 +- src/openhuman/tools/impl/system/npm_exec.rs | 4 +- src/openhuman/tools/impl/system/shell.rs | 2 +- src/openhuman/tools/ops.rs | 2 +- src/openhuman/webhooks/bus.rs | 8 +- src/openhuman/webhooks/ops.rs | 2 +- src/openhuman/webhooks/router.rs | 2 +- src/openhuman/webhooks/schemas.rs | 2 +- 45 files changed, 787 insertions(+), 76 deletions(-) create mode 100644 src/openhuman/agent/agents/skill_creator/agent.toml create mode 100644 src/openhuman/agent/agents/skill_creator/mod.rs create mode 100644 src/openhuman/agent/agents/skill_creator/prompt.md create mode 100644 src/openhuman/agent/agents/skill_creator/prompt.rs create mode 100644 src/openhuman/javascript/mod.rs rename src/openhuman/{node_runtime => runtime_node}/bootstrap.rs (100%) rename src/openhuman/{node_runtime => runtime_node}/downloader.rs (100%) rename src/openhuman/{node_runtime => runtime_node}/extractor.rs (100%) rename src/openhuman/{node_runtime => runtime_node}/mod.rs (50%) create mode 100644 src/openhuman/runtime_node/ops.rs rename src/openhuman/{node_runtime => runtime_node}/resolver.rs (100%) create mode 100644 src/openhuman/runtime_node/rpc.rs create mode 100644 src/openhuman/runtime_node/schemas.rs create mode 100644 src/openhuman/runtime_node/types.rs diff --git a/app/test/e2e/helpers/skill-e2e-runtime.ts b/app/test/e2e/helpers/skill-e2e-runtime.ts index 385aabd75..7a4230ba1 100644 --- a/app/test/e2e/helpers/skill-e2e-runtime.ts +++ b/app/test/e2e/helpers/skill-e2e-runtime.ts @@ -1,5 +1,5 @@ /** - * Seeds the minimal QuickJS echo skill used by Rust `json_rpc_skills_runtime_start_tools_call_stop` + * Seeds the minimal JavaScript echo skill used by Rust `json_rpc_skills_runtime_start_tools_call_stop` * so the desktop core can run `openhuman.skills_start` → `skills_call_tool` against a real skill tree. */ import fs from 'node:fs/promises'; @@ -13,7 +13,7 @@ const MANIFEST = { name: 'E2E Runtime Skill', version: '1.0.0', description: 'Minimal skill for desktop E2E (echo tool)', - /** Matches `SkillManifest` docs (`manifest.rs`); executed via QuickJS like `quickjs` alias. */ + /** Matches the legacy test manifest shape; executed as plain JavaScript. */ runtime: 'javascript', entry: 'index.js', auto_start: false, diff --git a/gitbooks/developing/architecture.md b/gitbooks/developing/architecture.md index 5997b5150..04241e20d 100644 --- a/gitbooks/developing/architecture.md +++ b/gitbooks/developing/architecture.md @@ -7,7 +7,7 @@ icon: code-branch **AI-powered super assistant for crypto communities, built on Rust.** -OpenHuman is a cross-platform communication and automation platform purpose-built for the cryptocurrency ecosystem. A single React + Rust (Tauri) codebase can target multiple platforms; **what we document and ship for users today is desktop only** - **Windows, macOS, and Linux**. Android, iOS, and web are **not** supported in current docs or releases. The stack includes a sandboxed JavaScript skills engine, persistent Rust-native WebSocket infrastructure, and an AI tool protocol that lets language models invoke any connected service in real time. +OpenHuman is a cross-platform communication and automation platform purpose-built for the cryptocurrency ecosystem. A single React + Rust (Tauri) codebase can target multiple platforms; **what we document and ship for users today is desktop only** - **Windows, macOS, and Linux**. Android, iOS, and web are **not** supported in current docs or releases. The stack includes a managed Node.js runtime for tool-capable skills, persistent Rust-native WebSocket infrastructure, and an AI tool protocol that lets language models invoke any connected service in real time. --- @@ -16,7 +16,7 @@ OpenHuman is a cross-platform communication and automation platform purpose-buil | Path | Contents | | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **`app/`** | Yarn workspace **`openhuman-app`**: Vite/React UI (`app/src/`), Tauri shell (`app/src-tauri/`), Vitest tests | -| **Repo root `src/`** | Rust **`openhuman_core`** library + **`openhuman-core`** CLI binary - `core_server`, JSON-RPC, QuickJS skills runtime (`src/openhuman/skills/`), channels, memory, etc. | +| **Repo root `src/`** | Rust **`openhuman_core`** library + **`openhuman-core`** CLI binary - core server, JSON-RPC, first-class JavaScript runtime (`src/openhuman/javascript/`) backed by a managed Node.js implementation, channels, memory, etc. | | **`Cargo.toml`** (root) | Builds the `openhuman-core` binary (`cargo build --bin openhuman-core`) staged into `app/src-tauri/binaries/` for the desktop bundle | | **`skills/`** | Skill packages consumed by the runtime | | **`docs/`** | This book + per-tree guides (`docs/src/`, `docs/src-tauri/`) | @@ -150,10 +150,10 @@ OpenHuman's defining capability is its **sandboxed JavaScript execution engine** | +--------+----------+ +--------+----------+ | | | | | | +--------v----------+ +--------v----------+ +----------+ | -| | QuickJS Instance | | QuickJS Instance | | Bridge | | -| | Skill A | | Skill B | | APIs | | -| | 64 MB memory cap | | 64 MB memory cap | +----+-----+ | -| | 512 KB stack | | 512 KB stack | | | +| | JavaScript Layer | | runtime_node | | Bridge | | +| | skill metadata | | managed Node.js | | APIs | | +| | + prompt context | | system/bundled | +----+-----+ | +| | + tool discovery | | tool execution | | | | +-------------------+ +-------------------+ | | | | | | +---------------------------------------------------v-----+ | @@ -163,19 +163,19 @@ OpenHuman's defining capability is its **sandboxed JavaScript execution engine** +---------------------------------------------------------------+ ``` -**QuickJS Runtime** (`rquickjs`): Each skill gets its own QuickJS `AsyncRuntime` and `AsyncContext`, fully isolated memory spaces with no cross-skill access. +**Node.js Runtime**: the core resolves a compatible system `node` when possible and otherwise installs a managed distribution into the OpenHuman cache. Skills primarily expose tool metadata and use the runtime bridge to list and execute tools rather than running isolated QuickJS VMs inside the core. -| Parameter | Value | -| ------------------------------ | ----------- | -| Default memory limit per skill | 64 MB | -| Stack size | 512 KB | -| Initialization timeout | 10 seconds | -| Graceful stop timeout | 5 seconds | -| Message channel buffer | 64 messages | +| Parameter | Value | +| ---------------------- | ----- | +| Public language slot | `javascript` | +| Current JS backend | `runtime_node` | +| Managed Node version | `v22.11.0` by default | +| Runtime source | system `node` or managed install | +| Integrity verification | SHA-256 against `SHASUMS256.txt` | -**Message-passing architecture**: Skills communicate with the core engine through async MPSC channels, no shared mutable state. The registry routes tool calls, server events, cron triggers, and lifecycle commands to the correct skill instance via its channel sender. +**Tool bridge architecture**: `SKILL.md` packages provide metadata, instructions, and optional bundled JS helpers. The Rust core owns the authoritative tool registry, and the JavaScript runtime bridge lists tools and dispatches named tool calls into the core or into Node-backed helpers. -**Bridge APIs** expose platform capabilities to skill JavaScript code: +**Bridge APIs** expose platform capabilities to the runtime bridge and Node-backed helpers: | Bridge | Capability | | --------- | ----------------------------------------------------------- | @@ -186,20 +186,17 @@ OpenHuman's defining capability is its **sandboxed JavaScript execution engine** | **log** | Structured logging routed through Rust `log` crate | | **tauri** | Platform detection, notifications, whitelisted env vars | -**Skill discovery** uses a manifest system. Each skill declares its metadata in a JSON manifest: +**Skill discovery** uses `SKILL.md` plus optional bundled resources: -| Field | Purpose | -| ----------------- | ----------------------------------------- | -| `id` | Unique identifier | -| `name` | Human-readable display name | -| `runtime` | Execution engine (`quickjs`) | -| `entry` | Entry point file (default: `index.js`) | -| `memory_limit_mb` | Per-skill memory cap (default: 64) | -| `platforms` | Supported platforms (default: all) | -| `setup` | OAuth and configuration wizard definition | -| `auto_start` | Start on app launch | +| Field | Purpose | +| ------------------ | ------- | +| `name` | Human-readable display name | +| `description` | Trigger/selection summary | +| `metadata.id` | Stable skill slug when present | +| `allowed-tools` | Tool allowlist guidance | +| bundled resources | scripts, references, assets | -Skills are synced from a GitHub repository and discovered at runtime. Platform filtering ensures skills only run where they're supported. +Skills are synced from a GitHub repository and discovered at runtime. Execution is no longer modeled as one embedded QuickJS VM per skill; JavaScript behavior flows through the shared runtime bridge instead. **Cron scheduler**: A 5-second tick loop checks all registered schedules against UTC time, using the `cron` crate for expression parsing. When a schedule fires, the scheduler sends a `CronTrigger` message to the skill's channel, invoking the skill's `onCronTrigger()` handler. @@ -341,7 +338,7 @@ Every layer is async and non-blocking. The Rust core processes thousands of conc | **Framework** | Tauri v2 | Native cross-platform with minimal overhead | | **Language** | Rust (2021 edition) | Memory safety, zero-cost abstractions | | **Async** | Tokio | High-performance async I/O runtime | -| **JS Engine** | QuickJS (rquickjs) | Lightweight sandboxed JS execution (~1-2 MB per context) | +| **JS Runtime** | Node.js | Managed V8 runtime for tool helpers and skill-adjacent JS | | **Database** | SQLite (rusqlite) | Embedded, zero-config, per-skill isolation | | **WebSocket** | tokio-tungstenite + rustls | Persistent connections with native TLS | | **HTTP** | reqwest | Async HTTP with rustls + native-tLS dual support | diff --git a/src/core/all.rs b/src/core/all.rs index 1d6b7ea2f..6aa8218f7 100644 --- a/src/core/all.rs +++ b/src/core/all.rs @@ -161,8 +161,10 @@ fn build_registered_controllers() -> Vec { controllers.extend( crate::openhuman::screen_intelligence::all_screen_intelligence_registered_controllers(), ); - // Bridge to external skill runtimes + // Backend Socket.IO bridge + related runtime plumbing controllers.extend(crate::openhuman::socket::all_socket_registered_controllers()); + // Managed Node.js runtime bridge (tool listing + dispatch) + controllers.extend(crate::openhuman::javascript::all_javascript_registered_controllers()); // Discovered SKILL.md skills and their bundled resources controllers.extend(crate::openhuman::skills::all_skills_registered_controllers()); // User workspace and file management @@ -283,6 +285,7 @@ fn build_declared_controller_schemas() -> Vec { crate::openhuman::screen_intelligence::all_screen_intelligence_controller_schemas(), ); schemas.extend(crate::openhuman::socket::all_socket_controller_schemas()); + schemas.extend(crate::openhuman::javascript::all_javascript_controller_schemas()); schemas.extend(crate::openhuman::skills::all_skills_controller_schemas()); schemas.extend(crate::openhuman::workspace::all_workspace_controller_schemas()); schemas.extend(crate::openhuman::tools::all_tools_controller_schemas()); @@ -365,11 +368,12 @@ pub fn namespace_description(namespace: &str) -> Option<&'static str> { "health" => Some("Process and component health snapshots."), "local_ai" => Some("Local AI chat, inference, downloads, and media operations."), "migrate" => Some("Data migration utilities."), + "javascript" => Some("First-class JavaScript runtime bridge for listing and dispatching tools."), "screen_intelligence" => Some("Screen capture, permissions, and accessibility automation."), "security" => Some("Security policy and autonomy guardrail metadata."), "service" => Some("Desktop service lifecycle management."), "skills" => Some("Discovered SKILL.md skills and their bundled resources."), - "socket" => Some("Skills runtime socket bridge controls."), + "socket" => Some("Backend Socket.IO bridge controls."), "memory" => Some("Document storage, vector search, key-value store, and knowledge graph."), "memory_tree" => Some( "Canonical chunk ingestion, provenance capture, and chunk retrieval for source-grounded memory.", diff --git a/src/core/event_bus/events_tests.rs b/src/core/event_bus/events_tests.rs index 9b84967a7..acdd05170 100644 --- a/src/core/event_bus/events_tests.rs +++ b/src/core/event_bus/events_tests.rs @@ -177,7 +177,7 @@ fn all_variants_have_correct_domain() { ( DomainEvent::SkillLoaded { skill_id: "s".into(), - runtime: "quickjs".into(), + runtime: "nodejs".into(), }, "skill", ), diff --git a/src/core/jsonrpc.rs b/src/core/jsonrpc.rs index 2c807f58b..32bbb0c58 100644 --- a/src/core/jsonrpc.rs +++ b/src/core/jsonrpc.rs @@ -872,8 +872,8 @@ async fn run_server_inner( let app = build_core_http_router(socketio_enabled); - // --- Skill runtime bootstrap ------------------------------------------- - bootstrap_skill_runtime(embedded_core).await; + // --- Core runtime bootstrap -------------------------------------------- + bootstrap_core_runtime(embedded_core).await; log::info!( "[core] OpenHuman core is ready — listening on http://{bind_addr} (version {})", @@ -1052,7 +1052,7 @@ async fn run_server_inner( /// Registers all long-lived domain event-bus subscribers exactly once. /// -/// Guarded by `std::sync::Once` so repeated calls to `bootstrap_skill_runtime` +/// Guarded by `std::sync::Once` so repeated calls to `bootstrap_core_runtime` /// are safe and idempotent. fn register_domain_subscribers( workspace_dir: std::path::PathBuf, @@ -1166,7 +1166,7 @@ fn register_domain_subscribers( } /// Initializes long-lived socket/event-bus infrastructure. -pub async fn bootstrap_skill_runtime(embedded_core: bool) { +pub async fn bootstrap_core_runtime(embedded_core: bool) { use crate::openhuman::socket::{set_global_socket_manager, SocketManager}; use std::sync::Arc; let cfg = match crate::openhuman::config::Config::load_or_init().await { @@ -1182,7 +1182,7 @@ pub async fn bootstrap_skill_runtime(embedded_core: bool) { // 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); // Register domain subscribers for cross-module event handling. - // Uses a Once guard so repeated calls to bootstrap_skill_runtime() + // Uses a Once guard so repeated calls to bootstrap_core_runtime() // cannot double-subscribe. register_domain_subscribers(workspace_dir.clone(), cfg.clone(), embedded_core); diff --git a/src/core/jsonrpc_tests.rs b/src/core/jsonrpc_tests.rs index ad2433eb8..a9e3d908f 100644 --- a/src/core/jsonrpc_tests.rs +++ b/src/core/jsonrpc_tests.rs @@ -83,7 +83,7 @@ async fn wait_until_port_released(port: u16) { /// `CancellationToken` is fired. /// /// **Ignored by default.** This test calls `run_server_embedded`, -/// which triggers the full production bootstrap (`bootstrap_skill_runtime` +/// which triggers the full production bootstrap (`bootstrap_core_runtime` /// → `register_domain_subscribers` → `scheduler_gate::init_global` + /// `memory::tree::jobs::start` + `composio::start_periodic_sync` + /// cron scheduler). Those code paths spawn detached `tokio::spawn` diff --git a/src/openhuman/agent/agents/loader.rs b/src/openhuman/agent/agents/loader.rs index 42638cc97..b2bd73a4f 100644 --- a/src/openhuman/agent/agents/loader.rs +++ b/src/openhuman/agent/agents/loader.rs @@ -92,6 +92,11 @@ pub const BUILTINS: &[BuiltinAgent] = &[ toml: include_str!("tool_maker/agent.toml"), prompt_fn: super::tool_maker::prompt::build, }, + BuiltinAgent { + id: "skill_creator", + toml: include_str!("skill_creator/agent.toml"), + prompt_fn: super::skill_creator::prompt::build, + }, BuiltinAgent { id: "researcher", toml: include_str!("researcher/agent.toml"), @@ -182,7 +187,7 @@ mod tests { fn all_builtins_parse() { let defs = load_builtins().expect("built-in TOML must parse"); assert_eq!(defs.len(), BUILTINS.len()); - assert_eq!(defs.len(), 16, "expected 16 built-in agents"); + assert_eq!(defs.len(), 17, "expected 17 built-in agents"); } #[test] @@ -344,6 +349,25 @@ mod tests { assert!(!def.omit_safety_preamble); } + #[test] + fn skill_creator_is_sandboxed_and_has_node_tools() { + let def = find("skill_creator"); + assert_eq!(def.sandbox_mode, SandboxMode::Sandboxed); + assert_eq!(def.max_iterations, 10); + assert!(!def.omit_safety_preamble); + match &def.tools { + ToolScope::Named(names) => { + for required in ["node_exec", "npm_exec", "apply_patch", "update_memory_md"] { + assert!( + names.iter().any(|name| name == required), + "skill_creator tool list missing `{required}`" + ); + } + } + ToolScope::Wildcard => panic!("skill_creator must have named tool allowlist"), + } + } + #[test] fn critic_is_read_only() { let def = find("critic"); @@ -633,6 +657,21 @@ mod tests { ); } + #[test] + fn orchestrator_subagents_include_skill_creator() { + use crate::openhuman::agent::harness::definition::SubagentEntry; + let def = find("orchestrator"); + let listed = def.subagents.iter().any(|e| match e { + SubagentEntry::AgentId(id) => id == "skill_creator", + _ => false, + }); + assert!( + listed, + "orchestrator.subagents must list `skill_creator` so the \ + routing layer can synthesise `create_skill`" + ); + } + #[test] fn welcome_has_onboarding_and_memory_tools() { let def = find("welcome"); diff --git a/src/openhuman/agent/agents/mod.rs b/src/openhuman/agent/agents/mod.rs index 1f930c588..f87484434 100644 --- a/src/openhuman/agent/agents/mod.rs +++ b/src/openhuman/agent/agents/mod.rs @@ -14,6 +14,7 @@ pub mod morning_briefing; pub mod orchestrator; pub mod planner; pub mod researcher; +pub mod skill_creator; pub mod summarizer; pub mod tool_maker; pub mod tools_agent; diff --git a/src/openhuman/agent/agents/orchestrator/agent.toml b/src/openhuman/agent/agents/orchestrator/agent.toml index 6f7d3d6b7..9129451ac 100644 --- a/src/openhuman/agent/agents/orchestrator/agent.toml +++ b/src/openhuman/agent/agents/orchestrator/agent.toml @@ -42,6 +42,7 @@ subagents = [ "planner", "code_executor", "tools_agent", + "skill_creator", "critic", "archivist", # Crypto wallet & market specialist (#1397). Synthesised into a diff --git a/src/openhuman/agent/agents/skill_creator/agent.toml b/src/openhuman/agent/agents/skill_creator/agent.toml new file mode 100644 index 000000000..f797f1f07 --- /dev/null +++ b/src/openhuman/agent/agents/skill_creator/agent.toml @@ -0,0 +1,35 @@ +id = "skill_creator" +display_name = "Skill Creator" +delegate_name = "create_skill" +when_to_use = "JavaScript skill/runtime specialist — creates or updates OpenHuman SKILL.md packages, Node-backed JS helpers, and tool-facing JavaScript code meant to be executed by the core's Node runtime bridge or by other agents." +temperature = 0.3 +max_iterations = 10 +max_result_chars = 16000 +sandbox_mode = "sandboxed" +omit_identity = true +omit_memory_context = true +omit_safety_preamble = false +omit_skills_catalog = true + +[model] +hint = "coding" + +[tools] +named = [ + "shell", + "file_read", + "file_write", + "git_operations", + "node_exec", + "npm_exec", + "grep", + "glob", + "list", + "edit", + "apply_patch", + "todowrite", + "plan_exit", + "web_fetch", + "lsp", + "update_memory_md", +] diff --git a/src/openhuman/agent/agents/skill_creator/mod.rs b/src/openhuman/agent/agents/skill_creator/mod.rs new file mode 100644 index 000000000..8bf84783c --- /dev/null +++ b/src/openhuman/agent/agents/skill_creator/mod.rs @@ -0,0 +1 @@ +pub mod prompt; diff --git a/src/openhuman/agent/agents/skill_creator/prompt.md b/src/openhuman/agent/agents/skill_creator/prompt.md new file mode 100644 index 000000000..5042dad07 --- /dev/null +++ b/src/openhuman/agent/agents/skill_creator/prompt.md @@ -0,0 +1,39 @@ +# Skill Creator — Node Runtime and SKILL Author + +You are the **Skill Creator** agent. Your job is to create or update OpenHuman skills and the JavaScript that supports them. + +## What You Build + +- `SKILL.md` skills and related bundled resources +- JavaScript or TypeScript files that should run through Node.js +- Repo wiring needed so orchestrator or other agents can use the new capability +- Targeted tests or validation commands that prove the skill/code works + +## Runtime Rules + +- **Do not assume QuickJS exists.** The old embedded QuickJS runtime is gone. +- **Target the real execution surfaces in this repo**: + - `node_exec` for one-off JS execution + - `npm_exec` for package/script workflows + - `javascript` controllers when the core should expose tool listing or named tool dispatch +- **Treat `SKILL.md` as metadata/instructions first.** If the user wants executable behavior, also add or update the Node-backed code path that actually runs it. + +## Working Style + +- Inspect existing patterns before inventing a new one. +- Prefer small, composable changes over a new parallel framework. +- When adding JS execution support, wire it to orchestrator/subagents through the existing agent and tool surfaces instead of hidden side paths. +- Keep naming consistent with the repo's current `javascript`, `tools`, and agent definitions. +- Add or update tests when behavior changes. + +## Validation + +- Run targeted checks after editing. +- For skills: validate the `SKILL.md` shape and any runtime code path you touched. +- For JavaScript: execute the narrowest useful `node_exec`, `npm_exec`, or project test command and fix failures before stopping. + +## Output Contract + +- Return what you changed. +- State how the orchestrator or another agent is expected to invoke it. +- Call out anything still missing from full end-to-end execution. diff --git a/src/openhuman/agent/agents/skill_creator/prompt.rs b/src/openhuman/agent/agents/skill_creator/prompt.rs new file mode 100644 index 000000000..5de0ebc22 --- /dev/null +++ b/src/openhuman/agent/agents/skill_creator/prompt.rs @@ -0,0 +1,120 @@ +//! System prompt builder for the `skill_creator` built-in agent. + +use crate::openhuman::context::prompt::{ + render_safety, render_tools, render_user_files, render_workspace, PromptContext, +}; +use anyhow::Result; +use tracing::{debug, trace}; + +const ARCHETYPE: &str = include_str!("prompt.md"); + +pub fn build(ctx: &PromptContext<'_>) -> Result { + debug!( + agent_id = %ctx.agent_id, + model_name = %ctx.model_name, + "[skill_creator::prompt] build: start" + ); + let mut out = String::with_capacity(4096); + out.push_str(ARCHETYPE.trim_end()); + out.push_str("\n\n"); + + let user_files = match render_user_files(ctx) { + Ok(user_files) => user_files, + Err(error) => { + debug!(%error, "[skill_creator::prompt] build: render_user_files failed"); + return Err(error); + } + }; + let has_user_files = !user_files.trim().is_empty(); + trace!( + has_user_files, + user_files_len = user_files.len(), + "[skill_creator::prompt] build: user files rendered" + ); + if has_user_files { + out.push_str(user_files.trim_end()); + out.push_str("\n\n"); + } + + let tools = match render_tools(ctx) { + Ok(tools) => tools, + Err(error) => { + debug!(%error, "[skill_creator::prompt] build: render_tools failed"); + return Err(error); + } + }; + let has_tools = !tools.trim().is_empty(); + trace!( + has_tools, + tools_len = tools.len(), + "[skill_creator::prompt] build: tools rendered" + ); + if has_tools { + out.push_str(tools.trim_end()); + out.push_str("\n\n"); + } + + let safety = render_safety(); + trace!( + safety_len = safety.len(), + "[skill_creator::prompt] build: safety rendered" + ); + out.push_str(safety.trim_end()); + out.push_str("\n\n"); + + let workspace = match render_workspace(ctx) { + Ok(workspace) => workspace, + Err(error) => { + debug!(%error, "[skill_creator::prompt] build: render_workspace failed"); + return Err(error); + } + }; + let has_workspace = !workspace.trim().is_empty(); + trace!( + has_workspace, + workspace_len = workspace.len(), + "[skill_creator::prompt] build: workspace rendered" + ); + if has_workspace { + out.push_str(workspace.trim_end()); + out.push('\n'); + } + + debug!( + output_len = out.len(), + "[skill_creator::prompt] build: done" + ); + Ok(out) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::openhuman::context::prompt::{LearnedContextData, ToolCallFormat}; + use std::collections::HashSet; + + #[test] + fn build_returns_nonempty_body() { + let visible: HashSet = HashSet::new(); + let ctx = PromptContext { + workspace_dir: std::path::Path::new("."), + model_name: "test", + agent_id: "skill_creator", + tools: &[], + skills: &[], + dispatcher_instructions: "", + learned: LearnedContextData::default(), + visible_tool_names: &visible, + tool_call_format: ToolCallFormat::PFormat, + connected_integrations: &[], + connected_identities_md: String::new(), + include_profile: false, + include_memory_md: false, + curated_snapshot: None, + user_identity: None, + }; + let body = build(&ctx).unwrap(); + assert!(!body.is_empty()); + assert!(body.contains("Do not assume QuickJS exists.")); + } +} diff --git a/src/openhuman/agent/harness/builtin_definitions.rs b/src/openhuman/agent/harness/builtin_definitions.rs index c86146f7d..2819deeaf 100644 --- a/src/openhuman/agent/harness/builtin_definitions.rs +++ b/src/openhuman/agent/harness/builtin_definitions.rs @@ -176,6 +176,7 @@ mod tests { "code_executor", "integrations_agent", "tool_maker", + "skill_creator", "researcher", "critic", "archivist", diff --git a/src/openhuman/channels/proactive.rs b/src/openhuman/channels/proactive.rs index 6243112c4..f11e5a6e4 100644 --- a/src/openhuman/channels/proactive.rs +++ b/src/openhuman/channels/proactive.rs @@ -29,7 +29,7 @@ use std::sync::{Arc, RwLock}; /// Register a web-only proactive message subscriber on the global event /// bus. Guarded by `std::sync::Once` so it is safe to call from both -/// `bootstrap_skill_runtime` (desktop/JSON-RPC) and domain-level +/// `bootstrap_core_runtime` (desktop/JSON-RPC) and domain-level /// startup — only the first call takes effect. pub fn register_web_only_proactive_subscriber() { use std::sync::Once; diff --git a/src/openhuman/channels/runtime/startup.rs b/src/openhuman/channels/runtime/startup.rs index ec28831f9..3fa680a90 100644 --- a/src/openhuman/channels/runtime/startup.rs +++ b/src/openhuman/channels/runtime/startup.rs @@ -54,12 +54,12 @@ pub async fn start_channels(config: Config) -> Result<()> { // a thin tokio task that ticks every minute and dispatches into // any provider whose `sync_interval_secs` has elapsed for an // active Composio connection. Safe to call here even though - // `bootstrap_skill_runtime` may also start it — `start_periodic_sync` + // `bootstrap_core_runtime` may also start it — `start_periodic_sync` // is intentionally cheap and the loop body no-ops when there are // no connections. crate::openhuman::composio::start_periodic_sync(); // Native request handlers. Re-registering is safe (latest wins) so - // this is idempotent even if `bootstrap_skill_runtime` also runs. + // this is idempotent even if `bootstrap_core_runtime` also runs. // Must happen before `run_message_dispatch_loop` begins, because // channel dispatch calls `request_native_global("agent.run_turn", …)` // for every inbound message. @@ -141,7 +141,7 @@ pub async fn start_channels(config: Config) -> Result<()> { tracing::debug!("[event_bus] global singleton initialized in start_channels"); // Initialise the sub-agent definition registry from this workspace. - // Idempotent — `bootstrap_skill_runtime` may also call it. + // Idempotent — `bootstrap_core_runtime` may also call it. if let Err(err) = crate::openhuman::agent::harness::AgentDefinitionRegistry::init_global( &config.workspace_dir, ) { @@ -151,7 +151,7 @@ pub async fn start_channels(config: Config) -> Result<()> { ); } // Note: WebhookRequestSubscriber and ChannelInboundSubscriber are registered - // in bootstrap_skill_runtime() (src/core/jsonrpc.rs) to avoid double-registration + // in bootstrap_core_runtime() (src/core/jsonrpc.rs) to avoid double-registration // when both startup paths run in the same process. let provider_runtime_options = providers::ProviderRuntimeOptions { diff --git a/src/openhuman/composio/periodic.rs b/src/openhuman/composio/periodic.rs index 0690f089d..571c1340d 100644 --- a/src/openhuman/composio/periodic.rs +++ b/src/openhuman/composio/periodic.rs @@ -65,7 +65,7 @@ use super::providers::{get_provider, ProviderContext, SyncReason}; const TICK_SECONDS: u64 = 1200; /// Process-wide guard so the scheduler is only started once even -/// when both `start_channels` and `bootstrap_skill_runtime` call into +/// when both `start_channels` and `bootstrap_core_runtime` call into /// us during startup. Without this we'd end up with two parallel tick /// loops competing for the same connections. static SCHEDULER_STARTED: OnceLock<()> = OnceLock::new(); diff --git a/src/openhuman/composio/providers/registry.rs b/src/openhuman/composio/providers/registry.rs index 689edc823..3e1444393 100644 --- a/src/openhuman/composio/providers/registry.rs +++ b/src/openhuman/composio/providers/registry.rs @@ -74,7 +74,7 @@ pub fn all_providers() -> Vec { } /// Register the built-in providers shipped with the core. Called once -/// from `start_channels` / `bootstrap_skill_runtime` startup paths. +/// from `start_channels` / `bootstrap_core_runtime` startup paths. /// /// Idempotent: re-running just re-registers (no-op in practice). pub fn init_default_providers() { diff --git a/src/openhuman/config/schema/node.rs b/src/openhuman/config/schema/node.rs index e509cf020..e6f64223e 100644 --- a/src/openhuman/config/schema/node.rs +++ b/src/openhuman/config/schema/node.rs @@ -18,7 +18,7 @@ pub struct NodeConfig { #[serde(default = "default_version")] pub version: String, /// Absolute path to a directory where managed Node distributions are - /// extracted. Empty string means "use the default workspace cache dir" + /// extracted. Empty string means "use the default OpenHuman cache dir" /// (resolved by the runtime bootstrap). #[serde(default)] pub cache_dir: String, diff --git a/src/openhuman/javascript/mod.rs b/src/openhuman/javascript/mod.rs new file mode 100644 index 000000000..9305faf6e --- /dev/null +++ b/src/openhuman/javascript/mod.rs @@ -0,0 +1,18 @@ +//! First-class JavaScript runtime surface. +//! +//! Today the implementation backend is the managed Node.js runtime in +//! [`crate::openhuman::runtime_node`]. This module exists so the rest of the +//! core talks to a language slot (`javascript`) rather than directly to a +//! 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}; +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, +}; +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, +}; diff --git a/src/openhuman/memory/global.rs b/src/openhuman/memory/global.rs index f42c94596..efd787769 100644 --- a/src/openhuman/memory/global.rs +++ b/src/openhuman/memory/global.rs @@ -1,7 +1,7 @@ //! Process-global memory client singleton. //! //! One `MemoryClient` (and its background ingestion-queue worker) lives for the -//! entire core process. Every subsystem — RPC handlers, skills runtime, screen +//! entire core process. Every subsystem — RPC handlers, node runtime, screen //! intelligence, CLI — shares this single instance so the worker is never //! prematurely dropped. //! diff --git a/src/openhuman/mod.rs b/src/openhuman/mod.rs index 5b9198dd7..b96079364 100644 --- a/src/openhuman/mod.rs +++ b/src/openhuman/mod.rs @@ -38,6 +38,7 @@ pub mod health; pub mod heartbeat; pub mod http_host; pub mod integrations; +pub mod javascript; pub mod learning; pub mod local_ai; pub mod mcp_server; @@ -46,7 +47,6 @@ pub mod meet_agent; pub mod memory; pub mod migration; pub mod migrations; -pub mod node_runtime; pub mod notifications; pub mod overlay; pub mod people; @@ -56,6 +56,7 @@ pub mod providers; pub mod redirect_links; pub mod referral; pub mod routing; +pub mod runtime_node; pub mod scheduler_gate; pub mod screen_intelligence; pub mod security; diff --git a/src/openhuman/node_runtime/bootstrap.rs b/src/openhuman/runtime_node/bootstrap.rs similarity index 100% rename from src/openhuman/node_runtime/bootstrap.rs rename to src/openhuman/runtime_node/bootstrap.rs diff --git a/src/openhuman/node_runtime/downloader.rs b/src/openhuman/runtime_node/downloader.rs similarity index 100% rename from src/openhuman/node_runtime/downloader.rs rename to src/openhuman/runtime_node/downloader.rs diff --git a/src/openhuman/node_runtime/extractor.rs b/src/openhuman/runtime_node/extractor.rs similarity index 100% rename from src/openhuman/node_runtime/extractor.rs rename to src/openhuman/runtime_node/extractor.rs diff --git a/src/openhuman/node_runtime/mod.rs b/src/openhuman/runtime_node/mod.rs similarity index 50% rename from src/openhuman/node_runtime/mod.rs rename to src/openhuman/runtime_node/mod.rs index 365937dcc..db9b2bc17 100644 --- a/src/openhuman/node_runtime/mod.rs +++ b/src/openhuman/runtime_node/mod.rs @@ -1,21 +1,30 @@ -//! Managed Node.js runtime for skills that require `node` / `npm`. +//! Managed Node.js runtime and tool bridge. //! //! Responsibilities are split across submodules: //! //! * [`resolver`] — detect a compatible system `node` on `PATH`. Cheap, //! synchronous, called first so we can skip the download path when a //! matching toolchain already exists on the host. -//! -//! Later commits layer on a downloader, archive extractor, cache manager, -//! and a bootstrap entry point that returns the resolved `node`/`npm` -//! binary paths for `node_exec` / `npm_exec` tools. +//! * [`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. pub mod bootstrap; pub mod downloader; pub mod extractor; +pub mod ops; pub mod resolver; +pub mod rpc; +mod schemas; +pub mod types; pub use bootstrap::{NodeBootstrap, NodeSource, ResolvedNode}; pub use downloader::{download_distribution, fetch_shasums, NodeDistribution}; pub use extractor::{atomic_install, extract_distribution}; +pub use ops::{execute_tool, list_tools}; pub use resolver::{detect_system_node, parse_node_version, SystemNode}; +pub use schemas::{ + all_controller_schemas as all_runtime_node_controller_schemas, + all_registered_controllers as all_runtime_node_registered_controllers, +}; diff --git a/src/openhuman/runtime_node/ops.rs b/src/openhuman/runtime_node/ops.rs new file mode 100644 index 000000000..3eda43b50 --- /dev/null +++ b/src/openhuman/runtime_node/ops.rs @@ -0,0 +1,222 @@ +use std::sync::Arc; +use std::time::Instant; + +use crate::core::event_bus::{publish_global, DomainEvent}; +use crate::openhuman::agent::host_runtime::{NativeRuntime, RuntimeAdapter}; +use crate::openhuman::config::Config; +use crate::openhuman::memory::Memory; +use crate::openhuman::runtime_node::types::{ExecuteToolOutcome, RuntimeToolSummary}; +use crate::openhuman::security::SecurityPolicy; +use crate::openhuman::tools::{self, Tool, ToolCallOptions, ToolScope}; +use tracing::{debug, trace}; + +fn tool_scope_label(scope: ToolScope) -> &'static str { + match scope { + ToolScope::All => "all", + ToolScope::AgentOnly => "agent_only", + ToolScope::CliRpcOnly => "cli_rpc_only", + } +} + +fn summarize_tool(tool: &dyn Tool) -> RuntimeToolSummary { + RuntimeToolSummary { + name: tool.name().to_string(), + description: tool.description().to_string(), + category: tool.category().to_string(), + permission_level: tool.permission_level().to_string(), + scope: tool_scope_label(tool.scope()).to_string(), + supports_markdown: tool.supports_markdown(), + parameters: tool.parameters_schema(), + } +} + +fn build_runtime_tools(config: &Config) -> Result>, String> { + debug!( + workspace = %config.workspace_dir.display(), + "[runtime_node::ops] build_runtime_tools: start" + ); + let security = Arc::new(SecurityPolicy::from_config( + &config.autonomy, + &config.workspace_dir, + )); + let runtime: Arc = Arc::new(NativeRuntime::new()); + let local_embedding = config.workload_local_model("embeddings"); + trace!("[runtime_node::ops] build_runtime_tools: create_memory_with_local_ai"); + let memory: Arc = Arc::from( + crate::openhuman::memory::create_memory_with_local_ai( + &config.memory, + local_embedding.as_deref(), + &config.embedding_routes, + Some(&config.storage.provider.config), + &config.workspace_dir, + ) + .map_err(|error| { + debug!( + error = %error, + "[runtime_node::ops] build_runtime_tools: create_memory_with_local_ai failed" + ); + error.to_string() + })?, + ); + trace!("[runtime_node::ops] build_runtime_tools: tools::all_tools_with_runtime"); + let built = tools::all_tools_with_runtime( + Arc::new(config.clone()), + &security, + runtime, + memory, + &config.browser, + &config.http_request, + &config.workspace_dir, + &config.agents, + config, + ); + debug!( + tool_count = built.len(), + "[runtime_node::ops] build_runtime_tools: done" + ); + Ok(built) +} + +pub fn list_tools(config: &Config) -> Result, String> { + debug!("[runtime_node::ops] list_tools: start"); + let mut summaries: Vec = build_runtime_tools(config)? + .into_iter() + .map(|tool| summarize_tool(tool.as_ref())) + .collect(); + summaries.sort_by(|a, b| a.name.cmp(&b.name)); + debug!( + count = summaries.len(), + "[runtime_node::ops] list_tools: done" + ); + Ok(summaries) +} + +pub async fn execute_tool( + config: &Config, + tool_name: &str, + args: serde_json::Value, + prefer_markdown: bool, +) -> Result { + debug!( + tool_name, + prefer_markdown, "[runtime_node::ops] execute_tool: start" + ); + let tools = build_runtime_tools(config)?; + trace!( + tool_count = tools.len(), + tool_name, + "[runtime_node::ops] execute_tool: runtime tools built" + ); + let tool = tools + .into_iter() + .find(|tool| tool.name() == tool_name) + .ok_or_else(|| { + debug!( + tool_name, + "[runtime_node::ops] execute_tool: tool not found" + ); + format!("unknown tool `{tool_name}`") + })?; + + let started = Instant::now(); + debug!( + tool_name, + "[runtime_node::ops] execute_tool: publish ToolExecutionStarted" + ); + publish_global(DomainEvent::ToolExecutionStarted { + tool_name: tool_name.to_string(), + session_id: "javascript".to_string(), + }); + + trace!( + tool_name, + "[runtime_node::ops] execute_tool: dispatch execute_with_options" + ); + let execution = tool + .execute_with_options(args, ToolCallOptions { prefer_markdown }) + .await + .map_err(|error| { + debug!( + tool_name, + error = %error, + "[runtime_node::ops] execute_tool: tool execution failed" + ); + format!("tool `{tool_name}` failed: {error:#}") + }); + + let elapsed_ms = started.elapsed().as_millis() as u64; + let success = execution + .as_ref() + .map(|result| !result.is_error) + .unwrap_or(false); + debug!( + tool_name, + success, elapsed_ms, "[runtime_node::ops] execute_tool: publish ToolExecutionCompleted" + ); + publish_global(DomainEvent::ToolExecutionCompleted { + tool_name: tool_name.to_string(), + session_id: "javascript".to_string(), + success, + elapsed_ms, + }); + + let result = execution?; + trace!( + tool_name, + success = !result.is_error, + elapsed_ms, + "[runtime_node::ops] execute_tool: returning outcome" + ); + Ok(ExecuteToolOutcome { + tool_name: tool_name.to_string(), + elapsed_ms, + result, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use async_trait::async_trait; + use serde_json::json; + + struct DummyTool; + + #[async_trait] + impl Tool for DummyTool { + fn name(&self) -> &str { + "dummy_tool" + } + + fn description(&self) -> &str { + "Dummy tool" + } + + fn parameters_schema(&self) -> serde_json::Value { + json!({"type": "object"}) + } + + async fn execute( + &self, + _args: serde_json::Value, + ) -> anyhow::Result { + Ok(crate::openhuman::skills::types::ToolResult::success("ok")) + } + } + + #[test] + fn summarize_tool_exposes_metadata() { + let summary = summarize_tool(&DummyTool); + assert_eq!(summary.name, "dummy_tool"); + assert_eq!(summary.category, "system"); + assert_eq!(summary.permission_level, "ReadOnly"); + assert_eq!(summary.scope, "all"); + } + + #[test] + fn tool_scope_labels_are_stable() { + assert_eq!(tool_scope_label(ToolScope::All), "all"); + assert_eq!(tool_scope_label(ToolScope::AgentOnly), "agent_only"); + assert_eq!(tool_scope_label(ToolScope::CliRpcOnly), "cli_rpc_only"); + } +} diff --git a/src/openhuman/node_runtime/resolver.rs b/src/openhuman/runtime_node/resolver.rs similarity index 100% rename from src/openhuman/node_runtime/resolver.rs rename to src/openhuman/runtime_node/resolver.rs diff --git a/src/openhuman/runtime_node/rpc.rs b/src/openhuman/runtime_node/rpc.rs new file mode 100644 index 000000000..7e4140131 --- /dev/null +++ b/src/openhuman/runtime_node/rpc.rs @@ -0,0 +1,58 @@ +use serde::Deserialize; +use serde_json::{json, Value}; + +use crate::openhuman::config::rpc as config_rpc; +use crate::openhuman::javascript; +use crate::rpc::RpcOutcome; + +#[derive(Debug, Deserialize, Default)] +pub(crate) struct ListToolsParams {} + +#[derive(Debug, Deserialize)] +pub(crate) struct ExecuteToolParams { + pub(crate) tool_name: String, + #[serde(default)] + pub(crate) args: Value, + #[serde(default)] + pub(crate) prefer_markdown: bool, +} + +pub(crate) async fn list_tools_handler( + _params: ListToolsParams, +) -> Result { + let config = config_rpc::load_config_with_timeout().await?; + let tools = javascript::list_tools(&config)?; + let payload = json!({ "tools": tools }); + let log = vec![format!( + "javascript.list_tools: count={}", + payload["tools"] + .as_array() + .map(|tools| tools.len()) + .unwrap_or(0) + )]; + RpcOutcome::new(payload, log).into_cli_compatible_json() +} + +pub(crate) async fn execute_tool_handler( + params: ExecuteToolParams, +) -> Result { + let config = config_rpc::load_config_with_timeout().await?; + let args = if params.args.is_null() { + json!({}) + } else { + params.args + }; + let outcome = + javascript::execute_tool(&config, ¶ms.tool_name, args, params.prefer_markdown).await?; + let payload = json!({ + "tool_name": outcome.tool_name, + "elapsed_ms": outcome.elapsed_ms, + "result": outcome.result, + }); + let log = vec![format!( + "javascript.execute_tool: tool_name={} elapsed_ms={}", + payload["tool_name"].as_str().unwrap_or(""), + payload["elapsed_ms"].as_u64().unwrap_or(0) + )]; + RpcOutcome::new(payload, log).into_cli_compatible_json() +} diff --git a/src/openhuman/runtime_node/schemas.rs b/src/openhuman/runtime_node/schemas.rs new file mode 100644 index 000000000..c2aa8f7d4 --- /dev/null +++ b/src/openhuman/runtime_node/schemas.rs @@ -0,0 +1,142 @@ +use serde_json::{Map, Value}; + +use crate::core::all::{ControllerFuture, RegisteredController}; +use crate::core::{ControllerSchema, FieldSchema, TypeSchema}; +use crate::openhuman::runtime_node::rpc::{ + execute_tool_handler, list_tools_handler, ExecuteToolParams, ListToolsParams, +}; + +pub fn all_controller_schemas() -> Vec { + vec![ + schemas("javascript_list_tools"), + schemas("javascript_execute_tool"), + ] +} + +pub fn all_registered_controllers() -> Vec { + vec![ + RegisteredController { + schema: schemas("javascript_list_tools"), + handler: handle_list_tools, + }, + RegisteredController { + schema: schemas("javascript_execute_tool"), + handler: handle_execute_tool, + }, + ] +} + +pub fn schemas(function: &str) -> ControllerSchema { + match function { + "javascript_list_tools" => ControllerSchema { + namespace: "javascript", + function: "list_tools", + description: "List the agent-callable tool registry exposed through the JavaScript runtime bridge.", + inputs: vec![], + outputs: vec![FieldSchema { + name: "tools", + ty: TypeSchema::Array(Box::new(TypeSchema::Json)), + comment: "List of tool metadata objects: name, description, category, permission_level, scope, supports_markdown, parameters.", + required: true, + }], + }, + "javascript_execute_tool" => ControllerSchema { + namespace: "javascript", + function: "execute_tool", + description: "Execute a named tool through the JavaScript runtime bridge and return its MCP-style ToolResult envelope.", + inputs: vec![ + FieldSchema { + name: "tool_name", + ty: TypeSchema::String, + comment: "Exact tool name returned by javascript.list_tools.", + required: true, + }, + FieldSchema { + name: "args", + ty: TypeSchema::Option(Box::new(TypeSchema::Json)), + comment: "Tool argument object. Defaults to an empty object.", + required: false, + }, + FieldSchema { + name: "prefer_markdown", + ty: TypeSchema::Option(Box::new(TypeSchema::Bool)), + comment: "Hint to tools that can return a compact markdown rendering for LLM consumption.", + required: false, + }, + ], + outputs: vec![ + FieldSchema { + name: "tool_name", + ty: TypeSchema::String, + comment: "Echo of the executed tool name.", + required: true, + }, + FieldSchema { + name: "elapsed_ms", + ty: TypeSchema::U64, + comment: "Wall-clock execution time in milliseconds.", + required: true, + }, + FieldSchema { + name: "result", + ty: TypeSchema::Json, + comment: "MCP-style ToolResult payload: {content, is_error, markdownFormatted?}.", + required: true, + }, + ], + }, + _ => ControllerSchema { + namespace: "javascript", + function: "unknown", + description: "Unknown javascript controller.", + inputs: vec![], + outputs: vec![FieldSchema { + name: "error", + ty: TypeSchema::String, + comment: "Lookup error details.", + required: true, + }], + }, + } +} + +fn handle_list_tools(params: Map) -> ControllerFuture { + Box::pin(async move { + let params: ListToolsParams = + serde_json::from_value(Value::Object(params)).map_err(|error| error.to_string())?; + list_tools_handler(params).await + }) +} + +fn handle_execute_tool(params: Map) -> ControllerFuture { + Box::pin(async move { + let params: ExecuteToolParams = + serde_json::from_value(Value::Object(params)).map_err(|error| error.to_string())?; + execute_tool_handler(params).await + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn catalog_lists_both_runtime_controllers() { + let schemas = all_controller_schemas(); + assert_eq!(schemas.len(), 2); + let names: Vec<&str> = schemas.iter().map(|schema| schema.function).collect(); + assert!(names.contains(&"list_tools")); + assert!(names.contains(&"execute_tool")); + } + + #[test] + fn execute_tool_schema_requires_tool_name() { + let schema = schemas("javascript_execute_tool"); + assert_eq!(schema.namespace, "javascript"); + assert_eq!(schema.function, "execute_tool"); + assert!(schema + .inputs + .iter() + .any(|field| field.name == "tool_name" && field.required)); + } +} diff --git a/src/openhuman/runtime_node/types.rs b/src/openhuman/runtime_node/types.rs new file mode 100644 index 000000000..8fed0ec49 --- /dev/null +++ b/src/openhuman/runtime_node/types.rs @@ -0,0 +1,23 @@ +use serde::Serialize; + +use crate::openhuman::skills::types::ToolResult; + +/// Agent-callable tool metadata exposed through `javascript.list_tools`. +#[derive(Debug, Clone, Serialize)] +pub struct RuntimeToolSummary { + pub name: String, + pub description: String, + pub category: String, + pub permission_level: String, + pub scope: String, + pub supports_markdown: bool, + pub parameters: serde_json::Value, +} + +/// Result of `javascript.execute_tool`. +#[derive(Debug, Clone, Serialize)] +pub struct ExecuteToolOutcome { + pub tool_name: String, + pub elapsed_ms: u64, + pub result: ToolResult, +} diff --git a/src/openhuman/scheduler_gate/gate.rs b/src/openhuman/scheduler_gate/gate.rs index 3eb61d721..b8add05cf 100644 --- a/src/openhuman/scheduler_gate/gate.rs +++ b/src/openhuman/scheduler_gate/gate.rs @@ -335,7 +335,7 @@ pub fn set_signed_out(signed_out: bool) { /// Use this in any test that exercises a code path that itself calls /// [`set_signed_out`] *after* [`init_global`] has promoted [`STATE`]. /// Notably the JSON-RPC server bootstrap (`run_server_embedded` → -/// `bootstrap_skill_runtime` → `register_domain_subscribers`) flips +/// `bootstrap_core_runtime` → `register_domain_subscribers`) flips /// the flag to `true` whenever the workspace has no stored session /// token, which is the common case for tests using a fresh /// `tempfile::tempdir()` workspace. diff --git a/src/openhuman/screen_intelligence/cli/mod.rs b/src/openhuman/screen_intelligence/cli/mod.rs index b60bdb4bf..e2ac3996b 100644 --- a/src/openhuman/screen_intelligence/cli/mod.rs +++ b/src/openhuman/screen_intelligence/cli/mod.rs @@ -1,7 +1,7 @@ //! `openhuman screen-intelligence` — standalone CLI for the screen intelligence loop. //! //! Boots **only** the screen intelligence engine (accessibility capture + local-AI -//! vision) without the full desktop app, Socket.IO, or skills runtime. Useful for +//! vision) without the full desktop app, Socket.IO, or the node/tool runtime. Useful for //! testing the capture → save → vision-analysis pipeline from a terminal. //! //! Usage: @@ -175,7 +175,7 @@ pub(super) fn is_help(value: &str) -> bool { fn print_help() { println!("openhuman screen-intelligence — standalone screen intelligence runtime\n"); println!("Boots only the screen intelligence engine (accessibility capture + local-AI"); - println!("vision) without the full desktop app, Socket.IO, or skills runtime.\n"); + println!("vision) without the full desktop app, Socket.IO, or the node/tool runtime.\n"); println!("Usage:"); println!(" openhuman screen-intelligence run [--ttl ] [--no-vision-model] [-v]"); println!(" openhuman screen-intelligence status [-v]"); diff --git a/src/openhuman/skills/README.md b/src/openhuman/skills/README.md index ad0ca2457..2ce896088 100644 --- a/src/openhuman/skills/README.md +++ b/src/openhuman/skills/README.md @@ -1,6 +1,6 @@ # Skills -Discovery, parsing, and per-turn injection of agentskills.io-style skills (a directory containing `SKILL.md` with YAML frontmatter and Markdown instructions). Owns scope resolution (User vs Project vs Legacy), trust-marker enforcement, resource reading, install / uninstall, and the matching heuristic that decides which `SKILL.md` body to splice into a chat turn. Does NOT own runtime execution internals (the `rquickjs` engine that runs skill JS lives elsewhere) or general tool execution (`tools/`). +Discovery, parsing, and per-turn injection of agentskills.io-style skills (a directory containing `SKILL.md` with YAML frontmatter and Markdown instructions). Owns scope resolution (User vs Project vs Legacy), trust-marker enforcement, resource reading, install / uninstall, and the matching heuristic that decides which `SKILL.md` body to splice into a chat turn. Does NOT own runtime execution internals or general tool execution (`tools/` / `javascript/`). ## Public surface diff --git a/src/openhuman/skills/mod.rs b/src/openhuman/skills/mod.rs index 85092e291..c1a4da1d9 100644 --- a/src/openhuman/skills/mod.rs +++ b/src/openhuman/skills/mod.rs @@ -1,4 +1,4 @@ -//! Legacy skill metadata helpers retained after QuickJS runtime removal. +//! Skill metadata helpers and prompt-injection support. pub mod bus; pub mod inject; diff --git a/src/openhuman/skills/types.rs b/src/openhuman/skills/types.rs index ca8a77613..ee733ba41 100644 --- a/src/openhuman/skills/types.rs +++ b/src/openhuman/skills/types.rs @@ -1,4 +1,4 @@ -//! Shared tool result types retained after QuickJS runtime removal. +//! Shared tool result types used by the tool and node runtime surfaces. use serde::{Deserialize, Serialize}; diff --git a/src/openhuman/tool_timeout/mod.rs b/src/openhuman/tool_timeout/mod.rs index d8d96b794..5cab6987c 100644 --- a/src/openhuman/tool_timeout/mod.rs +++ b/src/openhuman/tool_timeout/mod.rs @@ -1,4 +1,4 @@ -//! Wall-clock timeouts for tool execution (skills runtime + agent loop). +//! Wall-clock timeouts for tool execution (node/tool runtime + agent loop). //! //! Override with the `OPENHUMAN_TOOL_TIMEOUT_SECS` environment variable (1–3600; default 120). diff --git a/src/openhuman/tools/impl/system/node_exec.rs b/src/openhuman/tools/impl/system/node_exec.rs index 6c0565853..b0befe279 100644 --- a/src/openhuman/tools/impl/system/node_exec.rs +++ b/src/openhuman/tools/impl/system/node_exec.rs @@ -4,7 +4,7 @@ //! Sibling to [`crate::openhuman::tools::impl::system::shell::ShellTool`]: same //! security gates, same env hygiene, but the command is pinned to the `node` //! binary resolved by -//! [`crate::openhuman::node_runtime::NodeBootstrap`]. +//! [`crate::openhuman::javascript::NodeBootstrap`]. //! //! Two input modes: //! @@ -22,7 +22,7 @@ //! `PATH`. Subsequent calls reuse the cached install. use crate::openhuman::agent::host_runtime::RuntimeAdapter; -use crate::openhuman::node_runtime::NodeBootstrap; +use crate::openhuman::javascript::NodeBootstrap; use crate::openhuman::security::SecurityPolicy; use crate::openhuman::tools::traits::{Tool, ToolResult}; use async_trait::async_trait; diff --git a/src/openhuman/tools/impl/system/npm_exec.rs b/src/openhuman/tools/impl/system/npm_exec.rs index 4e5e9bda9..a1d1b80ab 100644 --- a/src/openhuman/tools/impl/system/npm_exec.rs +++ b/src/openhuman/tools/impl/system/npm_exec.rs @@ -2,7 +2,7 @@ //! toolchain. //! //! Thin wrapper over `npm ` that piggybacks on -//! [`crate::openhuman::node_runtime::NodeBootstrap`] for binary resolution. +//! [`crate::openhuman::javascript::NodeBootstrap`] for binary resolution. //! Same security posture as //! [`crate::openhuman::tools::impl::system::shell::ShellTool`] and //! [`crate::openhuman::tools::impl::system::node_exec::NodeExecTool`]: @@ -18,7 +18,7 @@ //! POSIX-safe single-quoting. use crate::openhuman::agent::host_runtime::RuntimeAdapter; -use crate::openhuman::node_runtime::NodeBootstrap; +use crate::openhuman::javascript::NodeBootstrap; use crate::openhuman::security::SecurityPolicy; use crate::openhuman::tools::traits::{Tool, ToolResult}; use async_trait::async_trait; diff --git a/src/openhuman/tools/impl/system/shell.rs b/src/openhuman/tools/impl/system/shell.rs index 1c0ed9afb..bde7527fc 100644 --- a/src/openhuman/tools/impl/system/shell.rs +++ b/src/openhuman/tools/impl/system/shell.rs @@ -1,5 +1,5 @@ use crate::openhuman::agent::host_runtime::RuntimeAdapter; -use crate::openhuman::node_runtime::NodeBootstrap; +use crate::openhuman::javascript::NodeBootstrap; use crate::openhuman::security::SecurityPolicy; use crate::openhuman::tools::traits::{Tool, ToolResult}; use async_trait::async_trait; diff --git a/src/openhuman/tools/ops.rs b/src/openhuman/tools/ops.rs index bef42bd6c..53ed828fb 100644 --- a/src/openhuman/tools/ops.rs +++ b/src/openhuman/tools/ops.rs @@ -2,8 +2,8 @@ use super::*; use crate::openhuman::agent::host_runtime::{NativeRuntime, RuntimeAdapter}; use crate::openhuman::config::{Config, DelegateAgentConfig}; +use crate::openhuman::javascript::NodeBootstrap; use crate::openhuman::memory::Memory; -use crate::openhuman::node_runtime::NodeBootstrap; use crate::openhuman::security::SecurityPolicy; use std::collections::HashMap; use std::sync::Arc; diff --git a/src/openhuman/webhooks/bus.rs b/src/openhuman/webhooks/bus.rs index bb68752c3..f72739eea 100644 --- a/src/openhuman/webhooks/bus.rs +++ b/src/openhuman/webhooks/bus.rs @@ -190,9 +190,9 @@ impl EventHandler for WebhookRequestSubscriber { } } Some(ref reg) => { - // skill target kind or any other unrecognised kind — skill runtime not available + // skill target kind or any other unrecognised kind — direct skill dispatch is not available tracing::debug!( - "[webhook] skill tunnel {} (kind={}) — skill runtime not available", + "[webhook] skill tunnel {} (kind={}) — direct skill dispatch not available", tunnel_uuid, reg.target_kind, ); @@ -200,12 +200,12 @@ impl EventHandler for WebhookRequestSubscriber { correlation_id: correlation_id.clone(), status_code: 501, headers: HashMap::new(), - body: error_body("Skill runtime not available for direct dispatch"), + body: error_body("Direct skill dispatch is not available"), }; ( resp, Some(reg.skill_id.clone()), - Some("skill runtime not available".to_string()), + Some("direct skill dispatch not available".to_string()), ) } None => { diff --git a/src/openhuman/webhooks/ops.rs b/src/openhuman/webhooks/ops.rs index 54c3a31ec..ca042c831 100644 --- a/src/openhuman/webhooks/ops.rs +++ b/src/openhuman/webhooks/ops.rs @@ -136,7 +136,7 @@ pub async fn unregister_echo( /// Register an agent-backed webhook tunnel. /// /// Incoming requests on this tunnel will be routed to the triage -/// pipeline instead of the (removed) skill runtime. +/// pipeline instead of direct skill dispatch. pub async fn register_agent( tunnel_uuid: &str, agent_id: Option, diff --git a/src/openhuman/webhooks/router.rs b/src/openhuman/webhooks/router.rs index a88a0c5e6..3761e865e 100644 --- a/src/openhuman/webhooks/router.rs +++ b/src/openhuman/webhooks/router.rs @@ -128,7 +128,7 @@ impl WebhookRouter { /// Register an agent-backed webhook tunnel. /// /// Requests arriving on this tunnel are routed into the triage - /// pipeline rather than the skill runtime. `agent_id` is stored + /// pipeline rather than direct skill dispatch. `agent_id` is stored /// for observability and rebind validation; the triage evaluator /// currently selects the target agent dynamically regardless of /// this value. diff --git a/src/openhuman/webhooks/schemas.rs b/src/openhuman/webhooks/schemas.rs index 7de221a38..2f60a13a8 100644 --- a/src/openhuman/webhooks/schemas.rs +++ b/src/openhuman/webhooks/schemas.rs @@ -210,7 +210,7 @@ pub fn schemas(function: &str) -> ControllerSchema { function: "register_agent", description: "Register an agent-backed webhook tunnel. Incoming requests on this tunnel \ - are routed to the triage pipeline instead of the skill runtime.", + are routed to the triage pipeline instead of direct skill dispatch.", inputs: vec![ FieldSchema { name: "tunnel_uuid",