How the OpenHuman Skills Runtime Sandbox Executes QuickJS Skill Code

OpenHuman uses an isolated QuickJS sandbox built on the rquickjs Rust crate to compile, load, and execute user-contributed skill code with process-level isolation and curated host bindings.

The tinyhumansai/openhuman repository implements a secure skills runtime that leverages QuickJS to run user-contributed TypeScript bundles in a controlled environment. This system creates distinct execution contexts for each skill, ensuring that untrusted code cannot access host memory or interfere with other running skills while still providing controlled access to logging and RPC capabilities.

Skills Architecture Overview

OpenHuman separates skill packages from the core application by storing them in the tinyhumansai/openhuman-skills registry repository. The core application pulls compiled TypeScript bundles from this external registry at runtime rather than embedding them directly in the binary.

According to the Skills registry section in [AGENTS.md](https://github.com/tinyhumansai/openhuman/blob/main/AGENTS.md), this separation allows skills to be developed, versioned, and distributed independently from the runtime engine. The core application discovers available skills through the registry and fetches the compiled JavaScript bundles on demand.

QuickJS Sandbox Implementation

The sandbox implementation resides in src/openhuman/skills/ and utilizes the rquickjs crate to create secure JavaScript execution environments.

Creating the Isolated Runtime

The primary logic for initializing the sandbox lives in src/openhuman/skills/ops.rs. When a skill is loaded, the runtime calls SkillRuntime::new(), which performs the following:

  1. Creates a new rquickjs::Runtime instance dedicated to that specific skill
  2. Spins up a Context within that runtime for code evaluation
  3. Compiles the JavaScript bundle source into QuickJS bytecode

Each skill receives its own Runtime + Context pair, guaranteeing memory isolation. A skill cannot access global state from the host process or other skills, as the QuickJS engine runs within a sandboxed heap distinct from the host Rust application.

Host Bindings and Security Model

The sandbox exposes only a curated subset of host functions to the JavaScript environment. According to the implementation in src/openhuman/skills/ops.rs, the runtime registers specific bindings for operations like logging and RPC access while explicitly excluding Node.js, Electron, or browser APIs.

This restricted binding model ensures that skill code operates within a minimal capability set. The JavaScript entry point (main.js) executes without access to the file system, network (unless explicitly proxied through the RPC interface), or other sensitive host resources.

Execution Flow

The lifecycle of a skill spans discovery, initialization, execution, and cleanup across multiple source files.

Skill Discovery and Loading

In src/openhuman/skills/ops_discover.rs, the core implements the discovery logic:

  1. Queries the skills registry for available bundles
  2. Downloads the compiled TypeScript/JavaScript bundle to local storage
  3. Invokes SkillRuntime::new() to instantiate the QuickJS context
  4. Evaluates the skill's main.js entry point within the sandbox

Once initialized, the skill registers callback handlers such as onMessage or onSchedule with the runtime. The core later invokes these registered functions via the same QuickJS context, marshaling arguments from Rust into JavaScript values and returning results back to the host.

Cleanup and Lifecycle Management

The runtime ensures safe resource destruction through the skill cleanup subscriber implemented in src/openhuman/skills/bus.rs. While currently a no-op placeholder (see lines 1-17), this subscriber preserves the contract that cleanup is side-effect-free and will eventually handle the explicit dropping of the Runtime when a skill is unloaded.

When the SkillRuntime object goes out of scope in Rust, the underlying QuickJS Runtime and its associated Context are automatically dropped, freeing all memory associated with the skill's execution environment.

Code Example: Loading and Running a Skill Bundle

The following Rust code demonstrates how to instantiate the skills runtime sandbox and execute skill code:

use openhuman::skills::{SkillRuntime, SkillBundle};
use std::path::Path;

fn load_and_run_skill(bundle_path: &Path) -> anyhow::Result<()> {
    // 1. Read the bundled JS source (generated by the external skills repo)
    let bundle = SkillBundle::from_path(bundle_path)?;

    // 2. Create an isolated QuickJS runtime for this skill
    let mut runtime = SkillRuntime::new(&bundle)?;

    // 3. Initialise the skill (executes its main.js)
    runtime.execute_entrypoint()?;

    // 4. Call a skill-exposed function, e.g. `runTask`
    let result: String = runtime.call("runTask", ("hello",))?;
    println!("Skill returned: {result}");

    // Runtime is automatically cleaned up when it goes out of scope.
    Ok(())
}

This pattern ensures that each skill operates within its own isolated QuickJS environment, with the SkillBundle providing the code and SkillRuntime managing the sandbox lifecycle.

Summary

  • Process isolation: Each skill runs in a dedicated rquickjs::Runtime with its own Context, preventing cross-skill memory access
  • Curated bindings: The sandbox in src/openhuman/skills/ops.rs exposes only whitelisted host functions (logging, RPC) while blocking system access
  • Registry separation: Skill code lives in tinyhumansai/openhuman-skills and is fetched at runtime by the discovery logic in ops_discover.rs
  • Safe cleanup: The bus.rs subscriber ensures that QuickJS runtimes are properly dropped when skills unload, with placeholder infrastructure for future event-bus integration

Frequently Asked Questions

How does OpenHuman prevent malicious skill code from accessing the file system?

The QuickJS sandbox explicitly does not expose Node.js or standard library APIs to the JavaScript environment. The runtime in src/openhuman/skills/ops.rs only registers specific host bindings for logging and RPC communication, meaning JavaScript code has no native mechanism to read files or open network sockets unless the host application explicitly provides such capabilities through the restricted binding interface.

Can skills communicate with each other directly within the QuickJS sandbox?

No. Each skill receives its own isolated rquickjs::Runtime instance created by SkillRuntime::new() in ops.rs. These runtimes cannot access each other's memory or global objects. If skills need to communicate, they must do so through the host application's mediated channels, such as the RPC bindings registered during sandbox initialization.

What happens if a skill enters an infinite loop or hangs during execution?

While the current implementation creates isolated QuickJS contexts, specific execution timeouts or CPU throttling mechanisms would need to be implemented at the host level. The rquickjs crate supports interrupt handlers that the OpenHuman runtime could leverage to terminate runaway scripts, though the specific timeout policy would be configured in the SkillRuntime initialization logic within src/openhuman/skills/ops.rs.

Where is the actual skill code stored if not in the main repository?

Skill packages reside in the separate tinyhumansai/openhuman-skills repository. As documented in AGENTS.md, the core application treats this as an external registry, downloading compiled TypeScript bundles at runtime when the discovery process in src/openhuman/skills/ops_discover.rs identifies a skill that needs to be loaded.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →