Skip to content

Extension Attributes

@extension_attr declares an extension's metadata on its entry function; the framework uses it for version gating, data-directory versioning and CLI registration.

from xun import ExtensionContext, extension_attr

@extension_attr(
    api_min_version="1.4.0",     # lowest xun version required
    api_max_version="1.9.9",     # highest xun version required (inclusive)
    data_version="2",            # data layout version
    cli=(build_parser(), run),   # parser and handler handed to `xune <name>`
)
def setup_extension(ctx: ExtensionContext) -> None: ...

All four parameters are optional; the decorator returns the original function and attaches the metadata to the function attribute __xun_extension_attr.

Version gate

Parameter Semantics
api_min_version Current version below it → skipped
api_max_version Current version above it → skipped

Comparison looks only at the numeric core: 1.2rc3 equals 1.2, 1.4 equals 1.4.0 (shorter versions are zero-padded), and suffixes (dev, rc, post) take no part. Both ends of the range are inclusive.

When the gate blocks an extension the status is skipped, not failed, and reason looks like requires xun >= 1.4.0, running 1.3.0, visible in /extensions.

No version check when running from source

xun_version() reads package metadata (importlib.metadata.version("xun-agent")) and returns None when xun runs straight from source (no installed distribution), in which case the version check passes outright. So local development never skips an extension over its version declaration; the gate only bites once a package is installed.

data_version

data_version is not a version of your data contents but a version of the directory layout: once declared, the data directory gains one more layer, data_v_<version>.

Declaration Actual directory
Not declared $XUN_HOME/extension_data/<extension>/
data_version="2" $XUN_HOME/extension_data/<extension>/data_v_2/
  • The value must be a single safe path segment: \w[\w.+-]*, with no leading dot and no path separator, otherwise constructing ExtensionAttr fails immediately.
  • Raising the version means starting from scratch: the old directory is neither deleted nor migrated — relocating data is on you.
  • extension_data_dir(name, data_version) is the public form of the same rule, so external tools (the xune CLI, for example) can locate the data directory without entering an extension context.

cli

cli=(parser, handler): parser is an argparse.ArgumentParser, handler receives the parsed Namespace, and an int return value becomes the process exit code. It runs through xune <name>; see Extension CLI entry xune.

ExtensionContext

setup_extension(ctx) receives this single object, and it is new on every initialize():

Member Description
ctx.agent The not-yet-initialized Agent; every effect an extension has is applied here
ctx.name Extension name, also the key used in extension_settings and for the data directory
ctx.settings agent.config.extension_settings[ctx.name], {} when unset; values already have environment variables substituted
ctx.data_dir(_create=True) Private data directory (layered by data_version), created by default; _create=False only resolves the path without touching disk (for tests)
config.json
{
  "extension_settings": {
    "my-extension": {
      "endpoint": "${XUN_MY_ENDPOINT}",
      "verbose": true
    }
  }
}
def setup_extension(ctx: ExtensionContext) -> None:
    endpoint = ctx.settings.get("endpoint", "")
    cache = ctx.data_dir() / "cache.json"

Status overview

/extensions (and ShowExtensionsEvent on the web) lists one row per extension:

Field Source
Name File name or directory name
Description Docstring of the entry function
Path Source file path
Status uninitialized / loaded / skipped / failed
Reason Explanation for skipped and failed

The same data in-process: ExtensionLoader.infos(); only the successfully imported ones: imported().

Declarations of the three built-in extensions

Extension Declaration
mcp api_min_version="1.3.0"
skills api_min_version="1.4.0"
stat api_min_version="1.4.0", data_version=DATA_VERSION, cli=(build_parser(), cli.run)

Copying the closest one and adapting it is usually faster than writing from scratch.