Runtime abilities: modules, packages, and execution
An AOS package can carry both built software and a configuration module. Modules
compose through Nix's recursive options/config fixed point. Ability operations
add typed, deferred host modifications to that configuration. Evaluation produces
data; the Rust runtime executes the selected handlers later.
This guide describes the native package and runtime interface. The examples introduce the machinery with a small service contract; production domain contracts are documented from their actual module declarations. Implementation and qualification progress are tracked in the migration checklist.
flowchart LR
contracts["Interface modules"] --> evaluation["Nix module fixed point"]
packages["Package configuration"] --> evaluation
operators["Operator configuration"] --> evaluation
backends["Handler modules"] --> evaluation
evaluation --> effects["Typed effect graph"]
effects --> transaction["APM transaction infrastructure"]
transaction --> handlers["Selected handlers"]
handlers --> primitives["OS primitives"]
This is package-level machinery. A profile, container, or host is an installation scope using the same evaluator. Abilities are not limited to services: a package can declare operations for filesystem paths, kernel settings, networking, sandboxing, or another domain. Domain definitions belong to packages and system modules. The generic library supplies module types and graph construction.
Installation into an inactive container separates file/configuration effects from startup effects. Missing enabled handlers still reject evaluation; delayed effects remain explicitly pending rather than being treated as successful. See packages in containers for Dockerfile installation, init selection, optional daemon integration, and startup recovery.
#Read an ability declaration
An ability groups named operations. Each operation declares its inputs and results, selects a handler, and contains named effects. This schematic example omits the interface and handler bodies; the complete service example follows.
aos.abilities.filesystem.operations.directory = {
input = { /* Module declaring path, owner, mode, etc. */ };
result = { /* Module declaring the returned path, etc. */ };
handler = { /* Selected implementation module. */ };
effects.database = {
input.path = "/var/lib/database";
input.mode = "0700";
lifetime = "persistent";
};
effects.cache = {
input.path = "/var/cache/example";
input.mode = "0755";
};
};
Here filesystem is the ability, directory is the operation, and database
and cache are two deferred invocations of that operation. Their arguments are
checked against the same input module and interpreted by the selected handler.
During Nix evaluation, effects are typed configuration values. They declare work;
they do not perform it. The generated graph gives each invocation an identity,
dependencies, and a lifetime. The runtime executes the handler later and checks
its returned values against the result module. An effect's outputs contains
typed references to those future results, not values already produced by Nix.
The database invocation explicitly retains its state until retirement. The cache
invocation uses the default instance lifetime: its handler is asked to remove
the managed state when the effect disappears from desired configuration. The
handler defines the actual removal behavior.
#Define an interface with ordinary modules
An ability groups operations. Each operation owns an input module, a result module, an optional handler module, and configured effect instances:
{ lib, ... }: {
aos.abilities.service.operations.ensure = {
input = { name, lib, ... }: {
options = {
name = lib.mkOption {
type = lib.types.str;
default = name;
description = "Name of the service instance.";
};
command = lib.mkOption {
type = lib.types.listOf lib.types.str;
description = "Program and arguments to run.";
};
};
};
result.options.handle = lib.mkOption {
type = lib.types.str;
description = "Service identity returned by the manager.";
};
};
}
These are mergeable module definitions. Another file can extend the same input:
{ lib, ... }: {
aos.abilities.service.operations.ensure.input.options.after = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [];
description = "Service names that must be started first.";
};
}
The resulting operation sees both definitions. Normal module imports, defaults,
priorities, conditional configuration, and type checking apply. There is no
parallel registry of interface schemas, providers, requests, and documentation.
The operation's read-only module value exposes its effect submodule for reuse
by composed handlers.
#Offer a convenient domain option
The service manager can reuse that exact input schema under aos.services and
derive effects from the final merged configuration:
{ config, lib, ... }: {
options.aos.services = lib.mkOption {
default = {};
extensible = true;
type = lib.types.lazyAttrsOf (lib.types.submodule [
config.aos.abilities.service.operations.ensure.input
{ options.enable = lib.mkEnableOption "this service"; }
]);
};
config.aos.abilities.service.operations.ensure.effects =
lib.mapAttrs
(_: service: {
input = builtins.removeAttrs service [ "enable" ];
})
(lib.filterAttrs (_: service: service.enable) config.aos.services);
}
There is one declaration of command, name, and after. Extending the operation
input extends the public service configuration too. Separate modules may set
attributes of the same aos.services.web instance; the evaluator merges them
before the manager derives an effect. extensible exposes this package-owned
option as an extension point to other package modules. The manager owns that derived effect's
lifecycle even when several packages supplied its settings.
A daemon's package module supplies its default command:
{ package, lib, ... }: {
aos.services.web.command = [ (lib.getExe package) "--foreground" ];
}
An operator enables the instance and sets policy:
{
aos.services.web = {
enable = true;
after = [ "network" ];
};
}
The daemon does not need an instances.service = {}; declaration. Its ordinary
configuration is enough. A domain that needs no convenience layer may configure
aos.abilities.<ability>.operations.<operation>.effects.<name>.input directly.
#Select or compose a handler
An implementation package can select its own built program:
{ package, ... }: {
aos.abilities.service.operations.ensure.handler.program = package;
}
package is the explicit retained artifact supplied to that module, including
its outputs and mainProgram. Build-time handlers may also use derivations.
The evaluator lowers this Nix value to an immutable program reference; users do
not duplicate an executable string or manually maintained revision number.
A handler may select a named package output too. For example, systemd keeps its
service, network, and resource handlers in package.handlers, separately from
the manager binaries in package.out. The small handler output retains its
tools through ordinary store references. Admission checks its exact NAR at each
dispatch, without hashing unrelated files in the manager output.
A richer service manager can compose lower operations instead. Suppose
init.install accepts name and command, returning path, while
supervision.ensure accepts definition and after, returning handle.
Declare definition with lib.types.deferred lib.types.str so it can receive
the earlier operation's result:
{ config, ... }:
let
install = config.aos.abilities.init.operations.install;
supervise = config.aos.abilities.supervision.operations.ensure;
in {
aos.abilities.service.operations.ensure.handler =
{ input, children, ... }: {
children.definition = {
imports = [ install.module ];
input = { inherit (input) name command; };
};
children.running = {
imports = [ supervise.module ];
input = {
definition = children.definition.outputs.path;
inherit (input) after;
};
};
exports.handle = children.running.outputs.handle;
};
}
flowchart LR
install["init.install"] -->|"typed path result"| supervise["supervision.ensure"]
supervise -->|"typed handle result"| service["service.ensure result"]
The output reference creates an execution dependency. No Nix expression waits for a running service or reads a runtime result during evaluation. Rust resolves references after their producer completes and checks result types before the consumer runs. The parent exports the child's result without launching a second parent process.
The production service-management interface also composes package resource
groups with services. A daemon package that already declares aos.services.nginx
can supply a group directly and pass its typed result into that service:
{ config, lib, package, ... }:
let
operations = config.aos.abilities.serviceManagement.operations;
group = operations.resourceGroup.effects.${package.name};
in {
config = lib.mkIf config.aos.services.nginx.enable {
aos.abilities.serviceManagement.operations.resourceGroup.effects.${package.name}.input.name = "aos-pkg-${package.name}";
aos.services.nginx.resources.resource_group = group.outputs.name;
};
}
The package owns the group effect. The manager derives and owns the enabled
service effect from aos.services; its deferred resource_group reference adds
an execution dependency on the group producer. The selected handler realizes
the group using its manager's native resource, such as a systemd slice. This
contract belongs to the service-management package; the generic library only
checks the typed reference and constructs the dependency edge.
An advisory service can set lifecycle.start_mode = "enqueue" to acknowledge
the manager's queued startup job. A later process failure remains visible in
the manager without failing the configuration transaction. Its outputs identify
the installed service; they do not assert health. Recovery still requires
execution evidence for an interrupted dispatch and does not blindly run it again.
The production service handler applies lifecycle.configuration_change_action
when rendered units, installation links, explicit activation inputs, or
lifecycle.restart_token change. A new configuration generation used only for
ordering does not restart an otherwise unchanged service. An inactive ordinary
service is still started when restoring runtime state.
A package that reads configuration through a stable path includes the file's resource identity in its activation inputs. Content changes then trigger the declared reload or restart even though the path stays the same:
{ config, ... }: {
aos.services.nginx.activationInputs = [
config.aos.abilities.configuration.operations.file.effects.nginx.outputs.resource
];
}
The file effect remains the single configuration source. restart_token can
similarly carry a derived revision for configuration outside the effect graph.
Interrupted publication retains the previous lifecycle inputs until the required
dispatch completes.
input.after in the composed-handler example is service-manager policy. An effect's own after
field is an explicit activation dependency expressed using output references;
these are different kinds of ordering.
Different implementations may fulfill a rich interface with a single manager or several cooperating programs. Selecting one is ordinary module configuration. Incompatible definitions or missing required input fields fail evaluation. A configured, enabled operation with no handler fails graph construction before activation. Merely declaring an unused interface, or generating reference documentation, does not require a handler. Missing functionality is never silently dropped.
#Build and publish the package
A recipe keeps its ordinary build inputs and adds a module directory:
{ mkDerivation, service-interface, ... }:
mkDerivation {
pname = "web-server";
version = "1";
# Source, build dependencies, phases, and runtime dependencies go here.
module = ./web-server-module; # Contains module.nix.
moduleDeps = [ service-interface ];
}
moduleDeps supplies configuration interfaces or implementations. It is
separate from buildDeps and runtimeDeps. An ordinary payload-only package
needs no module; it still publishes a native deployment envelope.
#Version and compose interfaces across packages
Interfaces follow the release of their owner. An interface shipped by a package uses that package's version; interfaces supplied by the OS/base modules use the OS release version. Ability declarations contain their operations and schemas, without an independent ability version. An effect's automatic revision remains separate from release compatibility.
A package recipe's version also declares its compatibility promise. A bare
strict SemVer such as "7.4.2" defaults to caret compatibility. "^7.4.2",
"~7.4.2", and "=7.4.2" publish the same exact package version 7.4.2, with
caret, tilde, and equal-version requirements respectively. Other bare version
schemes remain exact-only. This shorthand accepts one optional operator followed
by a full release version, not compound ranges; there is no separate
moduleCompatibility field.
Recipes select this policy for their exposed interface. BIND, for example, uses
~9.20.27 to stay within its documented stable branch; a package without a
reviewed broader guarantee uses an equal-version requirement. This does not
change the source release used for download or build. Selected outputs and
bootstrap publication preserve the owning recipe's policy.
Source-language compatibility, binary ABI compatibility, configuration migration, and supported upgrade paths are separate promises. A range does not perform required migrations or enforce cluster version skew. AOS module authors must also preserve their own option and operation contracts within the declared range.
Generated package.versionRequirement retains the inferred requirement. A
consumer can therefore write moduleDeps = [ service-interface ]; without
repeating the interface package's declared range. Consumers capture that range
when their companions are generated; narrowing a provider recipe does not
retroactively change an already-published consumer's retained requirement.
Regenerate and publish those consumer companions to adopt the revised policy.
To override the range, use an explicit requirement:
{ mkDerivation, service-interface, ... }:
mkDerivation {
pname = "web-server";
version = "3.0.0";
module = ./web-server-module;
moduleDeps = [
{
package = service-interface;
packageVersion = "^7.0";
}
];
osVersion = "^1.0"; # Optional requirement on the selected host OS release.
# Ordinary source, dependencies, and build phases go here.
}
package supplies the exact build-time seed. Nix checks its package version
against the inferred or overridden range. APM may select another authenticated
release of the same package that satisfies the range. Package 7.3.0 satisfies
^7.0; package 8.0.0 does not. Declare each dependency package once.
Use moduleDeps = [ { package = service-interface; exact = true; } ]; to pin the
immutable source identity. This differs from packageVersion = "=7.4.2", which
requires that semantic version but can select another source with that version.
osVersion constrains the OS release already selected for the host, retained as
osRelease = { name = "aos"; version = "1.2.0"; } in activation inputs. The host OS
is a fixed input: dependency resolution checks it and never solves for or upgrades
it. A mismatch requires a compatible package choice or a separately managed OS
upgrade. Omitting the requirement imposes no explicit OS release range.
Keep interface changes compatible with the previous release's captured requirement. Caret policy normally permits a breaking change at the next major release; tilde and equal-version policies have narrower guarantees. A new release's policy cannot relax the previous release's obligations. OS release versions remain exact declarations, with default caret policy for the structural check. SemVer is the author's compatibility promise, not a proof that arbitrary Nix modules are interchangeable. Modules can still extend shared option trees.
Range matching and the structural release check require strict SemVer release numbers. Packages with other upstream version schemes can still use exact dependencies; AOS does not guess a SemVer interpretation.
Ranges use Rust SemVer syntax: caret, tilde, comparisons, wildcards, and comma-separated intersections. Prereleases require an explicit prerelease comparator for the same major/minor/patch. A version is bounded to 128 bytes and a range to 4,096 bytes and 32 comparators.
Repositories can obtain interface packages through pinned Nix inputs. For example, a flake can pass an external AOS interface package to this recipe:
# Inside a flake's outputs, with `aosPkgs` the target AOS package set:
aosPkgs.callPackage ./web-server.nix {
service-interface = inputs.interfaces.packages.${system}.service-interface;
}
The external package must expose AOS's native module and deployment companions.
Flakes acquire build inputs; they are not runtime header lookups. Publication
retains the module source, original dependencies, generated interface release
metadata, and documentation in authenticated package artifacts. Installation uses the configured
registries, which may publish packages from different Git repositories. A
dependency does not add registry URLs, signing keys, or trust settings. Package
names remain scope-wide identities; aliases for local registries do not namespace
them.
Resolution selects one exact package identity per package name per scope. Package-owned interfaces follow that selection; OS-owned interfaces follow the fixed host OS release. The resolver solves transitive ranges together, including constraints introduced by candidate packages. Conflicting consumers fail with dependency diagnostics before activation. The bounded solver reports search exhaustion separately from incompatible requirements. Ordinary installation prefers compatible retained choices; an explicit upgrade permits new selections, even when the application payload is unchanged. Upgrade filters, exclusions, and holds preserve the dependency choices of unaffected package owners. A shared dependency must still satisfy those retained choices. An unchanged resolution does not create a new generation.
The resulting resolution lock records original requirements, exact selected module sources, and the requesters' deployment companions. Reconfiguration, boot, and rollback replay these choices without consulting newer registry contents. New package mutations snapshot the currently selected target OS release; an old user profile does not keep admitting packages against an obsolete host version. Pending transaction recovery and rollback check retained package requirements against that current target before applying effects. Historical source replay preserves its original OS identity. Build-time images lock their selected seeds using the same format. Handler selection remains ordinary module configuration; resolving a compatible interface does not discover or select an unrelated service manager, nor rewrite compiled runtime dependencies or literal store paths embedded in modules.
Generated reference documentation uses these same declarations to show interface
release owners and links to requesting packages. moduleRequirements records
owner, package, and packageVersion; osRequirements records owner and
osVersion. aos docs and the Hub expose this metadata without a
second manually maintained ability catalog.
A structural compatibility check compares the generated public input/result schemas from the previous and current release, together with their package or OS release owner. Removed operations or fields, newly required inputs, and type changes are breaking changes or require review. Optional inputs and new operations are compatible additions. Opaque constraints require human review; the check does not prove runtime behavior. A breaking change is permitted when the new release falls outside the previous release's captured compatible range, or through a documented exception explaining the exact change and why it is permitted. This is a focused release check, not another schema catalog.
Compare the generated native references for two releases:
aos ability check-compat before-options.json after-options.json --owner service-interface
aos ability check-compat before-options.json after-options.json --os
Use --owner for interfaces owned by the named package or --os for OS/base
interfaces. The check reports structural changes and checks whether the new
release is outside the previous release's compatibility guarantee. An incompatible
report is still printed before the command exits unsuccessfully. --json emits the report as JSON.
For a reviewed exception, pass --exceptions exceptions.json. The file is a JSON
array naming the exact diagnostic ID and the reason it is permitted:
[
{
"id": "COPY_THE_REPORTED_DIAGNOSTIC_ID",
"reason": "Explain why this exact release change is permitted."
}
]
#Bind runtime dependencies
A runtime dependency list uses each package's name as its module binding. Use a named attribute set when dependencies have distinct roles, including two artifacts from the same package:
runtimeDeps = {
predecessor = previousImage;
candidate = nextImage;
};
The retained module receives dependencies.predecessor and
dependencies.candidate as artifact values. Each keeps its actual package name,
version, selected output, and available outputs; the binding name does not
rename the package. Interpolation such as "${dependencies.candidate}" retains
the selected artifact in the effect graph. The payload builder consumes the
same dependency values as an ordinary list.
Selecting an output installs that payload. The envelope's outputs map describes
other authenticated outputs that modules may reference; it does not install or
retain all of them. Module dependencies make configuration available without
installing their payloads. Evaluation adds outputs actually used by the bound
graph to the transaction's retained inputs. Runtime dependencies remain governed
by the selected payload's actual store references. The pre-evaluation descriptor
also retains the real deployment envelopes for module-only dependencies, so
subsequent offline reconfiguration can resolve their original dependency edges.
These metadata companions do not select additional payload outputs.
Before replacing an image, the host storage backend preserves the physical closure bytes needed by retained images, system-profile generations, pending transactions, and orphaned handlers. GC links alone cannot preserve files that exist only in the image being replaced. The backend verifies existing copies and publishes new copies atomically before writing the replacement root slot.
| Build output | Contents |
|---|---|
| Package outputs | Built software and runtime dependencies |
module | Immutable source directory, with module.nix as entry point |
deploymentArtifact/deployment.json | aos.package.deployment envelope: platform, payload outputs, runtime dependency artifacts, module source and module dependencies |
documentationArtifact/options.json | aos.module.documentation: options and operation reference generated from module evaluation |
The derivation also exposes the corresponding deployment and documentation
Nix values. Once the example package is registered in the package set, the
reference can be exported without building its handler:
nix eval --json --file . pkgs.web-server.documentation > options.json
Use the actual registered package name in that command. Descriptions belong beside mkOption; reference pages are derived
from those declarations and definition provenance. Do not author a second
ability catalog or option table for documentation.
Publication retains these artifacts and the exact module closure alongside the authenticated release. Native release metadata identifies each companion by its store identity, NAR hash, and exact document digest. Hub validates those bindings before indexing the generated options and operations. Publication does not execute effects; a locally imported document is an inspection view, not an authenticated release.
#Evaluate an installation scope
For already selected build-time packages:
lib.evalPackageModules {
scope = [ "host" "main" ];
packages = [ web-server selected-service-backend ];
operatorModules = [ { aos.services.web.enable = true; } ];
}
The names in this example stand for packages defining the interfaces above.
The result contains config, options, documentation, and deployment.
deployment has schema aos.package.transaction; its graph is the generated
aos.activation.graph. The graph is serialized output, not another tree for
operators to configure.
On a deployed system, the package manager supplies resolved immutable module
sources and explicit artifacts instead of constructing derivations. Module
arguments include package, dependencies, packageName, packageVersion,
lib, config, and options. Inline modules can compose with imported files;
file imports remain inside retained source roots.
sequenceDiagram
participant Build as Package build
participant Publish as Publisher
participant APM as Package manager
participant Nix as Pure Nix evaluator
participant Run as Rust runtime
participant OS as Handler / OS
Build->>Publish: Payload, module source, envelope, reference docs
Publish->>APM: Authenticated release and retained artifacts
APM->>APM: Resolve exact module closure and configuration
APM->>Nix: Locked sources, artifacts, operator modules
Nix-->>APM: Typed deferred transaction and documentation
APM->>Run: Retain inputs and prepare generation
Run->>OS: Apply handlers in dependency order
OS-->>Run: Typed results
Run-->>APM: Durable effect completion
APM->>APM: Commit generation
The reusable Rust entry points are aos_deployment::evaluation::{resolve_packages, Evaluation}
and aos_deployment::transaction::Transactions. The aos-package-manager crate
adapts these APIs to package lookup and retained installation state. Evaluation uses
stock Nix in pure/restricted mode with fixed source inputs and import-from-
derivation disabled. It builds nothing and executes no host modification.
Package/version/source conflicts fail before execution.
#Retain inputs before producing the graph
Production evaluation also retains an aos.package.evaluation-input document.
It identifies the immutable module library and its NAR hash, installation scope,
resolved packages, ordered baseline modules, operator configuration snapshot, and
supplemental retained inputs. Its moduleEnvelopes map retains the authenticated
companion for every resolved module, including dependencies whose payloads are
not installed. Offline reconfiguration reads those exact companions; it does
not reconstruct package declarations from flattened configuration.
Supplemental inputs are evidence or artifacts, not modules to import; their original admission must survive later reconfiguration.
It contains no output graph. Packages that need to perform another authorized
evaluation receive its immutable path as the evaluationInput module argument:
{ evaluationInput, ... }: {
aos.abilities.example.operations.evaluate.effects.main.input.source =
evaluationInput;
}
Here example.evaluate stands for a package-owned operation whose source
option accepts that retained descriptor. The path is an ordinary operation
input, so revision tracking and retention apply without a separate invocation
context channel. A caller authenticates the descriptor and its source identities
before admitting it; merely parsing a descriptor establishes no authority.
Baseline modules remain authored source files, rather than snapshots of their final merged defaults. Reconfiguration replaces the operator snapshot and reevaluates the new package closure against those sources. Different outputs of one package share one module identity; selected payload outputs remain explicit.
A retained descriptor can also be replayed without activation:
aos ability evaluate /nix/store/…-evaluation-input.json --nix-store "$AOS_NIX_STORE"
This emits a checked native transaction, verifies the declared library NAR, and keeps source roots alive during pure evaluation. It creates no profile generation or effect journal. It checks input integrity; authenticating the descriptor and its original source authority remains the caller's responsibility.
The selected Nix store supplies source bytes through its store accessor. The
evaluator exports each distinct source root as a bounded NAR and restores a
private, hash-locked read view for pure Nix. This also works when the selected
store's logical paths are absent from the machine's /nix/store. Temporary read
locations do not replace the original source identities in the descriptor or
transaction. Source loading and evaluation share one deadline; each source NAR
is limited to 64 MiB.
#Runtime state, reconfiguration, and recovery
A process handler implements apply, remove, and observe, accepting a JSON
invocation on stdin. Apply returns the declared result object; remove returns
an empty object. Observe reports current results, absence, a safe retry, or an
indeterminate outcome. See the execution contract
for the wire boundary and source locations.
Logical effect identity includes the installation scope, declaring package,
ability, operation, and instance name. Composed children also include their
parent. Upgrading a package does not change that identity merely because its
version or store path changes. Semantic inputs and handler artifacts determine
the revision automatically; changing descriptions does not trigger an update.
after establishes execution order. A dependency's result belongs in input
when changes to that result should change the consumer's resolved revision.
Portable type constraints are checked after Nix merging and again after runtime
result substitution.
| Lifetime | Retention |
|---|---|
instance (default) | Reconcile across generations; remove when the configured instance disappears |
transaction | Remove after its dependent operations finish |
persistent | Retain until explicitly retired, including after package removal |
Explicit retirement is part of the ordinary configuration evaluated into the transaction:
{
aos.activation.retire = [ "<retained-effect-id>" ];
}
Use an exact identity from retained runtime state. It must be absent from the new configured graph and present in the retained or durably retired effect inventory. Keeping a completed retirement declaration in later generations is a no-op; an unknown identity is rejected. Removing a package or pruning a generation alone does not retire persistent state.
Persistence also applies across reboot. Literal configuration-file generations carry retained persistent file bytes and ownership until explicit retirement. Live nonliteral file records retain metadata and existing upper-state custody; they do not synthesize boot-seed bytes. Before host activation, the selected identity backend restores its receipt-owned accounts from checked retained effect records, including accounts whose package has been removed. Existing foreign account rows remain intact; conflicting or incomplete ownership records stop restoration before account changes.
Reconfiguration prepares a new desired document. Handlers receive previous
state when inputs or implementations change. Interrupted mutations are observed
before retry. Effect execution and generation commit are separately durable.
An Installation receipt records checked results and pending startup work; a
Complete receipt requires every selected effect to finish. The retained receipt
prevents replaying completed one-shot work after a crash between these commits.
For installation before startup, see the container guide.
When an interrupted transaction resumes, the controller observes its completed
non-transaction process effects in graph order before continuing the pending
operation. A completion recorded before reboot does not establish that a kernel
resource or a file under /run still exists. Current resources require no
dispatch; absent or safely retryable resources are restored through separately
journaled intents. Each restoration must return the original typed result, so
the pending operation's resolved inputs remain unchanged. An indeterminate
observation or a different result stops recovery.
A further interruption can leave a restoration pending too. Recovery restores
its earlier prerequisites before resuming it, using a bounded stack of original
intents. Journal inspection reports the active restoration separately from the
original pending operation. Completed transaction-scoped effects are not
repeated during this process.
Rollback applies an older retained desired document as a new transaction. It is not automatic reversal of arbitrary external actions. Pruning old generations releases their artifact roots; persistent effects retain separate handler roots. Current-generation pruning is rejected. Journals are bounded and currently have no automatic compaction; pruning roots does not reclaim journal bytes.
Image construction selects package/module slices for the host, initrd, and
container. Host activation uses the same profile/system scope and generation
journal as subsequent package changes. The image supplies the initial desired
state; subsequent boots reconcile the committed profile and recover pending
work. They must not overwrite an installed profile with the original image's
package selection. Container images select a smaller package-managed base.
Boot reconciliation preserves the committed package generation, evaluation descriptor, and publication marker. It records a separate execution attempt bound to that generation's exact content, observes live resources, and repairs drift through the selected handlers. An interrupted attempt resumes its original identity and returns after completing that pending work, without starting a second transaction. This also applies when boot resumes a pending package activation, after validating its retained bootstrap sources. A publication-only repair after effects already completed still requires live reconciliation: repairing a profile pointer does not establish current resource state. Installation, removal, reconfiguration, and rollback publish new package generations; replaying the same committed state at boot does not.
On EFI systems, initrd stores durable generation, effect-journal, and admission
files as regular files on the boot ESP's FAT filesystem. Nix retention symlinks
live separately on a private POSIX tmpfs mounted at the journal's roots
directory for the current boot; this mount moves with /run through host handoff.
Before dispatch
or recovering a completed activation receipt, initrd application replays the
locked journals and checks the original artifacts' NAR identities to restore
generation, retained, pending, and release roots with their original ownership
keys. Missing artifact bytes stop restoration; journal evidence alone does not
recreate their payloads.
On the first host activation, the image's immutable bootstrap policy says whether platform metadata is required. When required, the bootstrap bridge checks the exact committed initrd result, retains the accepted host configuration and facts with their original receipt, and evaluates them before any host effects run. Later boots recover the existing profile and preserve subsequent operator changes. They do not reacquire metadata as a replacement for accepted configuration.
Initrd evaluates the accepted configuration only far enough to obtain the typed
aos.provisioning.storage plan. It validates and persists that plan before disk
changes. The capsule retains the original library, module sources, configuration,
and evidence required for this projection; describing the host's available
artifacts does not pull their payloads into initrd. The projection descriptor has
the same data as the host descriptor, with a smaller retained closure.
The host then evaluates and validates the complete activation graph before running host effects. This is a phase boundary: provisioning checks run before disk changes, while failures in unrelated host configuration may be discovered later. The accepted configuration and its original receipts remain available across that boundary.
Configuration-lower construction is an OS-owned operation. Its immutable output is retained by the native transaction and mounted before dependent file and service operations. Image path provenance lets removal hide obsolete baseline entries while preserving unrelated operator edits.
Optional execution observers receive only transaction/effect identities and
journal boundaries, without arguments or results. An observer failure stops
execution; recovery observes the exact durable invocation before retry. Observer
acknowledgements cannot supply a handler outcome. dispatch-started records an
attempt boundary immediately before calling the handler, including attempts
whose call fails. It does not prove the call ran: interruption can occur between
acknowledgement and dispatch. A missing dispatch-returned event therefore does
not establish that no attempt occurred. The observer must already be
available before the first observed dispatch; it cannot observe its own creation.
Image-owned startup or an externally supplied test listener can establish that
prerequisite.
#Journal inspection
Read-only journal inspection uses the same bounded replay model as activation:
aos ability journal activation.journal --format json
The public command reads an observed prefix without taking the journal lock, so it can read while the controller holds that lock: the reader captures the opened descriptor's length and verifies complete frames and native state transitions within that prefix. Later appends do not extend the observation. An incomplete final frame is reported and left unchanged.
Inspection preserves the desired graph exactly as recorded, including its
original schemas and revisions. It reports recorded desired state, pending
invocations, and completion; it is not proof that the writer finished fsync,
that live resources match, or that generation and effect journals agree with
profile publication. It does not repair or execute anything. Stable package
inspection and generation/effect authority readers remain locked. Package
inspection holds a shared generation-journal lock while reading its checked
snapshot and associated profile publication.
The current process transport uses Linux facilities; another execution platform needs a transport implementation as well as its own domain handlers. These interfaces do not themselves establish whole-system boot qualification.
#Image artifacts and runtime handlers
Image construction and delivery use two documents with separate responsibilities.
image-info.json describes the provider's actual filesystem, partition layout,
and EFI artifacts. image-delivery.json describes the downloadable encoding,
filename, digest, size, and compatible targets. Converting a raw image changes
the delivery document while retaining the provider document byte for byte.
The image staging handler reads component paths and identities from the retained provider document. It verifies the selected components before using them; it does not guess filenames from a slot name. Self-contained builds and external finalization share the canonical serializer. Fields requiring measurements or assembly commitments are emitted only when the producer has those facts. Publishing these artifacts still does not execute an image transition.
#Inspect the generated reference and execution path
The native reader works without Nix, a repository checkout, or a running AOS system:
aos docs runtime options.json
aos docs runtime transaction.json --format json
aos docs runtime transaction.json --format html --output execution.html
docs is an alias for doc. A package's options.json shows operation inputs,
results, option descriptions, and the packages that declare contracts, select
handlers, and configure effects. A transaction shows the selected graph's
ordered effects, owners, dependencies, handlers, execution phases, lifetimes,
and revisions.
These are configured relationships, not observations of running processes.
Conditional uses absent from the evaluated configuration cannot be inferred.
In AOS Hub, open /-/runtime-abilities, choose or paste one of these documents,
and select Inspect document. Links connect package/environment owners to
operations and back; transaction dependencies link to their producer effects.
The form submits the document to the Hub for rendering without retaining it.
Use a Hub appropriate for the configuration you are inspecting.
The read-only API accepts raw document JSON:
curl --data-binary @options.json -H 'Content-Type: application/json' \
'https://hub.example/-/api/runtime-documentation?format=html'
Omit format=html to return the parsed JSON after structural validation.
Inspection does not authenticate a release, evaluate configuration, or activate
a graph. Installed-package documentation and Hub release browsing use the same
native reader with their own authenticated package/release context. Package
pages distinguish declarations from configured deployment effects. A system's
reported desired graph and observed state are separate assertions; a document
digest alone is not evidence that a handler ran.
The executable integration example is
package_deployment_check,
using the source-built checks.effects fixture.
It exercises artifact publication, deployment-time evaluation, real handler
execution, generation reopening, reconfiguration, and pruning. It provides a
small executable example of the shared infrastructure boundary.