Launchfile Provider Contract
Status: draft for review.
SPEC.mddefines the file; this document defines what a provider does with it — the runtime counterpart. It consolidates ratified decisions D-37 (execution mode vs. environment), D-38 (install/dev+source), D-39 ($storage.*), and D-40 (the app/provider build line). Parts marked 📐 are not-yet-ratified elaborations — the cross-invocation state/event model (a design note) and--deps-only. Parts marked ✅ are implemented in the reference providers today.
A provider translates a Launchfile into a running (or described) deployment on one target — Docker Compose locally, native services on macOS, Terraform for AWS, Kubernetes, etc. The format captures intent; the provider maps intent to execution (P-1, P-5, P-11). This contract is what keeps “same file, every provider” (12-Factor X) honest.
Status legend: ✅ implemented in a reference provider · 🔶 specified, partially implemented · 📐 proposed (pending RFC / design note).
1. Relationship to the other specs
| Doc | Defines |
|---|---|
SPEC.md |
The file contract — fields, types, expressions. |
DESIGN.md |
Format design decisions (D-*), principles (P-*), limitations (L-*). |
PROVIDERS.md |
The runtime contract — verbs, lifecycle, modes, selection, build line, state. |
CLI-ROADMAP.md |
The reference CLI surface (launch <verb> [target] [flags]). |
The provider contract is normative for anyone implementing a provider, whether or not they use the reference @launchfile/sdk.
2. Verbs — the operational surface
A provider exposes a subset of these operations. up/down/status are the minimum for a runtime provider; a translation-only provider (e.g. AWS → Terraform) may implement only translate.
| Verb | Purpose | Status |
|---|---|---|
up |
Provision resources, prepare, and run the app (or a selected subset). | ✅ docker, macos-dev |
down |
Stop and (with --destroy) remove the deployment. |
✅ docker, macos-dev |
status |
Report what is running for a deployment. | ✅ docker, macos-dev |
logs |
Stream/print component logs. | ✅ docker |
env |
Print a component’s resolved environment as export K=V. |
✅ macos-dev |
list |
List known deployments. | ✅ docker |
translate |
Emit target artifacts without deploying (IaC, manifests). | 📐 (AWS probe) |
Identity & re-location: a provider keys each deployment by a stable id (slug / directory / content hash) so the CLI can re-find it later (see §8).
3. Lifecycle slots (D-37)
A provider operates on slots, not raw command names. A slot is a lifecycle phase; which command fills it is a mode-resolution detail (§4). Two slots are mode-aware; the rest are invariant.
| Slot | Purpose | Command by mode | When |
|---|---|---|---|
| prepare | make the app runnable (deps, compile, package) | artifact build · source install |
on change / on demand |
| release | one-off tasks before serving (migrations) | release |
per deploy |
| run | the long-running process | artifact start · source dev |
every launch |
| bootstrap | post-run setup against the running app | bootstrap |
on demand after run |
| seed / test / … | ad-hoc | seed / test / custom |
on demand |
Providers SHOULD surface progress on slot boundaries (prepare.start/end, run.healthy, …) — see §8. prepare MUST run on demand / on input change, not on every run.
12-Factor V (build/release/run):
preparegeneralizes factor V’s build (the source command is an install, not a build).prepareis the slot;buildis the artifact-mode command that fills it.
4. Execution modes (D-37, D-38)
A provider runs in exactly one mode per launch — artifact (built image/platform build) or source (run from the working tree). Mode is requested globally and resolved per component.
Run resolution, source mode, per component:
devpresent → rundevfrom source (in the component’ssource:dir).- else
imagepresent → run as artifact (the image is the only runnable form). - else → run
startfrom source (prepare’sbuildproduces any outputstartneeds).
Prepare (for a component resolved to source): install if present, else build.
Field partition — exactly one of the first two sets is active per component per launch:
| Set | Members | Active in |
|---|---|---|
| Artifact | image, build, start |
artifact mode |
| Source | source, install, dev |
source mode |
| Invariant | provides, requires, depends_on, health, storage, env, release, bootstrap, seed, test |
always |
Resources (requires) have no source form — provisioned identically in both modes. A cloud/artifact-only provider MUST ignore source-mode fields.
5. Component selection & --deps-only
up/down/status/dev accept an optional component selector (verb argument, not a file field — D-37).
- Selecting a component starts it plus its transitive downward dependency closure — its
depends_ontarget components and every closure member’srequiresbacking services (D-41). Selecting nothing acts on all components. ✅ (docker, macos-dev —selectComponents()/selectionClosure()in the SDK) - The closure is downward only:
up backendnever startsfrontend(a reverse-dependency) or unrelated components, and already-running dependencies are left untouched (idempotent).depends_onis honored as a hard prerequisite (D-16), so a selected component’sdepends_ontargets come along — they are not left down for the operator to satisfy. A future--no-depsopt-out starts only the directly-named components. --deps-only[=requires|supports]📐 — provision the resource closure of the selected (or all) components and start no component. It never traversesdepends_on.requires= mandatory;supports= optional (L-6, orchestrator-activated). A backing service modeled as a component (not arequires) is not picked up — select it explicitly.
6. Build: portable contract vs. provider specialization (D-40)
- Portable contract (every provider MUST be able to build from):
runtime+commands(build/install/start/dev/…). - Provider specialization (fenced): in-repo recipes a provider discovers —
build.dockerfile/target/args(OCI family),nixpacks.toml,Procfile, etc. Rules: discovered, not enumerated; never the sole build path (a provider that understands none of an app’s specializations MUST still build it from the contract); ignored safely (unknown recipe → fall back to contract, never error).build.dockerfile/target/argsare reclassified as OCI-family hints — never removed. No generalx-<provider>:block is admitted.
A specialization makes a matching provider more faithful; it never makes the app deployable only on that provider.
Reduced-portability diagnostic (D-40): a validate-only, non-fatal, suppressible warning that fires when an app’s only build path is a provider-specific recipe (a Dockerfile) or a prebuilt image: with no portable runtime/commands contract. It is never emitted by operational commands (up/down/logs/…), so the image-first catalog is unaffected in normal flows — only an explicit validate surfaces it. A second case (D-43): a source-needing app with no reachable origin — no image:, evaluated detached, and no repository: to fall back to. Same treatment: validate-only, non-fatal, suppressible.
Source acquisition (D-43)
Any build or source-mode operation needs the app’s source tree. Every provider resolves the tree through this precedence — a local provider trivially lands on rule 2, the checkout the Launchfile sits in:
- Orchestrator-supplied source. A source the orchestrator hands the provider — a repo URL + ref, a tarball, or a local tree — MUST take precedence over everything below. This is the home-#2 channel (D-36): forks, private mirrors, and per-environment ref choices (
mainto staging, a tag to prod) all arrive here, never in the file. - Attached context. A Launchfile is attached iff it was read from within the app’s own source tree — the checkout of the app it describes. That tree is the source; a remote provider MAY package and ship it as its build context. A file fetched standalone (a catalog entry, a URL) into a cache or temporary directory is detached, even though it momentarily sits in some directory.
- Detached fallback —
repository:. For a detached file, therepositoryfield (SPEC.md § Repository) is the canonical origin the provider MAY fetch, at the ref named by its#fragment (default: the repository’s default branch). Fetching and building a remote origin falls under the per-source confirmation rules (SPEC.md § Execution modes and source trust).
The in-file origin — URL and fragment alike — is a baseline default, never a lock: rule 1 always wins when the orchestrator supplies a source. A provider MUST NOT treat the fragment as pinning a deployment to that ref.
When no step resolves a source — detached, nothing supplied, no repository: — the operation fails with a clear error naming the missing origin; the D-43 validate diagnostic exists to surface exactly this case ahead of time.
A translation-only provider (§2) cannot ship a tree or perform a fetch; it MUST instead record the origin it would acquire (the resolved URL + ref, or “orchestrator-supplied” / “attached tree”) in its emitted artifacts or conformance report, so the acquisition step is explicit rather than silently out of band.
7. Expression resolution — provider-supplied values ✅
The file declares intent; the provider supplies values (P-11). A provider MUST resolve the $-expressions in env values and set_env values into concrete strings before the app sees them — a contract ratified and implemented today (D-33/D-35/D-36/D-39), covering every namespace in the table below. Whether that obligation also extends to command strings is contested and unresolved. This section previously asserted that it does; SPEC.md § Command interpretation and §10 rule 11 below hand the command string to a POSIX shell, which owns $ too. Neither passage has been withdrawn, and this section takes neither as the rule until #205 settles it — see Command strings below before writing one.
The three homes (D-36). Every value in a Launchfile has exactly one home. The provider owns home #3 — values it computes from its own routing, storage, provisioning, and PATH strategy. The app names the need; the provider resolves the value, the same expression yielding a different concrete string per provider. (Home #1 is the app’s command/intent; home #2 is per-environment config the orchestrator supplies — neither is the provider’s to invent.)
Resolution order. Reserved namespaces are matched before any user-named resource, so a resource or volume named app/storage cannot shadow them. An unknown reserved key resolves to the empty string (L-4), so a provider that doesn’t supply a given value degrades gracefully (P-13) rather than erroring.
| Expression | Home-#3 value the provider supplies | Source |
|---|---|---|
$app.* — url, host, port, name, authority, scheme, tls |
the app’s own public address, computed from the provider’s routing strategy | D-33, D-35 |
$secrets.<name> |
an app-wide generated secret | D-18 |
$components.<name>.* |
a sibling component’s endpoint, resolved by consumer vantage (§8) | — |
$storage.<name>.path |
the filesystem path the provider provisioned for the named volume | D-39 |
$<resource>.<prop> / enclosing $url, $host, … |
a provisioned resource’s connection properties | D-7 |
Command strings — unsettled, and divergent in practice. Whether a $-expression inside a commands.* string is a Launchfile reference at all is not decided. Two ratified passages point opposite ways: SPEC.md § Expression Syntax scopes the $ system to set_env values and env defaults, while SPEC.md § Command interpretation and §10 rule 11 below hand the command string to a POSIX shell — which owns $ too, and for which $VAR and ${VAR:-default} are its own syntax. Nothing resolves the overlap, so the reference providers each answer it differently:
| Slot | @launchfile/docker |
@launchfile/macos-dev |
@launchfile/aws |
|---|---|---|---|
bootstrap |
resolves $app.* and $secrets.* only |
resolves $app.*, $secrets.*, $components.*, resource properties |
not implemented |
release |
resolves $app.* and $secrets.* only |
no resolution — raw string to the shell | no resolution — raw into cloud-init |
build / install |
not implemented (source builds in the image build) | no resolution — raw string to the shell | no resolution — raw into cloud-init |
dev |
not implemented (source-mode run slot D-38 lets an artifact-executing provider ignore) | no resolution — raw to the process manager | not implemented (recorded as an explicit D-38 source-mode ignore) |
start |
no resolution — raw into the compose command: |
no resolution — raw to the process manager | no resolution — raw into the systemd unit |
$storage.* is populated for no command string, on any slot, on either deploying reference provider. Where resolution does run it covers the whole string, so an unresolvable reference — including a shell variable the resolver does not recognize — becomes "" — the same silent-empty L-4 defines for an unknown property, though L-4 does not sanction it for a namespace the provider never populated — rather than reaching the shell.
What to do meanwhile. Nothing here licenses $ in a command: the same expression resolves on one provider and slot and vanishes on another, which is exactly the P-5 breakage this document exists to prevent. There is also no portable workaround. Routing the value through an env: variable and reading it back as a shell variable fails in both directions. On the slots that resolve (docker bootstrap, docker release, macos-dev bootstrap) resolution covers the whole string and blanks the $VAR too — catalog/apps/paperclip’s bootstrap is destroyed on @launchfile/docker today for precisely this reason. On the slots that do not resolve, the variable arrives only if that provider puts it in that slot’s environment, and not all do: @launchfile/aws publishes env/set_env to SSM Parameter Store and sources nothing in the cloud-init that runs build and release, where an unset $VAR aborts the script under set -euxo pipefail. Until #205 lands, read the table for the provider and slot you actually target, verify the variable actually arrives there, and expect neither $ form to be portable.
Tracked, not promised. Settling this — and reconciling the four passages that disagree — is #205. The table above records the present state as fact, not as a target; it is not a decision, and a new provider should implement to whatever #205 lands, not to the divergence described here.
Storage paths (D-39) — the home-#3 obligation made concrete. The declared storage.<name>.path is the canonical / container path. A provider that provisions a volume MUST resolve $storage.<name>.path to the path it actually used and inject it wherever the app references it, so the path never has to appear in a command:
- a container provider bind-mounts the volume at the declared path →
$storage.<name>.path= that path (e.g./data/cache); - a native provider provisions a host directory →
$storage.<name>.path= that directory (e.g..launchfile/storage/<component>/cache).
The same Launchfile is therefore correct under both, and an author never hardcodes a path only one provider understands — the exact failure D-36/D-39 close. A provider that does not provision storage leaves $storage.* unresolved (→ "").
Why resolution is the provider’s job, not the file’s: a path, URL, or secret that varies by provider is home #3 — if the app embedded it, the file would stop being portable (P-1, P-5). Resolution is the mechanism that keeps “same file, every provider” honest, and is the concrete enforcement point for the D-36 litmus.
8. Deployment state & the event model 📐 (cross-invocation state design note)
Providers persist deployment state so status/env/down work across shells, and so separate invocations can compose one app (launch up backend && launch dev frontend) by sharing the runtime-resolved values (actual ports, generated secrets, captures) that env inheritance cannot carry sibling-to-sibling.
Model: event-sourced state, the file as the shared projection.
runtime → [events] → reduce() → atomic write ─┐
├─ state file (LAUNCHFILE_STATE)
local watcher ← emit ← diff() ← fs change ────┘
SDK (pure, no I/O) provides the vocabulary and folds — LaunchEvent, DeploymentState, reduce, diff, resolveRef(state, ref, vantage). The provider/orchestrator owns the I/O: atomic write (temp + rename(2), flock(2)), fs.watch, terminal rendering, and deployment-id resolution.
- Resolution by vantage:
resolveRefpicks an endpoint’spublishedvsinternaladdress from the consumer’s vantage (host-native →localhost:3001; in-network →backend:3000). Endpoints therefore carry both. - One stream, three surfaces: persistence (the rendezvous file), terminal/UI statuses, and reactivity (
depends_ongates, supervisors, dev reload). - Deployment id (so
&&is one deployment, not two):--state <path>/--name <id>›LAUNCHFILE_STATEenv › implicit app+dir. launch envreads this state and emits resolved, vantage-awareexport K=V—eval "$(launch env backend)".
Today each reference provider persists its own state shape (see §9); the unified
DeploymentState+ event model is the proposed standardization.
9. Reference providers — implemented today ✅
@launchfile/docker — artifact / container
- Verbs:
up,down,status,logs,list. upopts:detach,dryRun,yes. Returns:slug,appName,sourceType(local | catalog | url),sourcePath/sourceUrl.- Translation: Launchfile →
docker-compose.yml(compose-generator); one compose project per deployment, keyed byslug. - Ports: host-port allocation, persisted and collision-avoided across deployments (UC3 worktrees get distinct ports).
- Build: components with
build:are built from source insidedocker compose build(BuildKit — nothing from the repo runs on the host);image:services are pulled. - Flow: build (from source) → release (one-shot
docker compose run --rmper declaring component, independs_onorder; compose brings that component’s backing resources up healthy first, and a non-zero exit fails the deploy) → start (compose up) → bootstrap. - Sources: local path, catalog slug, remote URL (with a confirmation prompt for remote, bypassable via
yes). - Storage: resolves
$storage.<name>.pathto the bind-mounted container path (D-39). - State:
DockerStateper slug (compose project/path, allocated ports, source info) under the provider state dir. - Selection: honors the component selector; the post-
upsummary reports only the started subset.
@launchfile/macos-dev — source / native
- Verbs:
up,down,status,env. upopts:withOptional,noBuild,detach,dryRun,projectDir.- Resources (native, via Homebrew services):
postgres,mysql,redis,sqlite. - Runtimes:
bun,node,python,ruby. - Prepare-on-change:
lockfile-detectdecides when to (re)install —prepareis not re-run on everyup. - Process management: components are spawned detached;
pid/pgid/startedAt/commandare recorded sodownfrom another shell can signal the whole group, guarded against pid reuse. - Also: health checks, secret generation, persistent storage, env writing.
env: prints a component’s resolved environment (§7) — the read surface §8 generalizes.- Storage: resolves
$storage.<name>.pathto.launchfile/storage/<component>/<name>on the host (D-39). - State:
LaunchStateat<projectDir>/.launchfile/state.json, keyed by Launchfile content hash; holdsresources,secrets,ports,processes. - Selection: narrows
componentsto the selected set’s downwarddepends_onclosure (selectionClosure) after the prereq gate, so every phase honors it.
Mode coverage today
The docker provider is effectively artifact-first (it builds/pulls images); macos-dev is source/native-first. The explicit source/artifact mode taxonomy (§4) formalizes what these two already do in practice and is the bridge to a third, non-local provider.
10. Conformance — what a new provider must do
A provider claiming Launchfile support MUST:
-
Build from the portable contract (
runtime+commands) — never require a provider-specific recipe (§6). -
Ignore specializations it doesn’t understand and still launch (§6).
-
Resolve mode per component for whatever modes it supports; ignore the other mode’s fields (§4). (A cloud provider is typically artifact-only.)
-
Honor the component selector and
--deps-onlysemantics (§5). -
Provision
requiresresources as a precondition of any selected component, and start the selected components’ downwarddepends_onclosure (the declared dependency targets, transitively); never start unrelated components or reverse-dependencies (§5). -
Resolve the reserved expression namespaces it supports —
$app.*,$storage.<name>.path, resource properties,$secrets.*,$components.*; unknown reserved keys resolve to""(L-4). A provider that provisions storage MUST inject$storage.<name>.pathso the path never appears in a command (D-36/D-39, §7). -
Persist resolved deployment state and resolve cross-component references by consumer vantage (§8). Providers SHOULD interoperate via the shared state file so invocations compose.
-
Report gaps, not silent drops — if a field can’t be honored, surface it (the AWS probe’s conformance report is the model). Two fields carry the hard form of this rule:
-
schedule— a provider that does not execute a component’sscheduleMUST emit a launch-time warning naming the component and the field (D-51); a translation-only provider records the same gap in its conformance report, which is its equivalent of launch. The warning states what this provider does and MUST NOT assert what the app will do — a component may schedule itself, and a warning that misstates the author’s app is worse than the silence it replaces. Silence is the wrong default here because an unexecuted schedule produces no error and no missing endpoint: the component is started once at launch, which reads as a successful first run, and nothing afterwards distinguishes a scheduled component from an unscheduled one. This is distinct from the unknown-key tolerance (P-6; SPEC.md’s “unknown keys are ignored”):scheduleis a specified field, and known-but-unexecuted is a gap to report, not an extension to skip. -
env.<NAME>withrequired: true— a provider MUST NOT substitute a fabricated value for an unsupplied required environment variable (D-52). Both terms are narrow:- A required variable is unsupplied when none of the file’s own value sources yields a value for it: no
generator:, nodefault:(including an expression default), and no resourceset_env:binding that actually injects. The test is arrival, not declaration — aset_env:binding on asupports:resource injects only when that optional resource is provisioned (SPEC.md §Supports), so with the resource absent the variable is unsupplied despite a binding existing for it. A value that does arrive is supplied by the Launchfile and is not a fabrication:generator: secretis the sanctioned way to say “any strong value will do”, anddefault:is the author having already answered. Providers resolve those sources first; only what survives all of them is unsupplied. - Fabricated means a value the provider invented rather than obtained: a constant such as
PLACEHOLDER, a guess derived from the variable’s name, a value read from a documentation-only field such asexample:, or any other synthesized stand-in.
requiredstates that the app cannot start without the value (SPEC.md §Environment Variables); inventing one turns an unmet precondition into a launch that appears to have succeeded. That is a stronger violation than the silent drop this rule already forbids — a dropped variable still lets the program’s own presence check fire, while a fabricated one satisfies that check with a lie and defers the failure past every point where it could be diagnosed. The obligation is keyed to the verb (§2), not to the provider’s class, since one provider may expose several. Every verb in §2 falls in exactly one of these four branches:- Deploying verbs (
up, and any verb that provisions or runs) — for a component it is launching, a provider MUST either obtain the value from its own operator-facing channel (the launching environment, a prompt, a secret store; which one is the provider’s choice, P-11/L-3) or fail, naming the component and the variable. A prompt counts as an operator channel only in an interactive session. A non-interactive invocation MUST take the fail branch — converting a fast, named failure into a job hanging on stdin is a different silent failure, and this rule exists to prevent that class. Component selection bounds which components this reaches (§5, rule 4): an unsupplied variable in a component outside the start-set is not a launch-blocking gap. translate, where there is no launch at which to fail — a provider MUST leave the variable absent from the emitted artifact (absent, not empty: an empty value is a fabrication of the “it’s fine” kind, and rule 6’s""for unresolvable expressions does not apply here) and list it as unmapped on its conformance report.- Verbs that report a component’s resolved environment (
env;statuswhere its output includes resolved environment) — a provider MUST report the variable as unsupplied rather than omit it from the output. These verbs neither deploy nor emit an artifact, so neither branch above fits, butenvin particular is where an operator goes to find out what an app’s environment resolves to; silently dropping the one variable they are missing is the failure this rule exists to forbid, in the place it does the most damage. Becauseenvoutput is designed to beeval’d (§2), the report MUST NOT be a bare value line on stdout — a comment or a diagnostic on stderr keeps the output evaluable. down,list,logs, and astatuswhose output does not include resolved environment — no obligation. Teardown resolves nothing,listenumerates deployments,logsstreams process output, and astatusthat reports only what is running (§2’s definition, and what both reference providers implement) resolves no environment; none of them resolves or emits environment values, so an unsupplied variable is not reachable through them.
Where the variable is also
sensitive: true(D-18) this is a security requirement as well as a correctness one: a fabricated credential is a publicly known constant credential.Adoption status: at the time this rule was written, the deploying branch is satisfied by no reference provider — docker fabricates and macos-dev silently omits; aws satisfies the
translatebranch. A provider author reading this section should treat the rule as normative and the reference implementations as not yet conformant, not the reverse. See D-52 → Conformance at adoption. - A required variable is unsupplied when none of the file’s own value sources yields a value for it: no
-
-
Grant or refuse
host:capability entries — mount/forward the capability’s coordinate and populate its properties, or refuse the component with a clear surfaced message; never deploy a component whose required capability was silently dropped (§11). -
Honor the failure semantics (SPEC.md § Failure semantics) — a provider executing a deploy MUST run a declared
releaseafter the component’s required resources are ready and beforestart, and MUST fail the deploy on its error. A provider that cannot run one-shot commands says so (item 8), never skipsreleasesilently. A provider MUST NOT silently substitute a default for an unparseable duration — it surfaces the error with the disposition of the slot or health check the duration belongs to. Numeric timeout defaults for absent durations remain provider-side; a provider MUST document its defaults. -
Interpret command strings with a POSIX shell (SPEC.md § Command interpretation) — or report the command unhonored per item 8. A provider MUST NOT split a command on whitespace and execute the first token: any command using
&&, a pipe, a redirection or variable expansion is silently mangled, and the failure names a binary the author never wrote.
A translation-only provider (IaC/manifest emitter) satisfies the contract by mapping the fields above to its target and listing what it cannot map — it need not implement up/down. This is a statement about which verbs a provider must offer, not an exemption from the rules governing the verbs it does offer: where a rule above is keyed to a verb (rule 8’s required branches), the verb being invoked governs, and a provider that exposes both translate and up takes each branch in turn.
11. Host capabilities — the grant/refuse fulfillment mode ✅ (D-44)
requires/supports entries marked host: (SPEC Host capabilities) are not provisioned — they are granted or refused. This is a distinct fulfillment mode from provisioning backing services (§10 item 5). A provider MUST do one of:
- Grant — mount or forward the capability’s underlying coordinate (e.g. bind-mount
/var/run/docker.sock, or forward a TCP endpoint) and populate the capability’s properties —$socket,$url,$apiforcontainer_runtime— so the entry’sset_envwiring resolves. - Refuse — decline to deploy the component, with a clear, surfaced message naming the capability it cannot grant (§10 item 8: report gaps, not silent drops). A refusal is user-visible output, not a line buried in a warnings array.
Required vs optional follows the entry’s home: a requires capability MUST be granted or the component refused; a supports capability MAY be left ungranted — the component still deploys, its capability set_env vars are simply absent, and the provider SHOULD note the un-granted capability so the degradation is visible.
The legacy top-level host: block carries the same semantics (its keys are the same capabilities in block spelling) and MUST be honored equivalently — a file using the new entry form and a file using the legacy block get the same grant/refuse outcome.
Reference refuse behavior — @launchfile/docker. The Docker provider refuses required host capabilities: a component with a required container_runtime (or legacy host.docker: required), network: host, or privileged: true capability is excluded from the generated compose project with a surfaced refused: … message — it declines rather than attempting Docker-in-Docker. Optional (supports) capabilities are left ungranted with a note. A provider that can safely grant (e.g. a VM-per-app provider mounting the runtime socket into an isolated guest) grants and populates the coordinates instead.
This contract consolidates the provider-facing halves of D-37 (modes, slots, selection), D-38 (install/dev/source), and D-40 (build line), plus the cross-invocation state/event design note. DESIGN.md remains the file-format decision log; provider-runtime decisions live here.