The runtime merges every loaded plugin's configSchema onto a base schema, validates process.env at boot, and exposes the result on ctx.config.
Declare the schema with Zod
Prefix every variable with your plugin's name in SHOUT_SNAKE_CASE to avoid collisions across plugins.
import { z } from '@ixo/oracle-runtime';
const configSchema = z.object({
WEATHER_DEFAULT_UNITS: z.enum(['celsius', 'fahrenheit']).default('celsius'),
});Bundled plugins follow the same rule (MEMORY_MCP_URL, SLACK_BOT_OAUTH_TOKEN, …). See memory.plugin.ts for a required-field example.
Attach it as configSchema on the plugin class
The composer merges this schema with the base schema and every other loaded plugin's schema, then validates process.env.
import { OraclePlugin } from '@ixo/oracle-runtime';
export class WeatherPlugin extends OraclePlugin {
override readonly configSchema = configSchema;
}Boot fails fast on missing or invalid values: [boot-error] Plugin 'weather' env validation failed for 'WEATHER_DEFAULT_UNITS'. See weather.plugin.ts.
Read typed values from ctx.config
ctx.config is typed as MergedConfig (Record<string, unknown>). Parse it through your schema to get a typed view — this can't fail at runtime because boot already validated.
override getTools(ctx: PluginContext): PluginTool[] {
const units = configSchema.parse(ctx.config).WEATHER_DEFAULT_UNITS;
return [buildCurrentWeatherTool(units)];
}Same on RuntimeContext.config inside getRequestTools and tool handlers.
Gate the plugin on env with autoDetect (optional)
Implement autoDetect to make the plugin opt-in. Pair it with autoDetectHint so boot logs explain why it was skipped.
override autoDetect(env: NodeJS.ProcessEnv): boolean {
return Boolean(env.MEMORY_MCP_URL);
}
override readonly autoDetectHint = 'MEMORY_MCP_URL';Without autoDetect, a plugin is on by default. See memory.plugin.ts.
Force-toggle from your oracle's main.ts
Forks override autoDetect via the features map passed to createOracleApp.
const app = await createOracleApp({
config,
features: {
weather: true, // force on, skip autoDetect
slack: false, // force off
composio: 'auto', // explicit auto (the default)
},
});FeatureToggle is boolean | 'auto'. See plugin-loader.ts.
What to know before shipping
- Use
.default(value)and.coerce.number()freely — env values are strings, so coerce explicitly. - Disabling a plugin (via
featuresorautoDetect) automatically removes its env requirements. - Two plugins declaring the same env-var name fail boot. Follow the naming convention to avoid this.
- To read a variable your plugin doesn't own, declare a separate optional sibling schema and
safeParseit — don't require it through your ownconfigSchema. - Required boot-time fields stay in
configSchema; per-call settings belong elsewhere (e.g. request headers, manifest defaults).