Design direction

Execution Topology

The evolving execution-topology model for tasks, services, networks, readiness, and dependency isolation in ota.

referenceautomation buildersintermediatestable2026-05-30

Current problem

Use this page when the repo has more than one execution plane and the current backend model stops being honest.

The current repo-wide backend choice is too coarse for repos where host orchestration and container workloads both matter.

  • ota knows where tasks run, but not where services are controlled from
  • ota does not yet model where services are reachable from
  • container-backed repos can accidentally reuse host-native dependency trees like node_modules, which leaks platform-specific binaries into the wrong runtime
  • repo-wide requirements flatten host control-plane tools and workload-plane tools together
  • container workloads and host-managed services can therefore describe a truthful repo and still fail as a topology

The proposed model

  • execution contexts define where workloads run
  • execution-context support can be scoped explicitly with only_on and only_arch so unsupported hosts block early and honestly
  • typed service managers define how services are controlled
  • task-scoped workload listeners define how long-running app processes publish host-visible endpoints
  • context-scoped endpoints define how services are reached from each context
  • execution-context attachments can keep platform-sensitive dependency paths isolated from the host tree while the source checkout remains bind-mounted
  • context-scoped readiness defines where probes run from
  • context-scoped requirements keep host and workload dependencies separate

Execution contexts

Contexts are the core boundary. They replace the one-flat-backend story with named workload planes.

Requirements declared on a context are inherited by every task that runs there, so keep the context narrow enough that those requirements stay truthful for all bound tasks.

execution.contextsyaml
execution:  default_context: app  contexts:    host:      backend: native      requirements:        tools:          docker: "*"          lsof: "*"    host-dev:      extends: host      requirements:        runtimes:          python: ">=3.12,<3.14"          node: "^22.12.0"        tools:          poetry: ">=1.8"          npm: ">=10.5.0"    app:      backend: container      lifecycle: persistent      container:        image: maven:3.9.14-eclipse-temurin-21-noble      attachments:        compose:          - local      requirements:        runtimes:          java: ">=21"          node: ">=24.14.1"        tools:          maven: "*"
split a broad host contextyaml
tasks:  setup:config-basic:    context: host    run: make setup-config-basic   build:    context: host-dev    run: make build   test:frontend:    context: host-dev    run: make test

Tasks, workloads, and services

Tasks bind to contexts. Services use typed managers. Workload listeners keep app ingress on the task and publish host-visible endpoints without overloading dependency services.

tasks + workloads + servicesyaml
tasks:  compose:up:    context: host    run: docker compose up -d postgres  setup:    internal: true    context: app    run: mvn -q -DskipTests dependency:go-offline  db:integration:    context: app    requires_services:      - postgres    run: mvn -B -Dgroups=db-integration test  dev:    context: app    requires_services:      - postgres    run: pnpm dev    runtime:      kind: service      listeners:        http:          protocol: http          bind:            address: 0.0.0.0            port:              mode: fixed              value: 3000          project:            host:              address: 127.0.0.1              port:                mode: auto              path: / services:  postgres:    manager:      kind: compose      name: local      file: compose.yaml      service: postgres    endpoints:      host:        address: 127.0.0.1        port: 5432      app:        address: postgres        port: 5432    readiness:      from: app      run: pg_isready -h postgres -p 5432

Container-local dependency isolation

Use this when source must stay bind-mounted but platform-sensitive install artifacts should live in Ota-managed container storage instead of the host checkout.

  • use attachments.isolated_paths for dependency trees like node_modules when native binaries need to match the container platform
  • the repo source stays bind-mounted, but Ota overlays engine-managed named volumes for isolated paths so host and container artifacts stop colliding
  • run install and dev tasks against the same isolated path so later container runs reuse the right platform-specific artifacts
  • ota clean removes current and drifted Ota-managed isolated dependency volumes for contexts that declare attachments.isolated_paths, including ephemeral contexts
  • cleanup ownership is label-backed (dev.ota.managed, dependency-isolation kind labels, and repo token) so repos sharing one project.name do not delete each other's state
  • when ownership cannot be proven, ota reports managed state as skipped instead of deleting it unsafely
Isolated dependency pathsyaml
execution:  default_context: app  contexts:    app:      backend: container      lifecycle: persistent      container:        image: node:24-bookworm      attachments:        isolated_paths:          - node_modules      requirements:        runtimes:          node: ">=24.14.1"        tools:          pnpm: "*"tasks:  setup:    internal: true    context: app    run: pnpm install  dev:    context: app    run: pnpm dev

