> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://rivet.chrisboulton.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://rivet.chrisboulton.com/_mcp/server.

# Resource and provider authoring

> Registered definitions, typed assets, relationship hooks, and host lifecycle contracts.

For separately distributed executables, use the [plugin SDK](/contributing/plugins), which adapts the same portable definition and runtime contracts to persistent RPC. The in-tree workflow below requires rebuilding Rivet.

An in-tree resource type has one authoring directory in `rivet-resources`, containing its portable definition and host runtime. No catalog enum, compiler match, engine match, or central capability list needs an edit: register the type by adding one line to each explicit list in `crates/rivet-resources/src/resources/mod.rs` (`pub mod`, `builtin_definitions`, and for runtimes `linux_runtimes` and `linux_capabilities`). `tests/registration_contract.rs` fails if a resource directory is missing from those lists.

```text
crates/rivet-resources/src/resources/<type>/mod.rs          # declares the files below
crates/rivet-resources/src/resources/<type>/definition.rs
crates/rivet-resources/src/resources/<type>/runtime.rs      # optional, Linux only
```

Use a stable lowercase type name and a matching Rust module directory (lowercase letters and underscores). Keep desired models, compilation helpers, lifecycle comparison, and native primitives as sibling modules in that directory. Definitions compile on a controller; the runtime module is gated with `#[cfg(all(feature = "runtime", target_os = "linux"))]` in `mod.rs`. Keep host-independent policy (planning, comparison, verification) in a `<type>_lifecycle.rs` module gated only on the `runtime` feature, so in-memory test runtimes reuse it on every platform.

## Builds and shared host infrastructure

The default `rivet-resources` build contains portable definitions only. Controllers use `rivet-resources` without extra features; Linux runners enable `features = ["runtime"]`. The runtime feature enables the engine contracts and, on Linux, shared host dependencies. Native runtimes and registry construction remain Linux-gated; lifecycle helpers stay available to test runtimes when the feature is enabled.

```sh
cargo check -p rivet-resources --no-default-features --locked
cargo check -p rivet-resources --features runtime --locked
```

The CLI starts with `rivet_resources::builtin_definitions()` and `rivet_resources::linux_runtimes()`, then adds the definitions/runtimes of plugins approved by its `PluginPolicy`, including service and package backends, through the same registries. It passes `rivet_resources::linux_capabilities` to `rivet_linux::collect_local_facts` (or its custom-facts variant), so capability support comes from the same resource/provider probes used by runtime availability. `rivet-linux` has no resource definitions, registration, or dependency on `rivet-resources`; it supplies bounded processes, protected filesystem primitives, local account database access, facts, host locks, and state-directory safety. Resource-specific file staging, account-home policies, package database interpretation, and native provider operations live beside their owning resource runtimes.

## Portable definition

Implement `rivet_catalog::ResourceDefinition` with a serde `Desired` structure and `TYPE`/`SCHEMA_VERSION`. Use `#[serde(deny_unknown_fields)]`; choose strict, deterministic field representations. Debug implementations and diagnostics must redact secret values. The envelope itself has redacted debug output, but that does not protect separately formatted typed structures.

| Hook                           | Author responsibility                                                                                                                                                                                                            |
| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `schema`                       | Declare each attribute once with its coarse type, required/null policy, aliases, export flag, and sensitivity flag; child blocks use the same descriptors. Common `meta` fields belong to the compiler.                          |
| `supported_capabilities`       | Publish every capability the definition can require, including conditional behavior. The aggregate baseline is generated from definitions/providers.                                                                             |
| `compile`                      | Interpret already evaluated, parser-neutral values; apply defaults and normalization; return typed desired state plus deferred asset requests. Preserve parameter locations in safe errors.                                      |
| `validate`                     | Purely validate deserialized desired state and recompute its `ResourceSemantics`. Never trust serialized capability, ownership, or sensitivity annotations.                                                                      |
| `resolve_exported_attribute`   | Lazily read only the parameters needed for the requested export, with the same null/default/normalization rules as `compile`.                                                                                                    |
| `derive_catalog_relationships` | Optionally derive ordering/action edges for the current resource and its typed desired state against a read-only catalog view. Required edges are inserted at compile time and checked on receipt; fabricated action edges fail. |

Every definition hook is pure: no host inspection, asset download, process execution, or mutation. `ResourceCompileInput` exposes evaluated values, child-block values, target assumptions, logical identity, and source locations. `ParameterReader` evaluates static fields lazily; the compiler automatically merges dependencies and sensitivity from **every** read into the returned value. Transformations therefore cannot drop provenance accidentally. Omitted/null optional values must agree with compilation; avoid exporting a default that compilation interprets differently.

### Attribute descriptors and ensure

`DeclarationSchema.attributes` contains `AttributeDefinition` values. For example:

```rust
DeclarationSchema {
    attributes: vec![
        AttributeDefinition::string("path").required().exported(),
        ExampleDefinition::ensure_attribute(),
        AttributeDefinition::string("password_hash").nullable().sensitive(),
        AttributeDefinition::list("argv", AttributeType::String),
        AttributeDefinition::map("environment", AttributeType::String),
        AttributeDefinition::new("owner", AttributeType::ResourceHandle),
    ],
    ..Default::default()
}
```

Use `List`, `Map`, `Union`, and `Enum` for nested collections, alternative types, and string vocabularies. These checks never coerce input. Provider options also reject resource handles. Ranges, paths, normalization, and cross-field checks still belong to the resource. Descriptor defaults are declarations for an explicit shared resolver; the compiler does **not** inject defaults or erase null keys in compile maps. Default values and evaluated values must never appear in diagnostics or debug output.

Names and aliases must be unique within their field namespace. `meta` and child-block names cannot collide with root attributes. Names such as `tags` remain available to resources because common metadata lives inside `meta`. Source aliases are canonicalized before evaluation and compilation; supplying both spellings fails before either expression is evaluated. Export aliases are canonicalized before `resolve_exported_attribute`. Compiler-backed `ParameterReader::read_parameter` and `has_parameter` therefore use canonical names. Child descriptors may declare types, aliases, null policy and sensitivity, but `exported()` is rejected: child exports have no traversal syntax.

Compose the optional `Ensurable` trait when a resource has a desired condition. This complete portable definition uses the shared standard `Presence` enum; its omitted ensure is `present`, explicit null is rejected, and both compilation and lazy export use the same resolver:

```rust
use rivet_catalog::*;
use std::collections::BTreeSet;

struct ExampleDefinition;
impl Ensurable for ExampleDefinition {
    type Ensure = Presence;
}
impl ResourceDefinition for ExampleDefinition {
    type Desired = Presence;
    const TYPE: &'static str = "example";
    fn test_view(desired: &Self::Desired) -> Result<ResourceTestView, CatalogError> {
        Ok(ResourceTestView::new([("ensure", serde_json::json!(desired))]))
    }
    fn schema() -> DeclarationSchema {
        DeclarationSchema {
            attributes: vec![Self::ensure_attribute()],
            ..Default::default()
        }
    }
    fn supported_capabilities() -> BTreeSet<String> {
        BTreeSet::from(["example.local.v1".into()])
    }
    fn compile(input: &ResourceCompileInput<'_>)
        -> Result<CompiledResource<Presence>, CatalogError>
    {
        let ensure = Self::ensure_from_input(input)?.ok_or_else(||
            CatalogError::new("Example ensure is required", Some(input.source.clone())))?;
        Ok(CompiledResource::without_assets(ensure))
    }
    fn validate(_: &Presence) -> Result<ResourceSemantics, CatalogError> {
        Ok(ResourceSemantics::new("example.local.v1", 60_000))
    }
    fn resolve_exported_attribute(_: &ResourceId, _: &TargetAssumptions,
        input: &mut dyn ParameterReader, _: &str, source: &SourceLocation)
        -> Result<EvaluatedExpression, CatalogError>
    {
        Self::ensure_export(input, source)
    }
}
```

