How OmniRoute's Skills System Works: Architecture and Sandbox Isolation Explained

The skills system in OmniRoute provides a secure, extensible framework for executing reusable capabilities like file I/O and HTTP requests, using a registry-based architecture and Docker-based sandbox isolation to ensure untrusted code runs with strict resource limits and filesystem separation.

The OmniRoute repository (diegosouzapw/OmniRoute) implements a powerful skills system that allows the router to expose reusable capabilities to downstream clients while maintaining rigorous security boundaries. This system combines a flexible handler registry with containerized execution environments to safely process both trusted operations and untrusted code.

Core Architecture of the OmniRoute Skills System

The Skill Registry (src/lib/skills/registry.ts)

At the heart of the system lies the Skill Registry, a centralized map that associates string identifiers with their corresponding handler functions. This registry serves as the lookup mechanism for the A2A server and MCP server when clients invoke capabilities via the skills_execute tool.

The registry is implemented as a simple object that exports a skillRegistry constant:

// src/lib/skills/registry.ts (excerpt)
import { builtinSkills } from "./builtins";
export const skillRegistry = {
  ...builtinSkills,
  // custom skills can be added here at runtime
};

When a request arrives containing a skill field, OmniRoute resolves the appropriate SkillHandler (type defined in src/lib/skills/types.ts) from this registry and validates the input payload before execution.

Built-in Skill Handlers (src/lib/skills/builtins.ts)

The built-in skill set covers common operational patterns through async handler functions. Each handler receives a validated input payload and a request-context object containing the API-key identifier, enabling per-caller isolation.

The default capabilities include:

  • file_read: Validates relative workspace paths, enforces size limits, and reads content from the skill workspace
  • file_write: Validates paths and writes content atomically while respecting size constraints
  • http_request: Sanitizes headers, caps allowed methods, and streams responses with configurable byte limits via safeOutboundFetch
  • web_search: Delegates to the internal search engine through executeWebSearch
  • web_fetch: Wraps generic fetch operations with safety checks

All handlers access the context.apiKeyId property, which is hashed using SHA-256 to generate a per-API-key workspace directory via getWorkspaceRoot. This ensures filesystem operations remain strictly scoped to the caller's namespace.

How Sandbox Isolation Works in OmniRoute

The SandboxRunner Singleton (src/lib/skills/sandbox.ts)

For operations requiring execution of untrusted code, OmniRoute employs the SandboxRunner, a process-wide singleton that manages containerized execution environments. The runner is exported as sandboxRunner and instantiated via SandboxRunner.getInstance():

// src/lib/skills/sandbox.ts (excerpt)
export const sandboxRunner = SandboxRunner.getInstance();

The runner tracks container lifecycles through an internal runningContainers map and exposes methods including run(), kill(), killAll(), isRunning(), and getRunningCount() for execution management and monitoring.

ContainerProvider Abstraction (src/lib/skills/containerProvider.ts)

The sandbox system abstracts container runtime details through the ContainerProvider interface defined in src/lib/skills/containerProvider.ts. This abstraction supports multiple backends including Docker and Podman, allowing the runner to construct proper execution commands without hardcoding runtime-specific logic.

The provider handles the construction of docker run (or equivalent) commands and implements provider-specific kill mechanisms for graceful or forceful container termination.

Resource Constraints and Configuration

Every sandbox execution receives a SandboxConfig object that enforces strict resource boundaries. The configuration interface supports:

  • cpuLimit: CPU percentage caps (e.g., 200 for 200% of one CPU)
  • memoryLimit: Memory allocation in megabytes
  • timeout: Execution deadline in milliseconds
  • networkEnabled: Boolean flag controlling network access
  • readOnly: Flag mounting the filesystem as read-only

Default limits are defined in sandbox.ts as DEFAULT_CONFIG, while individual skill handlers can override these values per-invocation using the sandboxConfig helper.

Execution Lifecycle

When a skill requests sandbox execution, the runner executes the following sequence (lines 73-115 in sandbox.ts):

  1. Generates a UUID using randomUUID() as the container identifier
  2. Calls the provider's buildRun method to construct the execution command and arguments
  3. Spawns the process using child_process.spawn, piping stdout and stderr
  4. Sets a timeout; if expiration occurs, invokes kill() to send SIGTERM and execute provider-specific cleanup
  5. Resolves a SandboxResult containing exit code, captured output, duration, and a killed flag

Workspace Isolation and Security Boundaries

Beyond containerization, OmniRoute implements workspace isolation at the filesystem level. The resolveWorkspacePath function in builtins.ts ensures each API key receives a dedicated workspace directory derived from the SHA-256 hash of the apiKeyId.

This design guarantees that:

  • Skills cannot traverse outside their assigned workspace
  • File read/write operations are scoped to the specific caller
  • Multiple clients executing simultaneously remain completely isolated at the storage layer

