Manifest schema

Field-by-field reference for PluginManifest — every field, type, constraint, and validation rule the runtime enforces at boot.

Overview

PluginManifest is the structured metadata every plugin must declare. The runtime composes it into the Tier-1 prompt block, feeds it to the meta-tools, and validates it at boot.

import type { PluginManifest, ManifestExample } from '@ixo/oracle-runtime';

Type

export interface PluginManifest {
  title: string;
  summary: string;
  whenToUse: string[];
  whenNotToUse?: string[];
  examples?: ManifestExample[];
  tags?: string[];
  category?:
    | 'data' | 'communication' | 'automation' | 'memory'
    | 'integration' | 'ui' | 'auth' | 'observability' | 'core';
  visibility?: 'always' | 'on-demand' | 'silent';
  stability?: 'stable' | 'beta' | 'experimental';
}

export interface ManifestExample {
  user: string;
  thought?: string;
  tool: string;
  args?: Record<string, unknown>;
}

The Cloudflare Workers runtime exports its own PluginManifest with the same fields plus requires and, from @ixo/oracle-runtime-workers 0.39.0, brief, onDemandTools and routable. The Node.js runtime's type above accepts none of them:

import type { PluginManifest } from '@ixo/oracle-runtime-workers';

// In @ixo/oracle-runtime-workers, PluginManifest has the fields above, plus:
interface PluginManifest {
  requires?: ReadonlyArray<{ resource: string; action: string }>;
  brief?: string;                      // 0.39.0 and later
  onDemandTools?: readonly string[];   // 0.39.0 and later
  routable?: boolean;                  // 0.39.0 and later
}

Fields

title
  • Type: string
  • Required: yes

Human-readable name. Used to prefix every tool description ([Climate Data] Fetch emissions…) and appears in the app.plugins.status() report. Distinct from name — name is kebab-case unique identifier, title is prose.

summary
  • Type: string
  • Required: yes

One-line description. Rendered in the Tier-1 prompt block for always plugins as - {name}: {summary}. Keep it short — every Tier-1 plugin costs ~80 tokens on every turn.

Validation:

  • Hard: must be non-empty.
  • Soft warn: > 120 chars.
whenToUse
  • Type: string[]
  • Required: when visibility !== 'silent'

Trigger phrases — each entry teaches the agent a situation in which this plugin is the right call.

On the Workers runtime (0.39.0 and later) the capability router also reads every whenToUse line, with the summary, to decide whether a message needs the plugin. Name what the plugin does ("retire tokens", "cancel a reminder"): a summary alone often leaves that out, and a plugin the router misses needs a better whenToUse, not a lower threshold. load_capability returns the lines with the plugin's tool names.

Validation:

  • Hard: at least 1 entry when visibility !== 'silent'.
  • Soft warn: > 8 entries, or any entry > 100 chars.
whenNotToUse
  • Type: string[]
  • Required: no

Anti-patterns. Use to disambiguate from other plugins.

Validation:

  • Soft warn: > 4 entries, or any entry > 80 chars.
examples
  • Type: ManifestExample[]
  • Required: no

Few-shot examples. Each example binds a user message to a tool call.

Validation:

  • Hard: every example.tool must reference a tool the plugin actually registers — but only when the plugin registers boot-time tools. Plugins whose tools arrive at request time (MCP-sourced: sandbox, memory, firecrawl) register zero boot-time tools, so the cross-check is skipped for them. A sub-agent's wrapped name (call_<name>) is a valid example tool too.
  • Soft warn: > 3 examples.
examples: [
  {
    user: "What's the weather in Berlin?",
    thought: 'Direct lookup — call get_current_weather.',
    tool: 'get_current_weather',
    args: { city: 'Berlin' },
  },
]
tags
  • Type: string[]
  • Required: no

Labels for search and filter. Surfaced in list_capabilities output.

Validation:

  • Hard: every tag must be lowercase. An uppercase tag is a boot error — createOracleApp throws Plugin manifest validation failed and the oracle never starts (so tags: ['Weather'] aborts boot; use ['weather']).