For a custom vocabulary, supply a serde string enum as `Ensure` and override `ensure_values`, `ensure_default`, and optionally `ensure_null_policy`, `ensure_aliases`, and `normalize_ensure`. The vocabulary includes accepted inputs and canonical outputs. The shared resolver validates input, applies resource-owned normalization, validates the canonical result, and retains every dependency and sensitivity flag. It reads only ensure. Add its generated descriptor to your schema and call the same helpers from compilation and static export. No engine branch is required. Action resources such as exec need not implement this trait, and it does not define inverse commands or host lifecycle behavior.

Groups and users reuse standard presence. File uses `file`/`directory`/`symlink`/`absent`, defaults to `file`, and accepts `present` as a value alias whose compiled and exported value is `file`. Package retains `present`/`absent`/`purged`/`latest`. Service uses its own `running`/`stopped` vocabulary with no default activity; omitted or null ensure leaves activity unmanaged. Its legacy `state` spelling is a source and export alias for ensure, and supplying both is rejected. Omitted/null service ensure and state exports remain errors because activity cannot be discovered on a controller. `stopped` means inactive: it does not delete the unit, disable it, or mask it. Enablement and masking remain independent fields.

Typed receipt validation remains mandatory independently of declaration metadata: recompute capabilities, claims, assets, and intrinsic sensitivity in `validate`, and reject invalid deserialized desired state.

`ResourceSemantics` lists required capabilities, **all** asset digests, intrinsic sensitivity, physical ownership claims, affected paths, refresh acceptance, a positive operation timeout at most 24 hours, and required shared actions. Claims have an author-chosen physical domain/key/qualifier; `"*"` overlaps any qualifier. Different logical declarations cannot own the same physical object, even across provider types. Path summaries allow generic protection of runner state and locks. `CatalogView::resources_of::<D>()` performs strict typed decoding for catalog analysis.

### Typed deferred assets

Construct source/checksum/location metadata in `AssetRequest`, then attach its mandatory typed setter with `assign_to`. `CompiledResource<Desired>.assets` contains `AssignedAssetRequest<Desired>` values; an unassigned request cannot enter that vector. For example, this function produces a request for the existing file desired model:

```rust
use rivet_catalog::*;
use rivet_resources::FileDesired;

fn content_request(content: String, location: SourceLocation)
    -> AssignedAssetRequest<FileDesired>
{
    AssetRequest {
        content: Some(content),
        source: None,
        checksum: None,
        checksum_location: None,
        checksum_error: "File content checksum mismatch",
        location,
    }.assign_to(|desired: &mut FileDesired, digest| {
        desired.content = Some(digest);
        Ok(())
    })
}
```

The setter is a function pointer or noncapturing closure, invoked once for that request. It must be pure, update only its typed desired state, and return value-free `CatalogError`s on failure. Multiple fields use separate requests/setters beside their metadata. Setters and erased callbacks exist only during compilation; they never enter wire desired state or journals. The adapter strictly decodes/re-encodes this resource's payload around the typed assignment. The compiler calls the consumed erased callback only after verifying that asset's bytes and checksum; setter failure prevents publication.

Content can be inline text or a parsed local/cloud source with a checksum and safe diagnostic location. Whole declarations, outputs, ownership and relationships are validated **before any source acquisition**. The compiler confines and bounds acquisition, verifies checksums, deduplicates bytes while retaining strongest sensitivity, assigns verified digests, then validates final desired state and all asset references. Existing empty-content digest placeholders remain in draft desired values until assignment; authors must validate both draft and final state through the current protocol. Runtime code resolves only verified bundle assets through its context. The extension fixture exercises two independent asset slots without core changes.

### Manifest test views

[Manifest and module tests](/guides/testing) use the required `ResourceDefinition::test_view(&Self::Desired)` hook to inspect compiled desired state without a host runtime. Build the view with `ResourceTestView::new`, declaring each attribute explicitly. Wire field names, serde omission rules, and scalar or tuple payload shapes must not determine the assertion API.

