Operate

Local Topology Patterns

How to adopt target bindings, target activation, shared local backends, and backend fulfillment without repo-local glue.

learnmaintainersadvancedstable2026-04-27

When to use this page

Use this page when the repo problem is not language setup alone, but how one local workload should truthfully target or share execution with another.

This is the adoption page for the shipped local-topology surface: first-class task target bindings, target activation, shared local backends, and backend-scoped run-path fulfillment.

  • use it when a helper app, probe, or SDK harness should target a repo-managed service by service identity instead of a guessed URL
  • use it when multiple long-running container tasks should intentionally share one ota-managed backend boundary
  • use it when the shared backend itself needs preparation on the ota run path and repo-local bootstrap glue would be the wrong fix

Pattern 1: Task target bindings

Start here when one task should follow another repo-managed app by service identity.

Keep the topology truth in tasks.<name>.targets.<target>. This is the shipped service-target-default surface; there is no separate default_from.service field. Only use task input as the explicit operator override channel.

  • choose address_view: host when the consumer may run in another execution plane and should target the producer's published host URL
  • use service.repo when that producer is owned by another repo in the same ota.workspace.yaml; keep service.member for monorepo member producers
  • keep override_input only when operators genuinely need to point the task at staging, preview, or another local target
  • use activation.mode: ensure_started when ota should hand producer startup off immediately, restart_ready when ota should bounce a reachable producer and verify it again, ensure_running when ota should make the declared listener reachable first, and ensure_ready only when ota should also wait for deeper declared readiness
  • today the workspace cross-repo slice is host-view only: service.repo requires address_view: host plus one fixed project.host endpoint on the producer listener
  • read activation evidence as plain language: started_started / reused_started mean startup handoff, started_running / reused_running mean listener reachability, and restarted_ready / started_ready / reused_ready mean deeper readiness
  • omit override_input when the task should just consume OTA_TARGET_<TARGET> directly
Target binding excerptyaml
tasks:  sandbox:    inputs:      base_url:        description: Override the API base URL when needed    targets:      api:        service:          task: dev          listener: http          address_view: host        override_input: base_url    run: pnpm dev:sandbox

Pattern 1b: Target activation

Use target activation when target resolution alone is not enough and ota should also ensure the local producer service is up before the consumer runs.

Keep activation separate from target identity. targets.<name> still declares what the consumer talks to; activation.mode only declares whether ota should make that producer ready first.

  • ship manual, ensure_started, restart_ready, ensure_running, and ensure_ready
  • when the producer service task declares runtime.readiness, ota waits for that readiness contract instead of treating an open listener socket as sufficient
  • explicit operator override inputs skip producer auto-start and preserve the override value
  • ensure_started hands startup off immediately, restart_ready restarts a currently reachable producer before verifying it again, ensure_running waits only for the declared target listener plane, and ensure_ready waits for declared runtime.readiness when one exists
  • the current shipped slice auto-starts only producer tasks ota can own honestly: persistent container service backends, unix native producer services, built-in remote producer services only when caller and producer share one declared remote backend binding, and backend-provider remote producer services only when the matching backend_provider extension declares activation.provider_managed_cleanup: true; built-in shared-remote address_view: host / address_view: topology / address_view: internal may probe the remote plane, and backend-provider remote activation now covers those same shared-remote views when ota can call provider-owned cleanup first; ensure_started hands startup off immediately, restart_ready can bounce a reachable producer before waiting again, ensure_running observes listener reachability, and ensure_ready may observe tcp or http readiness
  • stream-mode runs show an explicit activation wait phase while ota is starting or waiting on the producer readiness contract
  • if activation started the producer for this run, ota cleans it up on interrupt; reused producers are left running intentionally
  • unsupported producer shapes fail clearly instead of guessing orchestration
Activation excerptyaml
tasks:  sandbox:    inputs:      base_url:        description: Override the API base URL when needed    targets:      api:        service:          task: dev          listener: http          address_view: host        override_input: base_url        activation:          mode: ensure_ready    run: ./scripts/sandbox/start.sh

Pattern 2: Shared local backends

Use a shared backend when multiple long-running tasks should intentionally reuse one ota-owned backend boundary.

This is not depends_on, and it is not implied by matching contexts. The shared boundary must be declared explicitly.

A good concrete use case is a repo where dev runs the main API and sandbox runs a helper UI or automation surface, and both should live inside one persistent backend boundary that ota reuses and prepares on purpose.

Do not reach for a shared backend when tasks are just separate consumers of a published service. If they do not need one intentional shared boundary, target binding is the simpler and more honest model.

  • use it when ota should treat the backend boundary itself as the reusable unit, not just the individual task processes
  • bind each participating task with runtime.backend_binding
  • keep backend shape deterministic: same effective image, dependency-isolation shape, and memory shape
  • let workloads differ where they should: commands, listeners, readiness, and publications
  • expect one shared backend to give ota the right place to own reuse, readiness reachability, fulfillment, and internal/topology resolution
  • expect ota to reject real bind/publication conflicts instead of forcing every bound workload into one publication shape
  • use address_view: topology only when ota can prove the caller and producer share that declared backend boundary
Shared backend excerptyaml
execution:  shared_backends:    workbench:      scope: local      backend: container      lifecycle: persistent      context: app tasks:  dev:    runtime:      kind: service      backend_binding: workbench   dev:debug:    runtime:      kind: service      backend_binding: workbench

Pattern 2b: Shared remote backends

Use a shared remote backend when those long-running workloads intentionally live off-host and ota should still own one truthful shared boundary for reuse, targeting, and readiness.

This is the right model for a remote devbox, a Teleport-managed host, a pod-local workflow, or a Daytona workspace where producer and consumer are meant to stay co-located on the remote plane.

  • use scope: remote + backend: remote when the producer and consumer truly share one remote execution boundary instead of only talking to an arbitrary external URL
  • choose ssh for a normal machine you can already reach with SSH, tsh for a Teleport-managed SSH workflow, kubectl when the boundary is a pod you already operate through Kubernetes, and daytona when the boundary is a Daytona-managed workspace
  • important fields before examples:
  • remote.provider chooses the remote transport/provider
  • remote.target identifies the remote boundary
  • optional remote.cwd sets the remote working directory
  • run receipts and summaries keep remote backend identity explicit: ota records the remote provider, the resolved target, and the declared cwd when one exists
  • optional remote.ssh.config_file / remote.ssh.identity_file are SSH-only pass-through hints
  • for provider: ssh, start with the default contract shape and let OpenSSH use normal ~/.ssh/config, SSH agent/default identity selection, and host aliases; add remote.ssh.config_file or remote.ssh.identity_file only when the repo must force one explicit config or key path
  • use activation.mode: ensure_started when ota should hand remote producer startup off immediately, restart_ready when it should bounce a reachable remote producer and verify it again, ensure_running when it should make the remote listener reachable first, or ensure_ready when it should also wait for deeper declared readiness; today that remote auto-start slice is split cleanly: built-in providers can use address_view: host with one fixed project.host endpoint or shared-remote address_view: topology / address_view: internal with remote-plane probes, and backend-provider remote activation now covers those same shared-remote host / topology / internal views when the matching extension declares activation.provider_managed_cleanup: true so ota can issue provider-owned cleanup before restart
  • test the provider outside ota first, then declare one fixed listener endpoint and a shared backend binding before relying on activation
  • do not use a shared remote backend when you only need to point a task at some existing staging URL; plain override input is the more honest model there

Start here when producer and consumer intentionally share one remote execution boundary and the consumer should target the producer by service identity.

Minimal shared remote backend excerptyaml
execution:  default_context: remote_app  contexts:    remote_app:      backend: remote      lifecycle: persistent      remote:        provider: ssh        target: user@devbox        cwd: /workspace/app  shared_backends:    workbench:      scope: remote      backend: remote      lifecycle: persistent      context: remote_app tasks:  dev:    run: pnpm dev:api    runtime:      kind: service      backend_binding: workbench      readiness:        kind: tcp        listener: http      listeners:        http:          protocol: http          bind:            address: 127.0.0.1            port:              mode: fixed              value: 8080   sandbox:    targets:      api:        service:          task: dev          listener: http          address_view: internal        activation:          mode: ensure_ready    run: pnpm dev:sandbox

Prefer the first shape and let normal OpenSSH behavior choose config, keys, and host aliases. Only force remote.ssh when the repo truly needs one explicit config or key path.