category
  • Type: 'data' | 'communication' | 'automation' | 'memory' | 'integration' | 'ui' | 'auth' | 'observability' | 'core'
  • Required: no

Grouping label. Surfaced in list_capabilities output.

visibility
  • Type: 'always' | 'on-demand' | 'silent'
  • Required: no
  • Default: 'on-demand'

Discovery and loading mode.

  • always — tools bound at boot; the plugin is listed in the Tier-1 prompt block.
  • on-demand — tools NOT bound at boot; the manifest is discoverable via list_capabilities; the agent calls load_capability to make the tools available. On the Workers runtime (0.39.0 and later) the plugin is also named in the prompt's on-demand catalogue, by its brief.
  • silent — the plugin is not advertised in the capability block or list_capabilities, but its tools still pass through the capability gate exactly like always and are visible to the model. silent means "unadvertised", not "hidden" or "secure" — it is not a security boundary.

See Visibility tiers.

stability
  • Type: 'stable' | 'beta' | 'experimental'
  • Required: no

Stability hint surfaced to the agent. experimental plugins get a warning footnote in list_capabilities output.

brief
  • Type: string
  • Required: no
  • Runtime: Cloudflare Workers runtime, @ixo/oracle-runtime-workers 0.39.0 and later.

A few words on what load_capability adds: the plugin's line in the system prompt's on-demand catalogue, so the model can pick it without calling list_capabilities. Without a brief, the line is the summary clipped at about 60 characters. For an always plugin with onDemandTools, describe the held-back tools, not the whole plugin.

Validation:

  • Soft warn: > 60 chars.
brief: 'reminders, scheduled and recurring background jobs',
onDemandTools
  • Type: readonly string[]
  • Required: no
  • Runtime: Cloudflare Workers runtime, @ixo/oracle-runtime-workers 0.39.0 and later.

Tool names this plugin keeps out of the model's tool list until load_capability names the plugin. Its other tools stay where its visibility puts them, and an always plugin keeps its Tier-1 entry. Use it for an always plugin whose rare tools carry most of its tool text. A tool's own visibility wins over this list.

Names are matched at request time, so tools from an MCP server can be listed. A Tier-1 entry never shows an example whose tool is listed here, since the model can't call it yet.

Validation:

  • Hard: every entry is a non-empty string. When the plugin registers boot-time tools, every entry must name one of them (as for examples).
// Keep search_skills bound; list_skills waits for load_capability(['skills']).
onDemandTools: ['list_skills'],

Bind every tool again from the host with manifestOverrides: { skills: { onDemandTools: [] } }.

routable
  • Type: boolean
  • Required: no
  • Default: true
  • Runtime: Cloudflare Workers runtime, @ixo/oracle-runtime-workers 0.39.0 and later.

false keeps the plugin away from the capability router: it stays in the catalogue and load_capability loads it, but no message preloads it. Set it on a plugin whose name attracts messages its loadable tools don't serve, or one the host loads itself when it applies. The bundled memory (its recall and save tools are always bound) and editor (an open document loads it) set it.

requires
  • Type: ReadonlyArray<{ resource: string; action: string }>
  • Required: no
  • Runtime: Cloudflare Workers runtime (@ixo/oracle-runtime-workers) only. The Node.js runtime (@ixo/oracle-runtime) has no such field.

The User-controlled Authorization Network (UCAN) capabilities that the user's delegation to the oracle must grant before this plugin can be used. Each entry is one capability: resource is the UCAN with (for example ixo:memory) and action is the UCAN can (for example memory/*).

A grant covers a requirement when both parts match:

  • Resource: the grant's resource is *, the same resource, or a parent of it. ixo:filesystem covers ixo:filesystem/.oracles; a narrower grant never covers a broader requirement.
  • Action: the grant's ability is *, the same ability, or a namespace wildcard. fs/* covers fs/read.

