Skip to content

Extension Mechanism and Loading

This chapter is about extensions (plugins): files dropped into a conventional directory that run automatically whenever every agent is initialized, with no change to the entry program. The counterpart is API usage: wiring tools=[...] at the call site, which affects only that one agent.

When to use an extension, when to use the API directly

API usage Extension
Scope The one agent where you write the code Every agent that is initialize()d, including derived sub-agents
Needs an entry-program change Yes No
The entry program can see it Yes No, it only knows the directory
Typical use One-off tasks, experiments, app-private tools Capabilities shared across a team, integrations that persist across sessions
Code location In your program $XUN_HOME/extensions/

The built-in MCP, Skills and usage-statistics features are extensions, not core functionality — keeping the core uninvolved is deliberate. The MCP extension, for example, ships its own client implemented on the standard library and the core has no idea MCP exists; all it needs are the generic interfaces ToolBox.register_raw, ToolBox.pop, ToolBox.is_disabled and AgentConfig.extension_settings. So “this integration is not in the core” does not mean it cannot be done — usually nobody has written it as an extension yet.

Extensions are not untouchable black boxes

Anything an extension can do, you can do at the same point: setup_extension(ctx) receives the not-yet-initialized agent itself. The converse holds too — when you need an extension to do something, copying the structure of a built-in extension is often the fastest route.

Discovery and import

flowchart TD
    A["$XUN_HOME/extensions/"] --> B{"Form"}
    B -- "<name>.py" --> C["Single-file extension"]
    B -- "<name>/setup_extension.py" --> D["Directory extension<br/>remaining files may be imported freely"]
    C --> E["Import + read extension_attr"]
    D --> E
    E --> F{"Version gate"}
    F -- "out of range" --> G["skipped"]
    F -- "compatible" --> H["Added to the ready list"]
    H --> I["On every Agent.initialize()<br/>setup_extension(ctx) runs in name order"]

Rule details:

  • Scanning is ordered by directory-entry name, and each directory is scanned only once per process and then cached; ExtensionLoader.clear_scan_cache() forces a rescan.
  • Entries starting with . or __ are ignored. A directory without setup_extension.py is skipped with a warning; when <name>.py and <name>/ both exist, the former is shadowed by the latter, with a warning.
  • The extension name must be a valid module identifier (a directory extension must also work as a sys.modules key), otherwise it is skipped with a warning.
  • The entry point name is fixed: a module-level callable setup_extension(ctx); without one, the extension is skipped with a warning.
  • Import failure → status failed, warning only; startup continues.

When XUN_HOME is unset it resolves to ./.xun/extensions/. In extension code go through ctx.data_dir() and config.extension_settings instead of hard-coding paths.

Re-run for every agent

Agent.initialize() calls extension_loader.apply(self), applying every extension in name order to the not-yet-initialized agent. The extension_loader field of Agent can be injected (in tests, pass a loader pointing at a temporary directory).

Situation Behavior
config.enable_extensions = false That agent skips extensions entirely
setup raises Warning + status failed, startup continues
Duplicate tool name (common with sub-agents) The conflict aborts the rest of that extension's setup for the child and is downgraded to a warning; hooks registered earlier stay in effect
KeyboardInterrupt / CancelledError Raised as-is, not counted as a failure

Because every agent replays it, setup_extension must be written to be idempotent: register-ing the same tool name twice raises a conflict, and expensive resources (server connections, disk reads) belong in module-level caches.

After changing extension code

Scan results are cached per directory, so editing files after the process has started has no effect — restart, or call clear_scan_cache() in tests. Derived sub-agents replay extensions as well.

Status and inspection

ExtensionLoader.infos() returns an ExtensionInfo for each extension (name, description, path, status, reason); /extensions renders it as a table, and the web counterpart is ShowExtensionsEvent.

Status Meaning
uninitialized Discovered, but setup has not run yet
loaded setup completed successfully
skipped Incompatible with the current xun version, skipped as declared
failed Import failed or setup raised; see reason

Security boundary

Extensions are full-privilege code

Extensions are imported in file-name order, and the import itself can execute arbitrary logic (it does not wait for setup_extension). They run inside the trust boundary, with the full authority of a shell rc file — this is not a sandbox. For isolation use the xunc container. config.enable_extensions only turns execution off; it is not a security boundary.