Practical Examples

Invoking a Built-in Skill

To execute a skill programmatically from client code:

import { skillRegistry } from "./src/lib/skills/registry";

async function readFileDemo(apiKeyId: string) {
  const handler = skillRegistry["file_read"];
  const result = await handler(
    { path: "notes/todo.txt", encoding: "utf8" },
    { apiKeyId }
  );
  console.log(result.content);
}

The file_read handler implementation resides in src/lib/skills/builtins.ts (lines 98-124).

Running Custom Commands in a Sandbox

For untrusted code execution, invoke the sandbox runner directly with custom constraints:

import { sandboxRunner, type SandboxConfig } from "./src/lib/skills/sandbox";

async function runLs() {
  const cfg: SandboxConfig = {
    cpuLimit: 200,          // 200% of one CPU
    memoryLimit: 128,       // MB
    timeout: 5000,          // ms
    networkEnabled: false,
    readOnly: true,
  };
  const result = await sandboxRunner.run(
    "alpine:3.20",                 // image (allowed by default)
    ["ls", "-l", "/workspace"],   // command inside the container
    {},                           // extra env vars
    cfg
  );
  console.log(result.stdout);
}

The run method is defined in sandbox.ts (lines 67-115).

Registering Custom Skills at Runtime

Extend the system by adding handlers to the registry dynamically:

import { skillRegistry } from "./src/lib/skills/registry";

skillRegistry["echo"] = async (input, context) => ({
  success: true,
  echoed: input.message,
  context: context.apiKeyId,
});

Custom skills become immediately available to all clients through the same skillRegistry object.

Request Flow: From Client to Container

The complete execution path demonstrates how the skills system integrates with sandbox isolation:

  1. Client request includes skill: "http_request" and the required payload
  2. Route handler extracts the skill name and performs a lookup in skillRegistry
  3. The selected handler validates inputs and determines execution mode (direct or sandbox)
  4. If sandbox mode is active (defaultMode === "sandbox" or user-specified), the handler invokes sandboxRunner.run(image, cmd, env, sandboxConfig)
  5. The ContainerProvider executes the container with strict resource caps and workspace mounting
  6. Upon completion, the SandboxResult is formatted into the standard skill response structure ({ success, ... }) and returned to the client

Summary

  • The skills system in OmniRoute centers on a registry pattern mapping skill names to handler functions in src/lib/skills/registry.ts, supporting both built-in and dynamically registered capabilities.
  • Sandbox isolation is achieved through the SandboxRunner singleton in src/lib/skills/sandbox.ts, which spawns containerized environments with configurable CPU, memory, timeout, and network restrictions.
  • Workspace isolation uses SHA-256 hashed API keys to create per-caller filesystem boundaries, preventing unauthorized file access across different clients.
  • The ContainerProvider abstraction in src/lib/skills/containerProvider.ts enables support for multiple container runtimes (Docker, Podman) without modifying core execution logic.
  • All built-in skills in src/lib/skills/builtins.ts receive a SkillContext object containing the hashed API key, enabling consistent security boundaries across file operations, HTTP requests, and custom code execution.

Frequently Asked Questions

What is the skills system in OmniRoute?

The skills system in OmniRoute is a modular framework that exposes reusable capabilities—such as file I/O, HTTP requests, and code execution—to downstream clients through a centralized registry. It allows both built-in operations and custom handlers to be registered at runtime, providing a consistent interface for capability invocation across A2A and MCP protocols.

How does OmniRoute achieve sandbox isolation?

OmniRoute achieves sandbox isolation through the SandboxRunner class, which spawns lightweight containers using Docker or Podman via the ContainerProvider abstraction. Each container runs with strict resource limits defined by SandboxConfig, including CPU percentage caps, memory limits, network restrictions, and timeout enforcement. The runner manages container lifecycles, automatically kills processes exceeding timeouts, and ensures filesystem access is restricted to per-API-key workspace directories.

What resource limits can be configured for sandboxes?

According to the SandboxConfig interface in src/lib/skills/sandbox.ts, administrators can configure cpuLimit (percentage of CPU), memoryLimit (megabytes), timeout (milliseconds), networkEnabled (boolean), and readOnly (boolean). These defaults can be overridden per-invocation, allowing fine-grained control over execution environments based on the specific requirements of each skill.

How are files isolated between different API keys?

Files are isolated through a workspace naming scheme that generates unique directories based on the SHA-256 hash of each apiKeyId. The getWorkspaceRoot function in src/lib/skills/builtins.ts resolves these paths, ensuring that handlers can only access files within their specific hashed workspace directory. This cryptographic separation guarantees that no skill can read or write files belonging to a different API key, even when executing concurrently on the same host.

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 →