The delegation checked is the one the request carried, or the user's stored delegation when the request carries none (a Matrix turn, for example). While any requirement is unmet:

  • load_capability does not load the plugin. It returns the plugin with a refused field that lists the missing capabilities and a reason the agent can pass on, asking the user to re-authorize. Other plugins in the same call still load.
  • list_capabilities lists the plugin with loaded: false and an unavailable field that lists the missing capabilities.
  • The plugin's tools and sub-agents are hidden from the model and refused if called, whatever the plugin's visibility. This holds even when an earlier turn loaded the plugin. An always plugin is also left out of the Tier-1 prompt block.

Omit requires, or leave it empty, and any user can load the plugin.

Declare exactly the capabilities your plugin proves through the user's delegation when it mints UCAN invocations. Don't declare a capability for a plugin that authorizes some other way, or that still works without the grant.

Validation:

  • Hard: every entry needs a non-empty resource and action.
requires: [{ resource: 'ixo:memory', action: 'memory/*' }],

The bundled plugins that declare it:

PluginRequires
memorymemory/* on ixo:memory
sandboxsandbox/* on ixo:sandbox
composiosandbox/* on ixo:sandbox

ManifestExample

interface ManifestExample {
  user: string;                       // representative user message
  thought?: string;                   // optional reasoning
  tool: string;                       // tool the agent should call (must exist)
  args?: Record<string, unknown>;     // tool args
}

thought is optional but useful — agents pick up the reasoning pattern alongside the example.

Validation behaviour

At boot the runtime calls validateManifest(manifest, pluginName) from @ixo/oracle-runtime/manifest. The result:

interface ManifestValidationResult {
  valid: boolean;
  errors: string[];        // hard violations — boot fails
  warnings: string[];      // soft violations — logged, boot continues
}

Hard violations abort boot with the full error list printed.

Soft violations log warnings. The plugin still loads.

Manifest validation runs before env validation, so manifest errors surface even when env vars are missing.

Cross-checking examples against tools

Each example.tool is checked against the names the plugin registers — the union of its boot-time tool names and its sub-agent wrapped names (call_<name>). If a manifest references tool: 'foo' but the plugin registers no such tool, boot fails:

[boot-error] [weather] examples[0].tool: references unknown tool "foo".

This catches typos and stale manifests during refactors.

The check is lenient for MCP-sourced plugins: when a plugin registers no boot-time tools (its tools come from getRequestTools fetched per-request from an upstream MCP server — sandbox, memory, firecrawl), the runtime can't see those names at boot, so it skips the cross-check rather than false-flag every example.

Worked example

const manifest: PluginManifest = {
  title: 'Weather',
  summary: 'Weather lookups via Open-Meteo (no API key required).',
  whenToUse: [
    'Whenever asked about current weather, temperature, precipitation, or wind in any city.',
    'Any question related to forecasts (today, tomorrow, next week) for any location.',
    'When the user asks "what should I wear", "do I need an umbrella", or similar outfit guidance.',
  ],
  whenNotToUse: [
    'Questions about historical or long-term climate data.',
    'Locations smaller than city-level precision.',
  ],
  examples: [
    {
      user: "What's the weather in Berlin?",
      tool: 'get_current_weather',
      args: { city: 'Berlin' },
    },
    {
      user: 'Forecast for Tokyo this week.',
      tool: 'get_weather_forecast',
      args: { city: 'Tokyo', days: 7 },
    },
  ],
  tags: ['weather', 'forecast', 'outfit'],
  category: 'data',
  visibility: 'on-demand',
  stability: 'experimental',
};

Retuning a bundled plugin's manifest

You don't have to fork a bundled plugin to change its manifest. createOracleApp accepts a manifestOverrides option that shallow-merges fork-supplied fields onto each loaded plugin's manifest before validation and registration — so the override is what the Tier-1 prompt, the meta-tools, and the visibility index all see. This is the supported way to flip a bundled plugin's visibility (e.g. drop a noisy always plugin to on-demand) or relabel its summary / tags / whenToUse without touching the plugin source.