Use canonical declaration names and values, such as service activity exposed as `ensure`, and include null for unmanaged nullable attributes. `ResourceTestView::octal_mode` formats an optional permission mask as a four-digit octal string, preserving null. Set `ResourceTestView::content` to the managed asset digest to enable UTF-8 content matchers; leave it `None` when content is unmanaged. The test runner resolves the asset once for both attribute and content assertions and applies sensitivity redaction.

Keep this hook pure: it must not inspect the host, execute commands, fetch assets, or change desired state. Check its attribute projection and content selection when adding a resource type or changing its desired model. Runtime convergence still needs separate disposable Linux tests.

### Per-resource relationships

`derive_catalog_relationships(resource, desired, catalog)` receives this instance's `CatalogResource` and decoded `Self::Desired`; it returns `Vec<CatalogEdge>` and defaults to empty. The registry calls it for each resource, with no resource-type list. Use the fallible `CatalogView::resources_of::<D>()` for strict typed peer decoding. `resources_with_semantics()` borrows generic peers and their semantic summaries from one freshly validated snapshot for this derivation stage. Each independent receipt validation recomputes the snapshot through registered typed `validate` hooks; serialized annotations are never trusted.

For example, a definition whose desired state is the existing service model can use this hook inside its `ResourceDefinition` implementation:

```rust
fn derive_catalog_relationships(
    resource: &CatalogResource,
    desired: &Self::Desired,
    catalog: &CatalogView<'_>,
) -> Result<Vec<CatalogEdge>, CatalogError> {
    if !desired.daemon_reload { return Ok(vec![]); }
    let mut edges = Vec::new();
    for (producer, semantics) in catalog.resources_with_semantics() {
        if semantics.affected_paths.iter()
            .any(|path| rivet_resources::is_service_definition_path(path))
        {
            edges.push(CatalogEdge {
                prerequisite: producer.id.clone(),
                dependent: resource.id.clone(),
                reason: EdgeReason::Action("systemd.daemon_reload.v1".into()),
                source: producer.source.clone(),
            });
        }
    }
    Ok(edges)
}
```

Service therefore accepts unit-definition producers of any registered type declaring affected paths; it does not depend on file's model. User derives this user's group ordering and contradictions against typed group peers. Relationship hooks are pure and never invoke asset setters, downloads, host inspection, or processes. Compilation and receipt use the same derivation; central validation rejects missing required edges, fabricated action edges, unknown endpoints and cycles.

Supply the registration function and add `<type>::register_definition(&mut registry)?;` to `builtin_definitions()`:

```rust
pub(crate) fn register_definition(
    registry: &mut rivet_catalog::DefinitionRegistry,
) -> Result<(), rivet_catalog::CatalogError> {
    registry.register::<ExampleDefinition>()
}
```

`builtin_definitions()` rejects duplicate/reserved names. The catalog/compiler accept explicit registries for custom compositions and tests. Bundle validation and publication require the same registry; unknown or unsupported types fail before mutation.

## Host runtime

Choose `rivet_engine::StateResourceRuntime` when success can be established by reading host state and comparing it with managed desired properties. Use the full `ResourceRuntime` for dynamic ownership or affected paths, prerequisites, maintenance without property changes, incomplete previews, refresh actions, or custom verification.

Both interfaces use **check → durable intent → apply → verify → acknowledgement**. Reads and comparisons must not write host state. Planning checks may repeat after prerequisites and their plans may be discarded. Preflight uses a separate required `inspect_safety` hook on both runtime interfaces; it must freshly return all dynamic ownership and affected paths without reading managed content, running guards, querying package plans, or validating calendars. Static-only runtimes return `RuntimeSafety::default()`. An inspection error blocks the gate rather than silently dropping claims. Application runs only after the engine commits intent and causal obligations; its `OperationContext` then carries the journalled staging name. Preflight inspects every resource once; before each mutation (recovery, a shared action, or application) the engine freshly re-inspects only the mutating resource and compares its claims with the latest claims of all others. Noop reads and reports expected changes without application, recovery, or journal writes.

### State-based resources

Implement `StateResourceRuntime` with the portable `Definition` and an ordinary implementation-local `State`. The three required operations are `read_state`, `expected_changes`, and `apply_state`, plus the usual `available_capabilities` probe. `State` requires no serialization; an `Option<State>` can represent an absent object. `expected_changes` returns property names and decides what to manage explicitly: there are no property/parameter schema flags or automatic field-equality rules. Omitted properties remain unmanaged. Existing definition schemas, compilation, `Ensurable`, validation, exports and child blocks keep their contracts.

For example, the file runtime delegates native operations and managed comparison:

```rust,ignore
pub struct ResolvedFileState {
    intent: ResolvedFileIntent,
    observed: FileObservation,
}

impl StateResourceRuntime for FileRuntime {
    type Definition = FileDefinition;
    type State = ResolvedFileState;
    fn available_capabilities(&self) -> BTreeSet<String> {
        runtime_capabilities(std::path::Path::new("/"), "")
    }
    fn operation_label(&self) -> &'static str {
        "file_mutation"
    }
    fn inspect_safety(&mut self, _: &FileDesired, _: &OperationContext<'_>) -> Result<RuntimeSafety, ResourceError> {
        Ok(RuntimeSafety::default()) // File paths are declared by the definition.
    }

    fn read_state(
        &mut self,
        desired: &FileDesired,
        ctx: &OperationContext<'_>,
    ) -> Result<Self::State, ResourceError> {
        let intent = ResolvedFileIntent::resolve(desired, ctx.deadline())?;
        let observed = inspect_resolved_file(&intent, ctx.deadline())?;
        Ok(ResolvedFileState { intent, observed })
    }
    fn expected_changes(
        &self,
        _desired: &FileDesired,
        current: &Self::State,
    ) -> Result<Vec<String>, ResourceError> {
        file_changed_properties(&current.intent.desired, &current.observed)
    }
    fn apply_state(
        &mut self,
        desired: &FileDesired,
        current: &Self::State,
        ctx: &OperationContext<'_>,
    ) -> Result<(), ResourceError> {
        apply_resolved_file(
            &current.intent,
            &current.observed,
            ctx.asset_bytes(desired.content.as_ref())?,
            ctx.staging_name()?,
            ctx.deadline(),
        )
    }
    fn recover_interrupted(
        &mut self,
        desired: &FileDesired,
        operation: &InterruptedOperation,
        ctx: &OperationContext<'_>,
    ) -> Result<(), ResourceError> {
        recover_file_with_deadline(desired, operation.staging_name(), ctx.deadline())
    }
}
```

Register it with `registry.register(StateRuntime::new(FileRuntime))`. The adapter reads state, builds the plan, carries checked state into application, then reads fresh state and repeats the same comparison to verify success. A failed verification, including a read failure or expired deadline, leaves the durable intent unresolved. It retains one fixed deadline and rejects refresh requests. Expected property names become verified report names only after postconditions succeed; a backend needing different outcome reporting should use the full lifecycle.

`apply_state` receives both desired and current state, so the author can distinguish creation, update and removal. Split these into ordinary helper methods where useful; separate create/update/delete trait methods are not required. Providers must preserve native drift checks before writes. Optional `operation_label` controls the report label; optional `recover_interrupted` establishes safe continuation. Recovery refuses automatic replay by default. The file implementation explicitly removes only its recorded staging candidate, and the group implementation explicitly permits safe reconvergence.

See the file runtime (`crates/rivet-resources/src/resources/file/runtime.rs`), the group runtime (`crates/rivet-resources/src/resources/group/runtime.rs`), and the synthetic resource integration (`tests/extensions/resource/runtime.rs`). The native file runtime calls filesystem functions directly from file_operations.rs (`crates/rivet-resources/src/resources/file/file_operations.rs`). It has no host trait or separate provider object. In-memory test runtimes in `crates/rivet-engine/tests/support` implement the same engine traits and reuse each resource's lifecycle helpers, so only host I/O is faked.

