OmniRoute Skills System Architecture for Extensible Functionality Explained
OmniRoute implements a registry-executor-handler pattern that isolates custom logic in versioned, schema-validated skills with SQLite-backed persistence and audit trails.
OmniRoute’s extensible functionality is powered by a modular skills system architecture that treats custom business logic as plug-in units. This TypeScript framework enables developers to register reusable handlers, enforce input/output contracts via JSON schemas, and execute code within a sandboxed, observable environment. The architecture separates skill metadata management from execution, allowing the routing engine, MCP tools, and A2A protocol to safely invoke third-party capabilities without compromising core stability.
Core Components of the Skills Architecture
Skill Definition and Schema Validation
At the foundation of the system lies the Skill interface defined in src/lib/skills/types.ts. This contract specifies every skill’s name, version, JSON schema (SkillSchema), and a reference to its handler function. The schema strictly defines expected input and output shapes, enabling validation before execution reaches custom code.
Skill records also carry metadata including enablement status, execution mode, tags, and source provider. This declarative approach ensures that the routing engine can inspect capabilities without loading implementation code, supporting features like semantic version resolution and dependency checking.
The SkillRegistry Singleton
The SkillRegistry class in src/lib/skills/registry.ts operates as a singleton that maintains an in-memory cache of all registered skills. It implements a sophisticated caching strategy:
- Key structure: Cache entries are keyed by
apiKeyId:name@version, ensuring per-API-key isolation. - Lazy loading: Skills are loaded on demand from the SQLite
skillstable vialoadFromDatabase, with deduplication of concurrent loads using apendingLoadpromise. - TTL-based staleness: The registry supports configurable
cacheTTLto refresh data without blocking requests. - Version resolution: The
resolveVersionmethod supports semantic constraints (e.g.,^1.2.0,~2.0.3) to select compatible skill versions dynamically.
The registry exposes CRUD APIs including register, unregister, list, and getSkill, automatically rebuilding internal version caches when mutations occur.
The SkillExecutor and Audit Layer
The SkillExecutor singleton in src/lib/skills/executor.ts serves as the secure runtime boundary for skill code. Before invoking any handler, it verifies that the global skillsEnabled flag is active and that the target skill is both registered and enabled.
Execution is fully audited: the executor inserts a row into the skill_executions table to track start time, input payload, and status, then updates this record with output data, error messages, and duration upon completion. The system supports configurable timeouts (setTimeout) and retry counts (setMaxRetries), with handler resolution occurring via a Map<string, SkillHandler> where keys correspond to handler names stored in skill records.
Runtime Extension Mechanisms
Handler Registration and Built-ins
Skill handlers are ordinary async functions receiving a validated input object and a context containing apiKeyId and optional sessionId. They return plain objects conforming to the declared output schema. Runtime registration occurs via skillExecutor.registerHandler(name, fn).
Built-in handlers (e.g., browser, a2a) reside in src/lib/skills/builtins.ts and auto-register on server startup. This pattern allows the core team to ship default capabilities while maintaining the same extension interface available to third-party developers.
Interception and Injection Hooks
The architecture supports aspect-oriented extensions through two specialized modules:
src/lib/skills/interception.ts: Allows skills to modify requests before they reach downstream executors (e.g., altering prompts or injecting headers).src/lib/skills/injection.ts: Enables response enrichment, letting skills augment output during translation phases.
These hooks extend functionality without modifying core routing logic, adhering to the open/closed principle.
Persistence and Configuration
SQLite Storage Layer
All skill metadata and execution logs persist in SQLite tables (skills and skill_executions). Database access is abstracted through src/lib/db/core.ts, which exports a singleton better-sqlite3 instance. This design ensures ACID-compliant writes and fast reads for the registry cache, with table-specific logic in src/lib/db/skills.ts.
Global Settings and API Key Isolation
Global toggles like skillsEnabled are stored in the settings table and accessed via getSettings() from src/lib/db/settings.ts. Per-API-key scoping is enforced by passing apiKeyId arguments throughout the registry and executor methods, ensuring that skills registered for one tenant remain invisible to others.
Practical Implementation Examples
Registering a New Skill
import { skillRegistry } from "./src/lib/skills/registry";
await skillRegistry.register({
name: "weather",
version: "1.0.0",
description: "Fetch current weather for a city",
schema: {
input: { city: "string" },
output: { temperature: "number", condition: "string" },
},
handler: "weatherHandler",
enabled: true,
apiKeyId: "public",
});
Registering the Corresponding Handler
import { skillExecutor } from "./src/lib/skills/executor";
skillExecutor.registerHandler("weatherHandler", async (input, ctx) => {
const city = (input as any).city;
const data = await fetch(`https://api.weather.example.com/${encodeURIComponent(city)}`).then(r => r.json());
return { temperature: data.temp_c, condition: data.condition };
});
Executing a Skill via MCP Toolset
import { skillExecutor } from "./src/lib/skills/executor";
const execution = await skillExecutor.execute(
"weather@1.0.0",
{ city: "Berlin" },
{ apiKeyId: "user-123", sessionId: "sess-abc" }
);
console.log(execution.output);
Listing Skills for a Specific API Key
import { skillRegistry } from "./src/lib/skills/registry";
const mySkills = skillRegistry.list("user-123");
mySkills.forEach(s => console.log(`${s.name}@${s.version}`));
Resolving Semantic Versions
const skill = skillRegistry.resolveVersion("weather", "^1.0.0", "user-123");
console.log(skill?.version);
Summary
- OmniRoute employs a registry-executor-handler pattern that decouples skill metadata from execution logic.
- The
SkillRegistryinsrc/lib/skills/registry.tsprovides in-memory caching with per-API-key isolation and semantic version resolution. - The
SkillExecutorinsrc/lib/skills/executor.tsenforces timeouts, retries, and comprehensive audit logging toskill_executions. - Skills are defined via strict TypeScript interfaces in
src/lib/skills/types.tswith JSON schema validation for inputs and outputs. - Runtime extensibility is achieved through handler registration, interception hooks, and injection points without modifying core routing code.
- SQLite persistence via
src/lib/db/core.tsensures ACID compliance, while global settings insrc/lib/db/settings.tscontrol feature enablement.
Frequently Asked Questions
How does OmniRoute handle versioning for skills?
OmniRoute supports semantic versioning through the SkillRegistry.resolveVersion method in src/lib/skills/registry.ts. Developers can register multiple versions of the same skill, and the system resolves compatible versions using standard constraints like caret (^) and tilde (~) ranges. The cache keys use the format apiKeyId:name@version, allowing precise version isolation per tenant.
What is the difference between the SkillRegistry and SkillExecutor?
The SkillRegistry manages metadata: it caches skill definitions, handles CRUD operations, and resolves versions from the SQLite skills table. The SkillExecutor handles runtime concerns: it validates that skills are enabled, manages execution timeouts and retries, invokes the actual handler functions, and writes audit records to skill_executions. This separation ensures that looking up a skill is lightweight and safe, while executing it occurs within a controlled, observable sandbox.
How does the skills system ensure isolation between different API keys?
Isolation is enforced at the registry level by keying cache entries with apiKeyId:name@version strings. When calling skillRegistry.list() or resolveVersion(), the apiKeyId parameter filters the query scope. Similarly, the SkillExecutor passes the apiKeyId through execution contexts, ensuring that handlers cannot access data belonging to other tenants. This design prevents cross-tenant skill leakage while allowing shared infrastructure.
Can skills modify HTTP requests and responses in the routing pipeline?
Yes, through the interception and injection modules. src/lib/skills/interception.ts allows skills to modify inbound requests before they reach downstream executors, while src/lib/skills/injection.ts enables augmentation of outbound responses. These hooks provide aspect-oriented extension points, letting developers implement cross-cutting concerns like logging, authentication, or payload transformation without altering core routing logic.
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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →