Reference

Ownership Layers

The canonical ownership model for ecosystem capability truth, repo-level orchestration, simple runtimes, standalone commands, and native prerequisites.

referenceautomation buildersintermediatestable2026-05-30

Purpose

Use this page to decide where prerequisite truth belongs.

The canonical split is capability-first for toolchains and mediation-first for orchestrators.

The goal is one owner per capability and one explicit repo execution story.

  • toolchains own required ecosystem capability truth.
  • orchestrators own repo-level trust, prepare, and mediated task execution.
  • runtimes own simple unmanaged runtime checks.
  • tools own standalone commands on PATH.
  • native_prerequisites own host-native build bundles, shell activation, and selected host package preparation truth.

Ownership rule

Pick the highest useful owner and do not repeat the same capability below it.

  • toolchains are capability-first: version says what the repo needs, while fulfillment says how ota may fulfill it on the selected path.
  • orchestrators do not replace toolchains; they model repo-level trust, install, and selected task execution.
  • Current shipped toolchain names are toolchains.rust, toolchains.node, toolchains.java, toolchains.python, toolchains.go, toolchains.ruby, and toolchains.dotnet.
  • Current shipped canonical fulfillment sources are rustup, corepack, sdkman, uv, go, ruby, and dotnet.
  • Legacy toolchain provider is still accepted for compatibility, but it is not the canonical public model.
  • If a declared toolchain owns the capability, do not also declare it under runtimes or tools.

Command behavior

This is what the ownership model changes in day-to-day use.

Read these behaviors as operational consequences of the contract: what ota can inspect, what it may fulfill, and where repo execution truth should live.

  • ota doctor diagnoses selected toolchains, orchestrators, and native prerequisites without mutation.
  • ota doctor --json and ota check --json expose additive selected-path toolchains[] evidence so automation can see the chosen fulfillment source, backend, target OS, fulfillment mode, and owned capabilities.
  • ota up --dry-run shows selected toolchains before execution.
  • fulfillment.mode: none means diagnose only; fulfillment.mode: run means ota may provision the selected toolchain on the selected run path.
  • ota up and ota run may fulfill selected native prerequisite package-manager guidance (apt, brew, winget, choco, scoop) before rerunning preconditions.
  • keep package-manager lanes aligned to the platform entry they live under: prefer apt under linux, brew under macos, and winget / choco / scoop under windows instead of mixing likely wrong-OS package-manager lanes into the same platform entry
  • avoid mixing opaque install shell glue with manager-owned package lanes on the same platform entry when the shell command is only there to install host packages; keep package-manager truth under apt / brew / winget / choco / scoop, and reserve install for the remaining manual step only when no first-class lane owns it yet
  • when an org policy pack is active, those native package installs must be approved under policies.native_packages.<manager>.approved; ota does not bypass that policy gate
  • tasks.<name>.execution.orchestrator makes mediated execution first-class instead of burying repo truth in run: mise ... shell strings.

Use this lookup when you already know the toolchain owner and need the canonical fulfillment source name Ota expects in the contract.

Shipped Toolchain Sourcesyaml
toolchains:  rust:    fulfillment:      source: rustup  node:    fulfillment:      source: corepack  java:    fulfillment:      source: sdkman  python:    fulfillment:      source: uv  go:    fulfillment:      source: go  ruby:    fulfillment:      source: ruby  dotnet:    fulfillment:      source: dotnet

When to use each layer

Toolchain

A managed ecosystem capability such as Rust, Node, Java, Python, Go, Ruby, or .NET.

When to use it

Use it when ota should understand owned runtimes, owned package managers, components, targets, or fulfillment.

Example

Orchestrator

A repo-level manager such as mise that mediates trust, install, and selected task execution.

When to use it

Use it when the selected repo path must run through one declared manager instead of hiding that truth in shell strings.

Example

Runtime

A simple language/runtime version requirement with no managed ecosystem ownership.

When to use it

Use it when a task only needs node >=24, python >=3.12, or another runtime to exist.

Example

Tool

A standalone command on PATH.

When to use it

Use it for Docker, GitHub CLI, jq, psql, or any command not provided by a declared toolchain.

Example

Before and after

Without the canonical model, repo truth often disappears into setup shell strings.

The first snippet shows the common anti-pattern: the repo works, but all the important ownership is hidden in commands.

The second snippet shows the same intent modeled structurally so ota can validate, diagnose, and mediate it.

This works mechanically, but the repo-level trust, install, and execution story is trapped inside shell commands that Ota cannot govern precisely.

Before: orchestration hidden in shell stringsyaml
tools:  mise: "*" tasks:  setup:    run: mise trust && mise install  server:verify:    run: mise run //server:ci-unit

This is the governed version of the same flow: Node capability lives under the toolchain, while mise owns trust, install, and mediated task execution explicitly.

After: capability + orchestrator truthyaml
toolchains:  node:    version: "24.15.0"    package_managers:      pnpm: "10.33.4"    fulfillment:      source: mise      mode: run orchestrators:  mise:    kind: mise    required: true    config_files:      - mise.toml    activation:      trust: true    prepare:      install: true tasks:  server:verify:    run: //server:ci-unit    execution:      orchestrator:        ref: mise        mode: task

Task requirement examples

Task requirements should select the owner that actually applies to the selected path.

These examples answer a simple question: what kind of thing does this task need before it can run?

Pick the narrowest truthful owner. If the task needs a governed ecosystem, require a toolchain. If it only needs a version on PATH, require a runtime. If it needs a standalone binary, require a tool. If it needs a host bundle, require native prerequisites.

Choose a toolchain requirement when the task depends on an ecosystem Ota understands structurally, such as Node with its owned package-manager lane.

Require A Managed Ecosystemyaml
tasks:  setup:    requirements:      toolchains:        - node

Choose a runtime requirement when the task only needs an interpreter or runtime version on PATH and no deeper ecosystem ownership is necessary.

Require Only A Runtimeyaml
tasks:  start:    requirements:      runtimes:        node: ">=24"

Choose a tool requirement when the task depends on a standalone binary such as Docker or jq rather than a governed language ecosystem.

Require A Standalone Toolyaml
tasks:  docker:proof:    requirements:      tools:        docker: ">=27"

Choose native prerequisites when the task needs a host-native bundle like compilers or platform build tools that do not belong under a language toolchain.

Require Host Build Prerequisitesyaml
tasks:  install:    requirements:      native:        - node-native-build-tools

Duplication boundaries

Duplicate ownership is invalid contract drift, not extra safety.

  • Reject toolchains.node plus runtimes.node or tools.node on the same contract.
  • Reject toolchains.python plus runtimes.python on the same contract, and reject toolchains.python.package_managers.poetry plus tools.poetry when Poetry ownership is explicit.
  • Reject toolchain-provided components such as rustfmt when they are also modeled as standalone tools.
  • Keep one owner for the selected capability and remove the duplicate location instead of carrying parallel truth.

Fulfillment behavior

Capability truth and fulfillment truth are separate on purpose.

The key distinction is this: version says what the repo needs, while fulfillment says whether ota may do anything about it on the selected path.

Use the two examples below as the default decision point: inspect-only when you want diagnosis, explicit run-path fulfillment when you want ota to provision through the declared owner.

  • ota validate validates contract shape only.
  • ota doctor diagnoses missing or mismatched toolchains without mutation.
  • ota up --dry-run shows the exact fulfillment action before execution.
  • Supported fulfillment.mode values today are only none and run.
  • Use fulfillment.mode: none when ota should only diagnose/check the toolchain truth and never provision it on the selected path.
  • ota up and selected ota run may provision only when fulfillment.mode: run is declared and policy permits it.
  • Use fulfillment.mode: run only when the selected repo path should let ota own fulfillment through the declared source.
  • Default fulfillment should be mode: none, so inspection commands never silently download or mutate.

Use mode: none when you want Ota to validate and diagnose the declared capability but never provision or mutate it on the selected path.

Inspect Onlyyaml
toolchains:  rust:    version: "1.94.0"    components:      - rustfmt    fulfillment:      mode: none

Use mode: run only when the selected path should let Ota fulfill the declared toolchain through its canonical source instead of leaving that step manual.

Allow Run-Path Fulfillmentyaml
toolchains:  rust:    version: "1.94.0"    components:      - rustfmt    fulfillment:      source: rustup      mode: run

