Enable bundled plugins

Toggle the 16 bundled QiForge plugins via the features map — opt out, force on, or let auto-detect handle it — and retune their manifests.

Copy-paste recipe

The whole API is a features map handed to createOracleApp. Each key is a bundled plugin name, each value is true / false / 'auto'.

import { createOracleApp } from '@ixo/oracle-runtime';
import { config } from './config.js';

const app = await createOracleApp({
  config,
  features: {
    composio: true,       // force on (set COMPOSIO_API_KEY)
    slack: false,         // force off
    firecrawl: 'auto',    // same as omitting — runs autoDetect
  },
  plugins: [],            // your own plugins go here
});

await app.listen();

That's the whole surface. The runtime pre-loads every bundled plugin instance from BUNDLED_PLUGINS; features only controls which ones survive resolution. Reference oracle: apps/qiforge-example/src/main.ts.

How resolution works

The runtime starts with the bundled list

BUNDLED_PLUGINS is a fixed 16-plugin tuple — memory, portal, firecrawl, domain-indexer, composio, sandbox, skills, editor, agui, slack, tasks, credits, calls, user-preferences, matrix-group-chats, vfs.

You do not import or instantiate the plugins you want at defaults — they are already there.

Each plugin gets a feature decision

For every bundled plugin, resolvePlugins reads features[plugin.name]:

type FeatureToggle = boolean | 'auto';

features?: Partial<Record<string, FeatureToggle>>;
ValueBehaviour
trueForce the plugin on. If its autoDetect(env) returns false, boot fails with boot.plugin.env_missing.
falseForce the plugin off. Skip autoDetect entirely.
'auto' (or omitted)Run plugin.autoDetect(env). Include if true, exclude otherwise.

Your own plugins are added next

Plugins from the plugins: [] array are always loaded — they're not gated by features. If a name collides with a bundled plugin, your instance wins (the loader dedupes by name).

Hard-dep cascades resolve transitively

If a loaded plugin has dependsOn: ['removed-plugin'] and that dep ended up excluded, the dependent cascades off too. Soft deps (softDependsOn) only log a warning.

What every bundled plugin does by default

Each plugin's autoDetect predicate decides whether to opt in when you leave it on 'auto'.

PluginAuto-detects whenNotes
memoryMEMORY_MCP_URL setVisibility always
portalalways onVisibility on-demand
firecrawlFIRECRAWL_MCP_URL setVisibility on-demand
domain-indexeralways onVisibility always
composioCOMPOSIO_API_KEY setVisibility on-demand
sandboxSANDBOX_MCP_URL setVisibility always
skillsalways onVisibility always; depends on sandbox
editoralways onNeeds matrixClient — instantiate explicitly
aguialways onVisibility on-demand
slackSLACK_BOT_OAUTH_TOKEN setVisibility silent (transport)
tasksREDIS_URL setVisibility on-demand; BullMQ-backed async tasks (needs REDIS_URL)
creditsalways onVisibility silent; pass redis for production
callsalways onVisibility silent; placeholder stub (no tools yet)
user-preferencesalways onVisibility always
matrix-group-chatsalways onVisibility on-demand; gating middleware + tools fire only in Matrix group rooms (memberCount > 2)
vfsalways onVisibility always; worker URLs derived from NETWORK. Contributes tools only when the oracle has a UCAN signing key and the user granted filesystem access

Full per-plugin env vars: plugin catalog and environment variables reference.

Opt out of a plugin

Set the feature flag to false

const app = await createOracleApp({
  config,
  features: {
    composio: false,
    'domain-indexer': false,
  },
});

The plugin is dropped before autoDetect runs. Its configSchema is also removed from the merged env schema, so its env vars become optional.

Check for dependents

If another loaded plugin lists the disabled plugin in its dependsOn, boot fails with a boot.plugin.dep_missing error naming both. Disable the dependent too, or keep the dependency loaded.

Force a plugin on

Set the feature flag to true

const app = await createOracleApp({
  config,
  features: {
    composio: true,
  },
});

The plugin loads even if autoDetect would skip it.

Set every env var the plugin needs

With features: { composio: true } and no COMPOSIO_API_KEY, the runtime throws at boot:

boot.plugin.env_missing: plugin 'composio' enabled via features but precondition failed (COMPOSIO_API_KEY).
Set the required env or disable: features: { composio: false }

See the plugin's page in the plugin catalog for its full env requirements.

Plugins that need constructor args

Two bundled plugins take a live runtime object you provide — instantiate explicitly and pass them via plugins:

import { createOracleApp, EditorPlugin, CreditsPlugin } from '@ixo/oracle-runtime';
import * as sdk from 'matrix-js-sdk';
import Redis from 'ioredis';

const matrixClient = sdk.createClient({
  baseUrl: process.env.MATRIX_BASE_URL!,
  userId: process.env.MATRIX_ORACLE_ADMIN_USER_ID!,
  accessToken: process.env.MATRIX_ORACLE_ADMIN_ACCESS_TOKEN!,
});

const redis = process.env.REDIS_URL ? new Redis(process.env.REDIS_URL) : null;

const app = await createOracleApp({
  config,
  plugins: [
    new EditorPlugin({ matrixClient }),
    ...(redis ? [new CreditsPlugin({ redis, network: 'devnet' })] : []),
  ],
});

Live example: apps/qiforge-example/src/main.ts.

Retune a bundled plugin's manifest

features decides whether a bundled plugin loads. To change how it's advertised — most usefully its visibility tier, but also summary, tags, or whenToUse — use manifestOverrides instead of forking the plugin. The override is shallow-merged onto the plugin's own manifest at boot, validated like any authored manifest, and seen by every downstream reader (Tier-1 prompt, list_capabilities / load_capability, visibility index).

const app = await createOracleApp({
  config,
  manifestOverrides: {
    // Take a noisy `always` bundled plugin out of the Tier-1 prompt.
    'domain-indexer': { visibility: 'on-demand' },
    // Hide a transport plugin entirely; its tools still bind.
    portal: { visibility: 'silent' },
  },
});

Override keys that don't match a loaded plugin are logged (boot.plugin.manifest_override_unknown) and ignored — so disabling a plugin via features and leaving its override entry behind is safe.

Inspect what loaded

Read app.plugins.status() after boot

const status = app.plugins.status();
// {
//   loaded: ['memory', 'domain-indexer', 'editor', 'user-preferences', 'weather'],
//   excluded: [
//     { plugin: 'composio', reason: 'auto-detect precondition not met (COMPOSIO_API_KEY)' },
//     { plugin: 'slack',    reason: 'feature flag set to false' },
//   ],
//   softDepGaps: [],
// }

Each excluded entry is { plugin, reason } — surface the reason in your boot logs so operators see why a plugin came up missing. (softDepGaps entries are { plugin, missing }.)