# OmniRoute Skills Framework: Building Custom Skills with Sandbox Isolation

> Explore the OmniRoute Skills framework to build custom skills with robust sandbox isolation. Safely run custom logic in routing pipelines using container-based isolation.

- Repository: [Diego Rodrigues de Sa e Souza/OmniRoute](https://github.com/diegosouzapw/OmniRoute)
- Tags: deep-dive
- Published: 2026-08-08

---

**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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/sandbox.ts)) provides optional container-based isolation for handlers requiring separate OS environments. It discovers available container runtimes through [`containerProvider.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/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`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/registry.ts):

```typescript
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`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/executor.ts):

```typescript
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:

```typescript
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:

```typescript
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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/registry.ts) with JSON schema validation, then executed via `skillExecutor.execute` in [`src/lib/skills/executor.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/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`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/sandbox.ts) and [`src/lib/skills/containerProvider.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/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`](https://github.com/diegosouzapw/OmniRoute/blob/main/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`](https://github.com/diegosouzapw/OmniRoute/blob/main/src/lib/skills/sandbox.ts), which leverages [`src/lib/skills/containerProvider.ts`](https://github.com/diegosouzapw/OmniRoute/blob/main/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.