Listener mode rules

  • use listener shorthand for common local HTTP or TCP listeners when the bind and projected host endpoint are the same fixed loopback port
  • use the full listener form when you need custom bind address, container host publication, auto host ports, primary listener selection, non-default paths, or multiple projected endpoints
  • bind.port.mode: fixed means the workload must listen on one explicit internal port; use it for containerized apps and any workload whose ingress should stay stable
  • bind.port.mode: discover means ota discovers the final listening port after a native task starts; use it for host-native dev servers that may auto-bump to a free port
  • project.host.port.mode: fixed means the published host URL should stay on one explicit port; use it when you want a stable bookmark, proxy target, or docs link
  • when project.host.port.mode: fixed is selected on the projected primary listener, operators can pick a one-run public URL with ota run <task> --host-port <port> without changing the internal bind port
  • execution.contexts.<name>.container.resources.memory.minimum/default keeps container memory truth on the execution context; use ota run <task> --memory <size> for one-run container overrides
  • --memory is container-only and is rejected when requested below a declared resources.memory.minimum
  • project.host.port.mode: auto means ota injects runtime URL env values before process start and reports the same resolved URL in receipts and summaries; ephemeral container runs pre-reserve a free host port, while persistent runs resolve the currently published host mapping
  • if more than one listener is projected to host, mark exactly one projected listener as project.host.primary: true so ota can choose one deterministic primary URL
  • native tasks may use fixed or discover, but discover must not be paired with a fixed host projection
  • container tasks with project.host must use a fixed internal bind port; only the host-side port may be auto
  • loopback-only container binds such as 127.0.0.1 or localhost are invalid when the listener is projected back to host

Listener shorthand

Use listener shorthand for the common local case where a task exposes one fixed HTTP or TCP port on loopback and the host-visible endpoint uses the same port.

The shorthand is authoring sugar. Ota normalizes it into the same listener model used by the full protocol, bind, and project.host form.

  • do not mix shorthand keys with verbose listener fields on the same listener
  • do not define both http and tcp on one listener
  • switch to the full listener form as soon as bind and host projection differ
  • switch to the Surfaces guide when the same endpoint is reused across multiple tasks, workflow readiness, or workflow exposes

Common local HTTP listener

Use this when a dev server, app server, or docs preview exposes one fixed local HTTP port and the browser should open the same loopback port.

web.http: 3000 means listener web, protocol http, fixed bind port 3000, fixed host projection 127.0.0.1:3000, and host path /.

HTTP shorthandyaml
tasks:  dev:    run: npm run dev    runtime:      kind: service      readiness:        kind: http        listener: web        path: /      listeners:        web:          http: 3000

Common local TCP listener

Use this when an app exposes a raw TCP listener and an open socket is the right readiness truth.

redis.tcp: 6379 means listener redis, protocol tcp, fixed bind port 6379, and fixed host projection 127.0.0.1:6379 with no HTTP path.

TCP shorthandyaml
tasks:  redis:    run: redis-server --port 6379    runtime:      kind: service      readiness:        kind: tcp        listener: redis      listeners:        redis:          tcp: 6379

Full HTTP listener form

This is the full shape Ota normalizes the HTTP shorthand into.

Use the expanded form directly when the task needs custom bind address, container host publication, auto host ports, primary listener selection, or a non-default path.

Expanded listener formyaml
listeners:  web:    protocol: http    bind:      address: 127.0.0.1      port:        mode: fixed        value: 3000    project:      host:        address: 127.0.0.1        port:          mode: fixed          value: 3000        path: /

Workload endpoint projection contract

Use this when one long-running task needs one truthful host URL without making developers guess host ports.

  • keep the app command simple (run: pnpm dev) and let ota own host projection
  • fixed bind means the app always listens on one internal port inside the container
  • auto host projection means ota picks a free host port so stale containers and port collisions do not force manual edits
  • if the primary projected listener uses project.host.port.mode: fixed, ota run <task> --host-port <port> can override only the published host/public port for one run
  • with multiple projected listeners, project.host.primary: true declares the one listener that controls OTA_PUBLIC_URL and summary endpoint rendering
  • ota exports OTA_PUBLIC_URL, OTA_PUBLIC_HOST, OTA_PUBLIC_PORT, and OTA_PUBLIC_URL_<LISTENER> before task start when the projection is known
  • use OTA_PUBLIC_URL when the app needs one canonical public URL, and use OTA_PUBLIC_URL_<LISTENER> for secondary listener-specific links
Task-owned listener projectionyaml
tasks:  dev:    context: app    run: pnpm dev    runtime:      kind: service      listeners:        http:          protocol: http          bind:            address: 0.0.0.0            port:              mode: fixed              value: 3000          project:            host:              address: 127.0.0.1              primary: true              port:                mode: auto              path: /        metrics:          protocol: http          bind:            address: 0.0.0.0            port:              mode: fixed              value: 9090          project:            host:              address: 127.0.0.1              port:                mode: auto              path: /metrics

Clone, run, open flow

  • ota owns host-port allocation and prints one reachable URL
  • ota keeps container memory requests explicit at execution-context scope and lets operators override per run when needed
  • the URL in terminal output, receipts, and JSON stays aligned with what was injected into the task env
  • ota run <task> --log writes durable task stdout/stderr artifacts under .ota/state/logs/; ota still keeps its own interpretation in the run summary and receipt, which also report that log location
  • readiness remains separate: service checks answer is postgres up?, listener projection answers how do I reach the app?
No manual host-port pickingbash
git clone <repo>cd <repo>ota run dev# optional one-run fixed host override (for fixed projected listeners)ota run dev --host-port 4000# optional one-run container memory overrideota run dev --memory 4GiB# optional durable stdout/stderr capture for postmortem debuggingota run dev --log# open the URL ota prints (same value as OTA_PUBLIC_URL)

