Skip to content

Authoring an extension

Phoenix is an optional built-in extension that registers typed configuration and contributes a container definition to Transmutation. Follow it from selection through registration, Rune loading, Quadlet projection, and tests to learn where a contribution belongs and what evidence a change needs.

The extension index maps the Extension Domains. ADR 05 owns admission and compatibility; ADR 12 owns configuration. This guide follows one package through those contracts.

First follow the contribution guide's Setup and Commands, then run this focused test selection from the checkout:

make test PYTEST_TARGETS="tests/integration/test_extension_configuration.py tests/unit/extensions" N=0

The configuration integration test creates its own temporary Codex and Crypt directories and runs separate Python interpreters. The snippets below explain that fixture's selection, configuration, and projection path. In an installation, lychd.toml and Rune documents live beneath the operator's Codex; Codex owns that operating workflow.

Select and register Phoenix

In the test's temporary Codex, lychd.toml selects Phoenix with:

[extensions]
builtins = ["observability/phoenix"]
crypt = []

Both selections default to empty. A built-in must have an explicit entry in the catalogue; adding a directory alone does not make it selectable.

Read Phoenix's registration shim. Its synchronous register(context: ExtensionRegistrationContext) -> None hook lazily imports ObservabilityConfig, PhoenixSettings, and PhoenixQuadletContributor, then registers them:

context.runes.add_schema(ObservabilityConfig)
context.runes.add_schema(PhoenixSettings)
context.transmutation.add_contributor(PhoenixQuadletContributor())

The host assigns the Registrant provenance builtin:observability/phoenix to these registrations. That identity records which package contributed them; each receiving store keeps its own contract. Selection admits the package at assembly. Phoenix contributes no container until it receives an active Rune.

Give the contribution a typed Rune

Phoenix's configuration module defines a branch and a leaf. ObservabilityConfig has path_fragment = Path("observability"); PhoenixSettings adds Path("phoenix"), producing the relative path observability/phoenix. The branch cannot own TOML; the leaf can.

A Rune is a frozen, validated top-level TOML instance. The fixture writes its active document at <Codex>/runes/observability/phoenix/desk.toml. This example shows the same configuration shape with the default image:

name = "desk-eye"
ui_port = 16006
otlp_port = 14317

[quadlet]
image = "docker.io/arize-ai/phoenix:latest"

PhoenixSettings rejects unknown fields and validates name as a safe service identity. The example overrides the default name and ports; its image line repeats the typed Quadlet setting's default. It is a configuration example, with no image pull or live-service receipt. The loader derives source_file from the document's path; do not put that field in TOML.

The schema also supplies a sample_template. Its rendered sample is inactive, identified by the exact first nonempty line:

# lychd: sample-rune

Activation requires a configured document with that marker removed. A selected Phoenix package with an absent or marked sample Rune still produces no Phoenix container. Codex owns Rune and binding operations.

Follow the Rune into Quadlet data

Read the Phoenix contributor next. It requests ctx.runes.one_or_none(PhoenixSettings). No instance produces an empty contribution; more than one raises an error.

Exactly one instance supplies a typed QuadletContribution containing a Phoenix container and two pod ports. The service name, image, and host ports come from the Rune. The database user and Phoenix secret reference come from Settings, and the container carries migration gate dependencies. The Transmuter places the host ports on loopback.

This output is projection data. The binding workflow separately owns publication and host reconciliation. Follow External Eyes for Phoenix's observability boundary: this example establishes neither a running Phoenix service nor an application telemetry export path. Phoenix contributes no Animator runtime or CallGrant service.

Test the complete configuration path

The configuration integration test follows the real Settings, extension manager, Rune loader, and Transmuter across separate Python interpreters. Its assertions cover:

  • Default-disabled selection leaves the Phoenix shim unimported.
  • Selected Phoenix with an inactive sample has the exact schemas, contributor, and provenance.
  • An active Rune is frozen, retains its exact source_file, and projects its configured service name, image, ports, and Settings-owned database values.
  • Editing the Settings file leaves the current process's cached generation unchanged.
  • A fresh disabled process produces no Phoenix contribution while the Rune and representative retained data bytes survive.
  • An unselected Crypt shim that would fail if imported remains unimported.

The fixture writes TOML directly. It does not execute the writer's Linux journaled inscription, bind units, or prove database recovery. Detailed registration collisions and import failures belong to the admission tests. For a contribution that supplies an Animator, the Crypt vertical test continues through its separate adapter and Dispatcher contracts.

Apply the registration contract in Crypt

A private package is selected explicitly:

[extensions]
builtins = []
crypt = ["my-private-organ"]

Its exact shim is PATH_EXTENSIONS_DIR/my-private-organ/register.py; Crypt explains the source location. An activation ID must be canonical and POSIX-relative, without traversal or aliases. The manager resolves only the selected register.py, with no environment or entry-point scan.

The shim uses the same synchronous register(context) contract and returns None. It may import relative siblings within its synthetic package namespace; adjacent unselected packages remain unimported. Crypt code is trusted, runs in process, and shares memory. This pre-v1 path accepts coupling to internal refactors. Built-ins release with Core; a versioned independent extension API remains future work under ADR 05.

Finish a bounded contribution

get_settings() and get_extensions() retain one process generation, and registration stores seal after assembly. Restart or replace the process after code, dependency, contribution-contract, or selection changes. Removing Phoenix's selector in a new process removes its contribution; registration and loading leave retained Runes and storage alone. This example does not implement installation, uninstallation, rollback, or automated Evolution. State of Work owns delivery status.

Change the owned schema, registration, or contributor together with a test of the affected behavior, then rerun the focused selection above. Follow the contribution quality checks for the applicable lint, format, type, and repository gates. Before opening a PR to dev, describe the boundary the change establishes and the evidence that supports it.