### Full resource lifecycle

Implement `rivet_engine::ResourceRuntime` with three associated types: the portable `Definition`, typed `Work` to apply, and a temporary `ApplyResult` for verification. Work and application results exist only in memory, require no serialization, and are never saved in catalogs or journals. Registry type erasure is internal; authors never implement erased dispatch. Services, users, exec and the shared package resource runtime use this interface for their additional lifecycle behavior. Package providers implement backend operations rather than this lifecycle.

| Hook                     | Author responsibility                                                                                                                      |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `available_capabilities` | Call the same capability probe used by facts collection; advertise only support actually usable on this host.                              |
| `validate_runtime`       | Optionally validate nested adapter options and versions without host inspection or journal creation.                                       |
| `begin_run`              | Optionally reset per-run caches before preflight.                                                                                          |
| `composition_conflicts`  | Inspect all typed declarations of this runtime once per preflight and return the IDs participating in incompatible shared host policies.   |
| `inspect_safety`         | Freshly resolve dynamic identities and affected paths independently of planning; failure blocks mutation.                                  |
| `check`                  | Read current state, compare managed properties, and return a plan with affected paths and any declared prerequisite.                       |
| `apply`                  | Perform the typed `Work` within the shared deadline using the engine's recorded staging name. Return temporary verification data.          |
| `verify`                 | Reinspect or interpret `ApplyResult` according to resource-specific success rules; return a confirmed `ResourceOutcome`.                   |
| `recover_interrupted`    | Optionally implement safe continuation after interrupted work. The default refuses automatic recovery and leaves the operation unresolved. |

See the service runtime (`crates/rivet-resources/src/resources/service/runtime.rs`) for refresh actions and dynamic ownership, and the user runtime (`crates/rivet-resources/src/resources/user/runtime.rs`) for affected home paths and resource-specific verification.

`RuntimeSafety` from `inspect_safety` is the sole runtime authority for dynamic ownership claims. `CheckResult` retains additional `affected_paths`, including `InSync` and `Skipped` plans. Set `prerequisite` only to a declared shared action key. After executing that action, the engine discards the earlier check result, checks ownership again, and calls `check` again to build fresh work. Do not put host writes or maintenance in a check.

`ResourcePlan` distinguishes `InSync`, `Skipped(reason)`, `CannotPlan(reason)`, and `NeedsApply { expected_changes, work, incomplete_preview_reason }`. Expected changes contain property names for noop, not desired values. An empty list allows required maintenance without inventing resource changes. `incomplete_preview_reason` describes information unavailable without application. Keep omitted properties unmanaged. `CannotPlan` produces a deferred report with its explanation; report statuses are distinct from runtime planning decisions.

`composition_conflicts` receives the complete typed declaration group explicitly, before individual checks. Before a mutation it is repeated for the mutating resource's type, so prerequisites that create accounts or change aliases are observed. Conflicts block writes before an intent is recorded. Composition safety checks use the longest declaration timeout in their group; each individual safety inspection uses its declaration timeout. Static semantics and runtime pairings are validated at run entry. Mutation gates repeat only the mutating resource's safety inspection and its type's composition policy; unrelated resources are not re-inspected and full planning never runs against the mutating resource's deadline. Ordinary checks receive no catalog or implicit peer list. A direct `OperationContext::new` check inspects one resource; use registry preflight to validate the whole composition.

