OmniRoute Skills Framework: Building Custom Skills with Sandbox Isolation

OmniRoute ships a production-grade Skills framework that combines a versioned in-memory registry, robust execution engine with timeout handling, and optional container-based sandbox isolation to safely run custom logic within routing pipelines.

The OmniRoute Skills framework enables developers to define, persist, and execute reusable code modules called skills directly from HTTP routing pipelines. Built as a set of singleton services in TypeScript, the framework separates skill metadata management from runtime execution while providing optional process isolation for untrusted or resource-intensive operations via Docker-compatible containers.

Core Architecture

The framework centers on three singleton components that manage the complete skill lifecycle from registration to execution.

Skill Registry

The Skill Registry (src/lib/skills/registry.ts) maintains an in-memory cache of skill definitions backed by a SQLite skills table. It handles semantic versioning constraints (^, ~, >, etc.) and auto-invalidates its cache every 60 seconds via loadFromDatabase. Operators interact with it through SkillRegistry.getInstance() to register new skills or resolve existing ones by name and version.

Skill Executor

The Skill Executor (src/lib/skills/executor.ts) orchestrates runtime invocation. When skillExecutor.execute is called, it validates that settings.skillsEnabled is true, retrieves the skill from the registry, and invokes the corresponding handler function within a Promise.race against a configurable timeout (default 30 seconds). The executor persists execution logs to the skill_executions table, capturing success states, error messages, and timing metrics for audit trails and retry logic.

Sandbox Runner

The Sandbox Runner (src/lib/skills/sandbox.ts) provides optional container-based isolation for handlers requiring separate OS environments. It discovers available container runtimes through containerProvider.ts (supporting Docker, Podman, etc.) and spawns isolated processes with strict resource limits. By default, sandboxes enforce CPU limits of 100%, memory caps of 256 MiB, 30-second timeouts, and read-only filesystems to prevent container escape or resource exhaustion attacks.

How Skills Flow Through the System

A skill progresses through six distinct stages when invoked:

  1. Registration – An API call or internal process invokes skillRegistry.register, validating input against SkillCreateInputSchema and storing metadata including the handler function name (e.g., "myCustomHandler"), version, and JSON schema.

  2. Loading – On first use or when the 60-second TTL expires, skillRegistry.loadFromDatabase populates in-memory maps (registeredSkills and versionCache) from the SQLite backend.

  3. Resolution – The executor receives a request to run a skill by name (optionally qualified with version like skillName@1.0.0). It verifies the skill exists and is enabled.

  4. Execution – The executor looks up the concrete handler in its internal map (handlers.get(skill.handler)), which plugins or core code register at startup via skillExecutor.registerHandler. The handler executes with a timeout guard.

  5. Sandbox (optional) – Handlers requiring isolation call sandboxRunner.run(image, command, env, config), which tracks container IDs, exit codes, and stdout/stderr streams while enforcing resource limits.

  6. Result persistence – The executor writes execution results to skill_executions, including status (SUCCESS or ERROR), output payload, and error messages, enabling UI monitoring and automated retry logic.

Implementing Custom Skills

Register a Skill Definition

Define the skill metadata and input schema in src/lib/skills/registry.ts:

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

await skillRegistry.register({
  name: "summarizeText",
  version: "1.0.0",
  description: "Summarizes a block of text using a LLM",
  schema: { 
    type: "object", 
    properties: { text: { type: "string" } }, 
    required: ["text"] 
  },
  handler: "summarizeHandler",
  enabled: true,
  apiKeyId: "default",
  mode: "on",
  sourceProvider: "local",
  tags: ["nlp", "llm"],
});

Register the Handler Function

Map the handler name to an implementation function in src/lib/skills/executor.ts:

import { skillExecutor } from "./src/lib/skills/executor";
import { sandboxRunner } from "./src/lib/skills/sandbox";

skillExecutor.registerHandler("summarizeHandler", async (input, ctx) => {
  // Run the LLM in a sandbox for isolation
  const { stdout } = await sandboxRunner.run(
    "omniroute-llm-image",
    ["node", "summarize.js"],
    { INPUT_TEXT: input.text as string },
  );
  return { summary: stdout.trim() };
});

Execute from a Route

Invoke the skill by name from within a route handler:

import { skillExecutor } from "./src/lib/skills/executor";

const result = await skillExecutor.execute(
  "summarizeText@1.0.0",
  { text: "Long article body…" },
  { apiKeyId: "default", sessionId: "sess-123" },
);

if (result.status === "SUCCESS") {
  console.log("Summary:", result.output?.summary);
} else {
  console.error("Skill failed:", result.errorMessage);
}

Run Sandbox Commands Directly

For ad-hoc isolated execution without the registry overhead:

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

const sandboxResult = await sandboxRunner.run(
  "python:3.11-slim",
  ["python", "-c", "print('Hello from sandbox')"],
);

console.log(sandboxResult.stdout); // → Hello from sandbox

Sandbox Isolation Configuration

The sandbox runner defined in src/lib/skills/sandbox.ts uses DEFAULT_CONFIG to enforce security boundaries:

  • CPU: 100% (one full core)
  • Memory: 256 MiB hard limit
  • Timeout: 30 seconds maximum execution time
  • Filesystem: Read-only root filesystem
  • Networking: Restricted to internal networks (configurable via containerProvider.ts)

These defaults prevent runaway processes from destabilizing the OmniRoute host while allowing sufficient resources for typical data transformation and LLM inference tasks.

Summary

  • OmniRoute Skills framework provides a three-layer architecture: the Skill Registry for versioned metadata caching, the Skill Executor for timeout-guarded invocation, and the Sandbox Runner for container isolation.
  • Skills are registered via skillRegistry.register in src/lib/skills/registry.ts with JSON schema validation, then executed via skillExecutor.execute in src/lib/skills/executor.ts.
  • The sandbox enforces strict resource limits (256 MiB RAM, 30s timeout, read-only FS) through src/lib/skills/sandbox.ts and src/lib/skills/containerProvider.ts.
  • All components are singletons (getInstance() pattern) ensuring low-overhead access across the server process with automatic 60-second cache invalidation.

Frequently Asked Questions

How does OmniRoute handle skill versioning?

The Skill Registry supports semantic versioning constraints including caret (^), tilde (~), and comparison operators (>, <). When executing a skill, you can specify an exact version (skillName@1.0.0) or rely on the registry to resolve compatible versions from the versionCache map, which refreshes every 60 seconds from the SQLite skills table.

What are the default resource limits for sandboxed skills?

According to src/lib/skills/sandbox.ts, the DEFAULT_CONFIG enforces CPU limits of 100%, memory allocation of 256 MiB, a 30-second execution timeout, and a read-only filesystem. These limits apply to all containers spawned by sandboxRunner.run, preventing resource exhaustion attacks while providing sufficient capacity for most computational tasks.

Can custom skills invoke other skills within the framework?

Yes. Since the Skill Executor is a singleton accessible via SkillExecutor.getInstance(), handler functions registered with skillExecutor.registerHandler can recursively call skillExecutor.execute to invoke other skills. The executor maintains the full execution context including apiKeyId and sessionId through the execution chain, with each invocation creating a separate entry in the skill_executions audit table.

How does the framework isolate untrusted code?

Untrusted code runs inside containerized sandboxes managed by src/lib/skills/sandbox.ts, which leverages src/lib/skills/containerProvider.ts to detect available runtimes (Docker, Podman). The sandbox spawns processes with restricted capabilities, resource quotas, and filesystem permissions, killing any process exceeding the 30-second timeout or 256 MiB memory limit while capturing stdout and stderr for debugging.

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 →