Skip to main content

Overview

createOracleApp(opts: CreateOracleAppOptions): Promise<OracleApp> resolves the plugin set, validates env, populates registries, builds the dynamic RuntimeAppModule, bootstraps NestJS, and returns an OracleApp whose listen() runs beforeListen callbacks then starts the HTTP server. Matrix init runs in the background — the returned promise resolves as soon as Nest is built. Status flips via onPluginStatusChange.
import { createOracleApp } from '@ixo/oracle-runtime';

CreateOracleAppOptions

export interface CreateOracleAppOptions {
  config: OracleConfig;
  features?: Partial<Record<BundledFeatureName | (string & {}), FeatureToggle>>;
  plugins?: OraclePlugin[];
  nestModules?: Array<Type | DynamicModule>;
  authExcludedRoutes?: AuthExcludedRoute[];
  bundledPlugins?: OraclePlugin[];
  env?: NodeJS.ProcessEnv;
  skipMatrixInit?: boolean;
  skipGracefulShutdown?: boolean;
  logger?: Logger;
  hooks?: MainAgentHooks;
}
  • Type: OracleConfig
  • Required: yes
Inline oracle config — name, optional org, optional description, optional prompt. The runtime fills in entityDid from the ORACLE_ENTITY_DID env var, so don’t put it here.
config: {
  name: 'My Oracle',
  org: 'My Org',
  description: 'What this oracle is for',
  prompt: {
    opening: '...',
    communicationStyle: '...',
    capabilities: '...',
  },
}
prompt sub-fields — all three are optional. Omit any you don’t need.openingUsed verbatim as the identity preamble — the very first section of the assembled system prompt. Write it as a complete paragraph describing who the oracle is and what it does.If opening is absent, the runtime generates a fallback from the other top-level fields:
  • name + org + description present → "You are {name}, an AI agent operated by {org}. {description}."
  • org missing → "You are {name}. {description}."
  • Both org and description missing → "You are {name}."
Providing opening gives you full control over tone, persona, and framing — use it whenever the generated fallback is too generic.communicationStyleInjected inside the hardcoded ”## Operating principles” section of the prompt. If set, it appears as a paragraph within that section immediately after the 7 standard operating-principle bullets. If empty or absent, the field is omitted entirely — the operating-principles section still appears, just without the custom style block.Use this to steer the agent’s tone: formal, concise, empathetic, domain-specific jargon preferences, and so on.capabilitiesRendered above the Tier-1 plugin capability block (the auto-generated list of visibility='always' plugins). No header is added by the runtime around this text — write your own heading if you want one. If empty or absent, the field is omitted entirely.Use this to describe high-level skills the oracle has that aren’t obvious from the plugin manifest alone — things like domain expertise, supported workflows, or what the agent should proactively offer.entityDidDo not set this in code. The runtime reads it from the ORACLE_ENTITY_DID environment variable and populates it automatically during boot. Setting it inline has no effect.Full example
config: {
  name: 'Aria',
  org: 'Acme Climate',
  description: 'A carbon-project advisory oracle for Acme Climate portfolio managers.',
  prompt: {
    opening: `You are Aria, the carbon-project advisory oracle operated by Acme Climate.
You help portfolio managers track project status, assess methodology compliance,
and draft stakeholder communications. You have deep knowledge of Verra VCS,
Gold Standard, and REDD+ frameworks.`,
    communicationStyle: `Be precise and data-driven. Lead with numbers and deadlines.
Use plain English — avoid jargon unless the user demonstrates familiarity.
When something is uncertain, say so explicitly rather than hedging with filler phrases.`,
    capabilities: `## What Aria can do
- Retrieve live project status and registry issuance data via the IXO domain indexer.
- Draft verification reports, CORSIAs letters, and board summaries.
- Search and cite methodology documents from the oracle knowledge base.
- Schedule follow-up reminders and track open action items across conversations.`,
  },
}
  • Type: Partial<Record<BundledFeatureName | (string & {}), FeatureToggle>> where FeatureToggle = boolean | 'auto'
  • Required: no
Override the default opt-in state per plugin. Keys are plugin names. Values: true (force on), false (force off), 'auto' (run autoDetect(env)). Omitted keys are treated as 'auto'.
features: {
  slack: false,
  composio: true,
  domainIndexer: 'auto',
}
  • Type: OraclePlugin[]
  • Required: no
Your plugin instances. Plus any bundled plugins you want to instantiate with constructor args (new EditorPlugin({ matrixClient }), new CreditsPlugin({ redis, network })). The plugin loader dedupes by name, so an explicit instance overrides the bundled default of the same name.
  • Type: Array<Type | DynamicModule>
  • Required: no
Your own NestJS modules. Spread into RuntimeAppModule.imports. They get full DI access to the Tier-0 services (Sessions, Messages, Secrets, UCAN, …) and can declare controllers, providers, OnModuleInit, OnModuleDestroy.
  • Type: AuthExcludedRoute[]
  • Required: no
Host-declared routes that must not pass through AuthHeaderMiddleware. Use for routes contributed by nestModules (webhooks, OAuth callbacks, public probes). Symmetric with each plugin’s getAuthExcludedRoutes() hook — both lists merge onto the runtime’s built-in exclusions (/health, /docs).
import { RequestMethod } from '@nestjs/common';