Every hook receives one `OperationContext` exposing `asset_bytes()`, `deadline()`, `reason()` and, during application only, `staging_name()`. `OperationReason::Converge` manages desired properties; `Refresh` is an action requested by a notification. `staging_name()?` is the exact recorded candidate identity and errors outside application. There is one fixed monotonic deadline across recovery, prerequisite work, checking, application, and verification; native operations must honor the remaining time. `ctx.deadline()` returns the shared `OperationDeadline`: use `.check()?` between native steps, `.remaining()?` for a remaining duration, or `.instant()?` for an existing primitive accepting an absolute `Instant`. These accessors never restart the timeout. Native `inspect_file` and `apply_file` functions each take `&OperationDeadline`. Named IDs and parent access policy are resolved at the runtime boundary. There are no timeout-free variants. File work retains `ResolvedFileIntent` separately from `FileObservation` so reads contain only host state. Shared-file writers use `snapshot_managed_file` to obtain bytes, digest, metadata and fingerprint from one verified descriptor read. `OperationContext::new` supports direct hook tests with already-verified assets; add `with_staging_name` to exercise application.

`ResourceOutcome` contains verified changed property names, optional safe output, `executed`, and `handled_refresh`. Execution is independent of output capture. `handled_refresh` means ordinary convergence also satisfied a queued refresh, such as starting a service with its new configuration. A command must not be executed again merely to verify its result.

`recover_interrupted` receives `InterruptedOperation`, which exposes only its recorded staging name and operation reason. Journal sequences and acknowledgement remain engine-owned. Returning success establishes safe continuation, not historical success. File recovery removes only its recorded candidate; resources that safely reconverge explicitly choose that policy; an interrupted exec follows its declared guard, always, or refresh-only policy.

Use named `ResourceError` constructors, which set its `HostEffect`: `before_change` for failures before side effects, `partial_change` for a known failed partial change, `unknown_outcome` when an operation may have run, and `partial_change_unknown` when some change is confirmed but the final outcome is unknown. `after_change(changed)` upgrades an error raised after a host change. Attach underlying errors with `.because(&cause)`; it appends only a value-free description. Never include secret arguments, passwords, or asset bytes in messages. Verification failures leave intent unresolved.

Shared prerequisites implement `ActionRuntime` with a stable versioned key, report label, capability, availability probe, and `execute_action(&OperationContext)`. The engine commits delivery before execution, coalesces requests, and repeats checks after the action.

Supply both Linux functions and list them in `linux_runtimes()` and `linux_capabilities()`:

```rust,ignore
#[cfg(target_os = "linux")]
pub(crate) fn register_runtime(
    registry: &mut rivet_engine::RuntimeRegistry<'_>,
) -> Result<(), rivet_catalog::CatalogError> {
    registry.register(ExampleRuntime::new())
}

#[cfg(target_os = "linux")]
pub(crate) fn runtime_capabilities(
    root: &std::path::Path,
    init_system: &str,
) -> std::collections::BTreeSet<String> {
    // Probe trusted native helpers; do not advertise unavailable support.
    todo!()
}
```

`linux_runtimes()` and `linux_capabilities()` list every runtime's registration and probe. Both facts collection and runtime availability must call the same `runtime_capabilities` or `provider_capabilities` probe rather than maintaining separate tool lists. An in-memory runtime can be registered directly for tests without invoking native mutation.

## Package providers

Package provider definitions and runtimes are also collocated in one directory:

```text
crates/rivet-resources/src/resources/package/providers/<provider>/definition.rs
crates/rivet-resources/src/resources/package/providers/<provider>/runtime.rs
```

Implement `PackageProviderDefinition` with serde `Options`, stable `NAME`, independent `SCHEMA_VERSION`, and `CAPABILITY`. Declare `attributes() -> Vec<AttributeDefinition>`, optional `auto_priority(target, has_source)`, architecture normalization, and pure validation returning semantic claims/assets/paths. Auto selection considers only providers whose capabilities exist in the target, chooses the highest priority, and rejects tied winners. Return `None` when the provider is inapplicable; explicit selection still passes strict validation. Reject unsupported common package policies explicitly. Native version ordering, repository selection, archive validation and mutation commands belong to each backend.

The definition's `register_definition(&mut PackageDefinitionRegistry) -> Result<(), CatalogError>` is listed in `package_definitions()` in `providers/mod.rs`. On the host, implement `PackageProvider` with the definition's `Options` type, `available`, and two backend operations:

