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 withoutsetup_extension.pyis skipped with a warning; when<name>.pyand<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.moduleskey), 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.
Related documents¶
- Writing your first extension: Quick Start
- Fields of
extension_attrandExtensionContext: Extension Attributes - Command lines shipped by extensions: Extension CLI entry xune