Component registry
Aether has two component inventories with different owners. The hub knows which wasm artifacts are stored. A substrate knows which component actors are live. Treating one as proof of the other causes most component-operation mistakes.
Three views, three meanings
| View | Owner | Current public access | What a row proves |
|---|---|---|---|
| stored artifact registry | hub | upload_component, registry list_components | bytes and manifest are available to selectors |
| trampoline/capability registry | one substrate | load_component, describe_component; live queries behind them | a lineage mailbox and its retained capabilities at observation time |
| MCP capability/kind caches | aether-mcp process | populated and refreshed by tool calls | an optimization, never the authority for liveness |
Registry list_components is deliberately artifact-facing. It does not list
loaded instances. The same artifact can be loaded zero times, once, many times
on one engine, or into several engines.
Current component tools
| Tool | Purpose |
|---|---|
upload_component | ingest a .wasm path into the hub store; optional pin: true |
list_components | list/filter stored component artifacts |
pin_artifact | durable explicit pin by exact stored content hash |
unpin_artifact | drop only the explicit pin; a name still protects |
publish | bind a stored module's namespaces in one engine; a successor republishes every live instance of them |
spawn | stand up (or find live) an instance of a published type |
load_component | publish then spawn in one call |
describe_component | inspect a live instance's or a published type's receive surface |
describe_kinds | inspect its config, input, and reply schemas in the engine's live vocabulary |
send_mail | drive a component, including the generic aether.component.drop lifecycle mail |
There is no dedicated drop_component MCP tool. Dropping is nevertheless a
current engine operation: use send_mail to the aether.component mailbox with
kind aether.component.drop and target set to the exact address returned by
the load.
Inspect that kind with describe_kinds before constructing params. Await its
aether.component.drop_result; do not fire-and-forget cleanup.
Drop runs the guest's unwire, releases the wasm, and closes the instance
(ADR-0241 §8). The close purges capability-owned subscriptions and routes
through each watcher's MonitorNotice, and the name tombstones for the engine's
lifetime: a later load of it is refused as retired, and a second drop at the
path is refused. A drop that arrives while the instance's module republishes
waits until the republish answers. Load or spawn under a new name to bring the
component back.
Upload before selector
Only upload_component accepts a filesystem path. publish,
load_component, and boot component specs accept registry selectors—not
paths and not inline wasm bytes.
The reliable sequence is:
- Build the wasm artifact on the same fleet host whose path the hub can read.
- Call
upload_component(staged_path, name?, pin?).pin: truerecords durable explicit protection before this upload's eviction, including unnamed uploads.pin: false(the default) never clears an existing pin. - Record the returned content hash and optional name.
- Confirm its manifest with registry
list_components; when no name was supplied, setinclude_history: trueand locate the returned hash. - Use the hash or name as the later selector.
Upload reads the wasm manifest without executing the module. It records exported actor namespaces, handled kind ids, fallback presence, and provenance. Identical bytes deduplicate to one hash.
Which selector?
| Selector | Meaning | Use when |
|---|---|---|
| content hash | exact stored bytes | rollout, rollback, reproducible test |
| name | hash currently pointed to by that name | operator-managed “latest” workflow |
module@actor | stored module plus exported actor type | multi-actor module selection |
An explicit export field overrides the actor half of module@actor. A
module exporting several types must be given an export; an omitted export
resolves only against a module with exactly one selectable (non-boot) export,
and is otherwise a clean load error naming the exports, not “first actor
wins.”
Names are mutable. Re-uploading under the same name repoints it. The old hash
remains stored and, once no other name points at it, becomes unnamed history
eligible for LRU eviction unless it still carries an explicit pin. It is not
deleted by repointing. Hashes are therefore the correct deployment evidence.
unpin_artifact removes only the explicit flag; it is not unname or delete.
Stored registry behavior
list_components can filter stored manifests by exported namespace and
handled_kind; filters are AND-combined. Its normal result is a bounded,
newest-first page of name-pointed entries. include_history: true admits
unnamed hashes. Use the returned total_matched and an explicit limit when a
complete history is actually needed.
A named artifact is protected from disk-budget LRU eviction. An unnamed,
unpinned history entry is eligible. pin_artifact / unpin_artifact (and
upload_component(pin: true)) record or drop durable explicit protection on an
exact content hash — names are never resolved. pin: false on upload is not
unpin. There is still no delete or unname operation. Component runtime
protection of stored artifacts is not provided here: the hub holds the binary
of every engine it supervises (issue 5686), but it does not supervise a loaded
component, so a component's stored wasm is protected by a name or a pin alone.
Fleet and MCP
must ship the same release: the pin field changes the typed upload kind
schema.
Loading into an engine
load_component resolves the selector at the hub, publishes the bytes to the
target engine's aether.component cap, then spawns the selected type and waits
for its SpawnResult.
On a single load, record both outputs:
address: the full lineage address used assend_mail.address, theaether.component.dropkind'starget, apublishconfig'saddress, and by livedescribe_component.capabilities: handled kinds, reply contracts, fallback, docs, and config kind for the selected actor type.
Never reconstruct the lineage from a short load name. Use the returned value,
the component's published name: NS for a singleton, NS:key for an
instanced type.
Configuration
Pass either inline JSON in config or a JSON-file path in config_path, never
both. The MCP layer resolves the selected artifact's declared Config kind and
schema-encodes that JSON before loading. Providing config to a component that
declares no Config kind is an error. Use describe_component to learn the kind
name and describe_kinds for its exact live schema.
Replicas
replicas: N (1..=256; replicas: 0 is refused, and a key combined with
replicas is refused, since each replica draws its own counter key) publishes
the module once, then spawns N counter-keyed instances of the selected type
(NS:0, NS:1, …). An entry that names no replicas spawns the one instance
its key names, or a counter key when it names none. The result carries one
shared capabilities block, since every instance is the same type, and an
instances list of each spawned address and whether it was already live.
A replica fan-out is not transactional. If instance K fails, the instances
before it remain live and the error says how many spawned. The failed call
does not return the successful prefix's instances records.
aether.component.list reports which are live. On a task-owned engine,
terminate and start clean. On a shared engine, stop and report the partial
prefix rather than guessing addresses or retrying into occupied ones.
Boot-time replicas use the same counter-keyed expansion. The substrate binds
only after every boot instance has answered its spawn, so a successful
spawn_substrate means every instance is live. A boot spawn that fails fails
the boot with a spawn_failed entry rather than reporting the engine ready.
Live introspection
describe_component should normally receive the lineage address. On a cache
miss, aether-mcp forwards that name to the substrate, whose registry owns the
live answer. This works for boot-loaded components, after an aether-mcp
restart, and after a successful in-place replacement.
The process-local capability cache is consulted before that live query. Loads
and replacements refresh it, but a generic aether.component.drop sent through
send_mail does not invalidate it. In that case, a same-process describe can
return pre-drop capabilities. The successful drop_result is the unload proof;
only a later name lookup after a cache reset is guaranteed to miss the cleared
substrate capability registry.
The cache is keyed by engine and canonical path, with no hub-epoch component.
After a hub restart reuses an engine_id, a boot-loaded component at the same
lineage can collide with an earlier capability entry. Use live probes or
reset/reconnect aether-mcp when an exact clean-epoch description is required.
An address is a lineage or a short path. Keep the lineage address as the durable observation handle for the life of the engine.
The live kind vocabulary is also substrate-owned. Loading registers the wasm's
kind descriptors into the same registry served by aether.inventory.
describe_kinds(engine_id: …) reads that view. The mail encoder refreshes its
per-engine cache once on an unknown kind or a stale-schema encode failure, but
the cache does not make an unloaded kind live. Kind descriptors are not removed
when a component drops, so kind presence is vocabulary evidence, not component
liveness evidence.
The standalone description call currently suppresses a failed live refresh and can return its static/prior snapshot. Use a bounded live probe when freshness or engine reachability matters.
Replacing safely
A publish of a successor republishes a module (ADR-0241 §7). Pass the current
engine id and a previously uploaded selector; prefer a content hash so the
successor is unambiguous. A publish names no instance and takes no module@actor selector:
every live instance of every namespace the module publishes moves to the
successor together, or none does, and each keeps its address and mailbox.
Identical bytes answer Ok with no swap. Before any instance is touched the
host refuses the whole republish, naming each instance a check refuses, when the
module is content-addressed, already has a republish in flight, declares a boot or
succeeds one that did, drops a namespace or narrows a handler row or fallback of
one, adds a dependency that is not live to a live instance's type, or leaves an
instance without a config of its type's kind. An instance whose type's config
kind changed needs a configs entry, {address, config | config_path}, which
the harness encodes to the successor's Config kind; every other instance keeps
its stored config. An inline child cannot be given a config, so a config-kind
change on a live inline child's type is refused.
Then every instance prepares its successor while mail for it waits at its inbox
gate, the module publishes, and every instance commits, unwiring the old guest
first and receiving the waiting mail in order. The reply, {engine_id, types: [{namespace, capabilities}]},
comes once every commit's chain has settled. A refusal while preparing, or a
publish failure, aborts every instance: each unwires its candidate exactly when
that candidate wired, reinstates its old guest, still wired, which gets back
the state its on_dehydrate saved through its on_rehydrate, with no second
wire. Only teardown outside that saved state and outside what wire built
stays gone. Nothing a failed successor sent leaves. Loads and
drops of the module's namespaces that arrive meanwhile wait for the answer.
Require an explicit successful result, then re-run describe_component and a
safe probe. After an error MCP's cache and the capability registry describe the
old handler sets, which a reinstated guest may no longer fully serve if its
hooks tore down state its saved state does not carry. Use
Replacement failure states rather
than treating describe_component as rollback proof.
Component cleanup
For a task-owned instance in a shared engine:
- Preserve its logs and any trace evidence.
-
Send settled
aether.component.dropthroughsend_mail. -
Require
aether.component.drop_resultsuccess. -
Do not use a same-process
describe_componentcache hit or lingering kind descriptor as unload confirmation. - Record that the name is retired, not freed: a reload needs a new name.
For an engine wholly owned by the task, terminating the engine is the simpler complete cleanup: all its live registries and components die with it. This does not delete the hub's stored component artifacts.
Source routes
- Tool contracts and shapes:
crates/aether-mcp/src/tools/mod.rsandargs.rs - Upload/list/publish/spawn/load orchestration:
crates/aether-mcp/src/tools/components/ - Selector resolution, boot staging, and live kind caching:
crates/aether-mcp/src/tools/state.rs - Hub artifact store:
crates/aether-fleet/src/store/ - Component host publish/spawn/load/list/describe/drop:
crates/aether-component/src/component/ - Drop-closes-the-instance behavior:
crates/aether-component/src/trampoline/runtime/mod.rs - Trampoline republish behavior:
crates/aether-component/src/trampoline/runtime/republish.rs - Live kinds:
crates/aether-inventory/src/
For failure branches, continue with Recovery.