```rust,ignore
fn read_state(
    &mut self,
    desired: &PackageDesired,
    options: &AptOptions,
    ctx: &OperationContext<'_>,
) -> Result<PackageObservation, ResourceError> {
    self.inspect_apt_package(desired, options, ctx.deadline().instant()?)
}

fn apply_state(
    &mut self,
    desired: &PackageDesired,
    options: &AptOptions,
    current: &PackageObservation,
    ctx: &OperationContext<'_>,
) -> Result<(), ResourceError> {
    self.apply_apt_package(desired, options, current, ctx.deadline().instant()?)
}
```

`read_state` must not mutate the host. It translates backend state into the common `PackageObservation`, resolves the target version using native semantics, and identifies pending index maintenance. `apply_state` receives the previously checked state and writes only after durable intent; it preserves backend drift checks and classifies side effects on error. Optional `begin_run` resets per-run caches; optional `recover_interrupted` establishes safe continuation and refuses replay by default. Provider authors do not implement plans, verification, application-result types or type-erased work.

The runtime's `register_runtime(&mut PackageProviderRegistry<'_>) -> Result<(), CatalogError>` registers the provider under its definition's descriptor (`registry.register(AptDefinition::descriptor(), None, AptPackageProvider::default())`); it and `provider_capabilities(root)` are listed in `linux_package_providers()` and `linux_package_capabilities()`. Providers have independently typed `Options`; one internal adapter validates and decodes them, without `Any` or state/result downcasts. The shared `PackageRuntime` owns comparison, held-package policy, planning, incomplete previews, verification and reporting. It rereads state after native application and confirms managed postconditions before acknowledging intent. Reported changes come from actual before/after installed state, so index maintenance alone does not notify dependent resources. It owns no provider-name match or closed provider union. Definitions declare ownership, assets and affected paths before runtime execution.

Service backends have the same shape: `ServiceProvider` with typed `Options` and `canonical_name`, `inspect`, `apply` and `refresh`, registered with `systemd_service_descriptor()` (or a plugin's descriptor) into a `ServiceProviderRegistry` that the shared `ServiceRuntime` wraps. Systemd's daemon reload is the `SystemdDaemonReload` action runtime rather than a provider method. [Plugins](/contributing/plugins#service-and-package-providers) add service and package backends through the same traits.

Package observations distinguish `installed_version` from `target_version`. The latter may be a pinned desired version, a repository-selected version, or a newer installed version retained by `latest`. `target_version_unconfirmed` means it cannot yet be relied on, even if a tentative value exists. `needs_index_update` is package-index maintenance, distinct from a resource notification refresh. An unconfirmed target produces an incomplete preview when updating indexes can resolve it, or `CannotPlan` when no such work is scheduled. Backends map partially configured packages and retained configuration into the common states rather than claiming they are installed or fully removed.

## Validation and extension proof

Run workspace tests, clippy with warnings denied, and formatting:

```sh
cargo fmt --all --check
python3 tests/resource_architecture_contract.py
cargo test --workspace --locked
cargo clippy --workspace --all-targets --locked -- -D warnings
```

Test defaults against static exports, null handling, strict received payloads, ownership conflicts, all asset assignments, omitted-property preservation, noop immutability, idempotence, and uncertain recovery. Host mutation tests belong in disposable Linux environments.

On Linux, run `python3 tests/resource_extension_workflow.py`. It copies the workspace, adds a synthetic resource and package provider (their files plus one line in each registration list), then exercises compilation, strict bundle receipt, multi-assets, provenance, planning, uncertain apply, recovery, missing runtime rejection, idempotence, provider options, and the registration contract. The script refuses other platforms so a skipped Linux test cannot masquerade as a passed extension proof.

Catalogs, the language, journals and the runner protocol each have exactly one supported version; other versions are refused, never migrated. Resource and provider payload schema versions remain independent and are checked on receipt.