Ingress troubleshooting

  • declares multiple projected listeners ... but none sets project.host.primary: true: mark exactly one projected listener as primary
  • declares multiple listeners with project.host.primary: true: keep one primary listener and remove the rest
  • cannot project to the host from a loopback-only container bind address: use bind.address: 0.0.0.0 for projected container listeners
  • could not publish host port: keep project.host.port.mode: auto, stop stale containers occupying the same address/port, and rerun
  • --host-port rejected: use it only for projected listeners with project.host.port.mode: fixed; keep auto listeners on automatic reservation

Two concrete topologies

What changes in doctor, up, and run

Use requires_services when a task should own its service prerequisites instead of depending on a manual ota up reminder.

ota keeps service ownership in services, starts what is needed before the task body runs, and keeps workload ingress with the task that owns the app process.

  • ota doctor validates context requirements, manager availability, endpoint projection, and readiness from the declared context
  • ota doctor rejects impossible workload listener projection plans such as loopback-only container binds with host publication
  • ota doctor, ota explain, and ota receipt now fail clearly when multi-listener projection omits a primary listener or marks more than one listener as primary
  • when a declared readiness context cannot actually execute, ota doctor now reports that as an explicit topology blocker instead of pretending the service itself simply failed readiness
  • when a container context declares attachments.isolated_paths, ota doctor and ota explain surface the isolated dependency paths instead of hiding them behind generic container execution notes
  • when a repo depends on remote execution contexts, native doctor mode reports local checks as a partial view instead of treating host checks as remote truth
  • ota doctor --mode remote probes runtime/tool versions, policy-backed provisioning installability, and approved org-policy version surfaces through executable remote contexts
  • ota up starts required services from the control plane, ensures attachments are valid, runs setup in its task context, then re-checks readiness from the correct context
  • ota up only reports workload endpoints for runtime-bearing tasks it actually executes during preparation today, so it stays honest about which app paths were really started
  • tasks.<name>.requires_services lets a task request canonical services before its body runs, so ota run db:integration can bring up postgres and verify it automatically without turning the service into a task dependency
  • if setup resolves to a remote context, ota up now blocks explicitly instead of silently falling back to native diagnosis and pretending the host view was authoritative
  • ota run resolves the task context first, attaches workload containers to declared service topology when required, injects resolved runtime URL env values before spawn when available, and records the same resolved workload endpoint for receipts/JSON
  • ota run resolves container memory from context resources (minimum/default) and applies --memory overrides directly to engine invocation for ephemeral and reconciled persistent runs
  • ota run <task> --log preserves durable task stdout/stderr artifacts under .ota/state/logs/ so failed or cleaned-up runs still leave postmortem evidence, while ota's own interpretation stays in receipt text, run summary output, and receipt JSON alongside the same log path
  • ota run now supports one-task mode-aware execution: tasks.<name>.execution.default_mode picks the default execution plane and ota run <task> --mode <mode> selects any matching mode branch for context, lifecycle, env, command body, and runtime listeners
  • when a task has no matching mode branch, ota run falls back to the task-level execution body and task-level execution settings instead of requiring redundant empty branches
  • for container contexts with attachments.isolated_paths, ota run and ota up mount Ota-managed dependency-isolation named volumes before process start so platform-specific artifacts stay out of the host tree
  • for container project.host.port.mode: auto, ephemeral runs verify the engine published the reserved host port and retry bounded times on host-port conflicts before failing clearly, while persistent runs verify the current published mapping
  • receipts and JSON output should report the same resolved topology truth instead of one generic repo backend

Status and migration

  • the execution-context foundation is shipped: execution.default_context, execution.contexts, contexts.<name>.requirements, tasks.<name>.context, and tasks.<name>.requires_services are accepted by the contract and now drive ota run, ota up, and context-aware readiness diagnosis
  • mode-aware task execution is shipped: tasks.<name>.execution.default_mode and tasks.<name>.execution.modes.<mode> let one task intent support native/container/remote execution shapes without duplicating task names
  • container dependency isolation is shipped: execution.contexts.<name>.attachments.isolated_paths now mounts Ota-managed, engine-owned named volumes for platform-sensitive paths like node_modules in container contexts
  • task-scoped workload listeners are shipped: tasks.<name>.runtime.kind: service plus named listeners now drive runtime endpoint resolution in ota run, receipts, and the tasks ota up actually executes during preparation
  • typed compose and host service managers are shipped for explicit control-plane ownership instead of legacy inferred service surfaces
  • context-scoped services.<name>.endpoints and services.<name>.readiness.from are now shipped and used by ota doctor plus ota services
  • container contexts with attachments.compose now attach workload containers to the derived Compose network during ota run and ota up task execution
  • current execution.preferred and services.provider/start/stop/healthcheck stay as legacy compatibility mode
  • legacy service fields remain host-bound unless upgraded to typed topology-aware service blocks
  • broader topology work is still in progress, including richer manager kinds beyond compose/host plus deeper per-context remote policy reporting and broader remote topology coverage