authExcludedRoutes: [
  { path: 'version', method: RequestMethod.GET },
]
  • Type: OraclePlugin[]
  • Required: no
Override the bundled plugin set. Provided primarily for tests so the harness can spin up createOracleApp without dragging in the full bundled catalog. Production callers should leave this unset.
  • Type: NodeJS.ProcessEnv
  • Required: no
Override process.env. Tests use this to inject a clean env. Production code should leave it unset.
  • Type: boolean
  • Required: no
Skip starting the Matrix background init. Tests set this so the factory resolves without touching real Matrix. Do not use in production.
  • Type: boolean
  • Required: no
Skip registering the SIGTERM/SIGINT shutdown handler. Tests set this so the harness does not leak process-level listeners. Do not use in production.
  • Type: Logger
  • Required: no
Override the bootstrap logger. Falls back to NestJS Logger.
  • Type: MainAgentHooks
  • Required: no
Optional overrides for the agent-build hooks (checkpointer, model resolver, prompt section snippets). Merged on top of the runtime’s defaults — host hooks win. The runtime supplies a per-user SQLite checkpointer by default; pass { checkpointerForUser: ... } here to swap it for an alternate implementation.

OracleApp

The resolved OracleApp exposes:
export interface OracleApp {
  getNestApp(): INestApplication;
  ambient: AmbientServices;
  plugins: { status(): PluginStatusReport };
  beforeListen(fn: (nestApp: INestApplication) => Promise<void> | void): void;
  onError(handler: (err: Error, source: string) => void): void;
  onPluginStatusChange(handler: (event: PluginStatusChangeEvent) => void): void;
  listen(port?: number): Promise<void>;
}
Returns the underlying INestApplication. Use it to add Express middleware, register global filters, or do anything else NestJS exposes. Mainly an escape hatch — nestModules and beforeListen cover most cases.
The production AmbientServices bag — used by MessagesController to build per-request RuntimeContexts before invoking createMainAgent. Forks normally don’t touch this directly.
Returns a snapshot of loader/exclusion/soft-dep state.
{
  loaded: string[];
  excluded: Array<{ plugin: string; reason: string }>;
  softDepGaps: Array<{ plugin: string; missing: string }>;
}
Register a callback that runs after Nest is built but before HTTP starts accepting. Multiple registrations run in order, awaiting each.
Subscribe to background errors. The runtime currently surfaces Matrix init failures here (with source: 'matrix-init'). If no handler is registered, errors log to the bootstrap logger.
Subscribe to plugin lifecycle changes. The current emitter is the Matrix init flow:
{ plugin: 'matrix', from: 'pending', to: 'loaded' | 'failed', reason?: string }
Multiple handlers may register; each is invoked for every event.
Start the HTTP server. Honours beforeListen callbacks first. Port resolution: explicit port arg → PORT env (validated) → 5678 default. Throws if called twice.

Behaviour

The function executes these steps in order:
  1. Validates config (name required).
  2. Resolves plugin set: bundled + your plugins, deduped by name, with features toggles and autoDetect predicates applied. Excluded plugins surface in plugins.status().excluded with their reason.
  3. Topologically sorts by dependsOn. Cycles or missing hard deps fail boot.
  4. Validates every plugin’s manifest. Hard violations fail boot.
  5. Composes env schema (base + all loaded plugins’ configSchema) and validates process.env. Missing required vars fail boot with [boot-error] Plugin '<name>' env validation failed for '<field>'.
  6. Cross-field check: the API key for the selected LLM_PROVIDER is present. (Schema-merge can’t express this.)
  7. Builds the internal OracleIdentity from config + validated env.
  8. Populates the six registries (tools, sub-agents, middleware, manifests, configSchema, sharedState).
  9. Collects getNestModules from every loaded plugin.
  10. Builds the dynamic RuntimeAppModule and bootstraps NestJS (NestFactory.create). CORS enabled with the framework’s allowed headers. Swagger UI mounts at /docs.
  11. Builds AmbientServices.
  12. Warms the boot caches (each registry runs its boot-time hooks once).
  13. Wires the default checkpointer (UserMatrixSqliteSyncService + SqliteSaver), then merges host hooks over it.
  14. Populates the OracleRuntimeBundleHolder so MessagesController can read it on each request.
  15. Schedules background Matrix init (unless skipMatrixInit: true). Registers a graceful-shutdown handler (unless skipGracefulShutdown: true).
  16. Returns the OracleApp.
Step 15 (Matrix init) runs asynchronously and does not block the returned promise. listen() blocks on beforeListen callbacks before starting HTTP.

Throws

  • Error('createOracleApp: \'config\' is required.')config missing.
  • Error('createOracleApp: \'config.name\' is required.')config.name empty.
  • Plugin resolution errors (cycle, missing hard dep, manifest invalid, env invalid). Each error is reported via logger.error with [boot-error] … prefix and a remediation hint.
  • Error('createOracleApp: ORACLE_ENTITY_DID env is required and was empty after validation.') — when the validated env’s ORACLE_ENTITY_DID is empty.
  • Error('OracleApp.listen called twice.') — when listen() is called more than once.