Current shipped examples

These examples use only the canonical public model.

Treat this section as a quick pattern catalog, not a copy-paste dump.

Each snippet shows the minimum truthful shape for a shipped owner so authors, agents, and crawlers can quickly see where a capability belongs and what Ota owns there.

A fully governed Rust example with version, profile, components, and targets owned by the Rust toolchain rather than scattered across shell setup.

Rust Toolchainyaml
toolchains:  rust:    version: "1.94.0"    profile: minimal    components:      - rustfmt      - clippy    targets:      - x86_64-unknown-linux-musl

A Node example that keeps runtime truth, pnpm ownership, and fulfillment source together so dependency hydration and execution can build on one owner.

Node Toolchainyaml
toolchains:  node:    version: "24.15.0"    package_managers:      pnpm: "10.33.4"    fulfillment:      source: corepack      mode: run

Java owns the JDK capability here, while Maven stays a standalone tool because it is not modeled as a Java-owned package-manager lane today.

Java Toolchainyaml
toolchains:  java:    version: "21"    fulfillment:      source: sdkman      mode: run tools:  maven: "*"

Python owns the interpreter and the uv package-manager lane here, which is the canonical shape when the repo relies on uv-managed Python setup.

Python Toolchainyaml
toolchains:  python:    version: "3.12"    package_managers:      uv: "*"    fulfillment:      source: uv      mode: run

A simple Go example where the toolchain owns both version truth and run-path fulfillment through the Go source.

Go Toolchainyaml
toolchains:  go:    version: "1.24"    fulfillment:      source: go      mode: run

Ruby owns both the interpreter and Bundler lane here, so repos do not need to hide Bundler hydration behind raw bundle install shell glue.

Ruby Toolchainyaml
toolchains:  ruby:    version: "3.3.11"    package_managers:      bundler: "2.5"    fulfillment:      source: ruby      mode: run

A minimal .NET shape where the SDK capability and its fulfillment path are both explicit, keeping the repo off ad hoc installer commands.

.NET Toolchainyaml
toolchains:  dotnet:    version: "9.0"    fulfillment:      source: dotnet      mode: run

Use an orchestrator block like this when repo execution should run through a declared manager such as mise instead of embedding trust and install commands in tasks.

Mise Orchestratoryaml
orchestrators:  mise:    kind: mise    required: true    config_files:      - mise.toml    activation:      trust: true    prepare:      install: true

Use this when repo tasks should run through devbox and ota should own the selected-path install step instead of shell glue.

Devbox Orchestratoryaml
orchestrators:  devbox:    kind: devbox    required: true    config_files:      - devbox.json    prepare:      install: true

Use this when repo tasks should run through devenv and the contract only needs mediated execution in the current slice.

Devenv Orchestratoryaml
orchestrators:  devenv:    kind: devenv    required: true    config_files:      - devenv.nix

What this is not

  • not a second task language
  • not a place for arbitrary setup commands
  • not a replacement for services or launch sources
  • not the right home for Docker itself
  • not the right home for OS-native build-tool bundles

Current shipped slice

  • top-level toolchains, top-level orchestrators, and task-scoped requirements.toolchains are supported
  • Rust, Node, Java, Python, Go, Ruby, and .NET are the shipped toolchain names today
  • ota doctor diagnoses missing toolchains and orchestrators without mutation, and Ruby can govern Bundler through toolchains.ruby.package_managers.bundler
  • for toolchains.ruby with fulfillment.mode: run, ota currently uses the selected Ruby to hydrate the declared Bundler lane structurally instead of treating bundle install as shell glue
  • selected ota run / workflow ota up paths can provision shipped fulfillment sources plus fulfillment.source: mise when fulfillment.mode: run is declared and policy allows it
  • shipped orchestrator kinds are currently mise, devbox, and devenv; activation.trust is currently mise-only, and prepare.install is currently supported for mise and devbox
  • command-backed prepare.kind: dependency_hydration and prepare.kind: tool_bootstrap can use execution.orchestrator.mode: exec; mixed/native prepare.kind: sequence cannot in the current slice
  • duplicate ownership is invalid and fails validation
  • Ota's own contract now uses this model instead of shell-based managed-surface setup