SSH provider: default vs explicit overrideyaml
# Prefer this first:execution:  contexts:    remote_app:      backend: remote      remote:        provider: ssh        target: user@devbox        cwd: /workspace/app # Only force this when the repo truly needs it:execution:  contexts:    remote_app:      backend: remote      remote:        provider: ssh        target: user@devbox        cwd: /workspace/app        ssh:          config_file: ~/.ssh/work.conf          identity_file: ~/.ssh/work_rsa
How to test it truthfullytext
1. Prove the provider works outside ota first.   - ssh user@devbox   - tsh ssh user@host   - kubectl exec pod/ota-dev -- true2. Keep the producer listener fixed.   - bind.port.mode: fixed   - bind.port.value: 80803. Use tcp readiness for the shipped remote activation slice.4. Bind producer and consumer to the same execution.shared_backends.<name>.5. Run the consumer task and verify:   - ota starts or reuses the producer   - readiness is observed on the remote plane   - interrupt cleanup only stops producer services ota started for that run

Pattern 3: Run-path fulfillment

Turn this on when ota should make the declared execution environment real on the run path instead of asking the repo to bootstrap missing tools itself.

Use the shared-backend form when multiple tasks intentionally share one backend boundary, or the direct context form when one container context should be prepared inside the actual execution container before the task body runs.

  • requirements declare what the environment needs; fulfillment declares whether ota should try to make that true on the run path
  • use fulfillment: none when the contract should fail clearly if the backend is missing prerequisites
  • use fulfillment: run when ota should prepare the backend on the actual run path
  • fulfillment: run still depends on org policy for approved sources and versions; it is opt-in runtime intent, not a policy bypass
  • if org policy enables strict_versions, ota also treats repo-satisfying but policy-noncompliant installed versions as gaps that fulfillment: run may repair with an approved exact version
  • for direct container contexts, fulfillment: run now prepares the real ephemeral execution container before the task body instead of provisioning a separate throwaway backend
  • read receipt/result vocabulary in plain language: requirements_satisfied means nothing had to be installed, fulfilled means ota provisioned successfully, missing_requirements means fulfillment was not allowed/selected, and failed means ota tried to prepare the backend but did not complete successfully
Shared backend excerptyaml
execution:  shared_backends:    workbench:      scope: local      backend: container      lifecycle: persistent      context: app      fulfillment: run
Direct context excerptyaml
execution:  contexts:    tooling:      backend: container      lifecycle: ephemeral      fulfillment: run      container:        image: node:24-bookworm

Pattern 4: Policy-governed backend environment

Use this when the repo should declare backend environment intent and let policy resolve the effective image honestly.

This keeps shared-backend shape and fulfillment attached to one approved profile or alias instead of hardcoding raw images everywhere.

  • use environment.profile for a named approved backend profile
  • use environment.image_alias when policy should map one repo-facing alias to the approved image
  • use literal environment.image only for compatibility or intentionally ungoverned local cases
  • use environment: {} when policy default_profile should choose the effective backend image
  • read receipts and ota execution plan to compare declared intent versus the effective image/source/registry ota actually selected
Environment intent excerptyaml
execution:  shared_backends:    workbench:      scope: local      backend: container      lifecycle: persistent      context: app      fulfillment: run      environment:        profile: java-node-workbench

Current constraints

The current design is the right long-term foundation, but the shipped surface is intentionally strict where ota cannot yet prove more.

  • shared backends currently ship as local container, local native, and remote remote
  • bound tasks must resolve one deterministic backend shape, but workload-local listeners/readiness/publications may differ
  • ota rejects conflicting in-backend bind endpoints and conflicting fixed host publications inside one shared backend boundary
  • address_view: topology for container callers resolves only when ota can prove shared locality through the declared backend binding
  • direct-context run-path fulfillment currently applies to container contexts only; native and remote contexts must not declare fulfillment
  • ephemeral direct container contexts support non-service task execution today; service-runtime fulfillment in that shape is still rejected
  • backend fulfillment currently acts on the effective shared backend requirement union for container, native, and remote shared backends

Canonical examples

Use these examples as the adoption baseline instead of rebuilding the patterns from scratch in each repo.