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.