Launchers
A launcher provider maps submit, status, and cancel (and, for providers that
declare it, capacity and logs) onto a platform’s real execution mechanism. This page
covers the credential-free local-runs launcher in depth, then the two platform launchers,
Weights & Biases Launch and Dagster, that share the same contract described in
contracts and conformance.
The local-runs launcher
Section titled “The local-runs launcher”plugins/local-runs is the zero-credential mlops.launcher provider: its provides block
maps submit/status/cancel plus the capacity and logs capability groups onto the
first-party mlops.local.* tools. It depends on mlops-core and declares one page type,
mlops/template.
Binding and template as page config
Section titled “Binding and template as page config”A binding page carries two pieces of metadata:
metadata.fs.path, the workspace directory (the same field the filesystem tracker watches).metadata.mlops.launcher {provider: 'local-runs', template: <page ref>, capacity?: {slots? | argv?}}, pointing at a separate template page.
The template page carries metadata.mlops.template {mode: 'bash' | 'docker', argv? (bash mode), image/cmd/mounts/gpus/memoryMb/cpus/network/env (docker mode)}. Placeholders
{jobId}, {runDir}, {gitRef}, and {config.<key>} render per launch instance, only
inside argv-style arrays, and an unknown placeholder fails the render closed rather than
launching with a literal {typo} in the command line.
The bash lane
Section titled “The bash lane”mlops.local.submit {page?, config?, gitRef?, hypothesis?} (admin-only, alwaysGate,
idempotency claim mlops.submit:<callId>; page defaults to the invoking page) renders the
bound template. In bash mode it spawns the rendered argv through the PTY host in a terminal
node on the binding page, so the run’s live progress is literally its scrollback. Spawning
requires SUBSPACE_OS_EXEC=1 on the server. Bash-lane submits always gate per call and
never auto-launch, regardless of any standing approval.
The Docker lane and containers.*
Section titled “The Docker lane and containers.*”Docker mode launches a detached container through the containers.* gated core tool
family, a thin wrapper over the Docker CLI via execFile (argv only, never a shell):
| Tool | Behavior |
|---|---|
containers.run {image, cmd?, mounts?, gpus?, memoryMb?, cpus?, network?, env?, name?} |
Detached start, returns the container id. Admin-only, alwaysGate, claim containers.run:<callId>. |
containers.status {id} |
Ungated read. |
containers.logs {id, tail?} |
Ungated read. |
containers.stop {id} |
Admin-only, needsApproval (policy-relaxable). |
Isolation defaults lock the container down unless the template explicitly opens it up:
--network none unless a network is named, mounts are read-only unless readonly: false,
env values are literal strings only (broker-acquired secret material never enters a
container’s environment), and gpus/memoryMb/cpus cap resource use. Opt in to real
Docker with SUBSPACE_CONTAINERS=1, or point SUBSPACE_DOCKER_BIN at a fixture binary for
testing.
Both lanes pre-create the mlops/run page skeleton before the process or container starts;
the filesystem tracker then adopts that same page by (source page, job id) on its next
sweep, so a run never gets a duplicate page and its status never gets demoted from a
terminal state by a stale materialize. The job id derives from the durable call id, so a
crash-replay resumes the same job rather than launching a second one.
Template approval autonomy
Section titled “Template approval autonomy”The launch template is the consent object for the Docker lane. mlops.approveTemplate {page} (admin) pins the template’s current version hash, captured through page versioning
and recorded in ops.plugin_state under local-runs. A subsequent Docker-lane submit
whose template still hash-matches the approved version auto-launches: it lifts the standing
alwaysGate through the tool’s standingApproval seam, checked as part of the ordinary
gating decision. This is deterministic and human-granted, never a model deciding to relax
its own gate: any edit to the template page changes its hash and immediately revokes the
standing approval, so the next submit falls back to a normal approval card.
mlops.revokeTemplateApproval {page} revokes explicitly, and mlops.templateApproval {page} reports {approved, approvedHash, currentHash, by, at} so you can see at a glance
whether a binding is currently auto-launching and against which pinned version.
Completion and restart survival
Section titled “Completion and restart survival”A bash run’s completion rides the PTY host’s notifyExit outbox event; a consumer
finalizes the run page from it. A Docker run has no such hook, so a watcher polls every
SUBSPACE_MLOPS_SWEEP_MS (default 20 seconds), scanning running mlops/run pages that
carry a containerId. Because all of this state lives on pages rather than in memory, a
server restart re-arms the watcher automatically with no separate recovery step. Finalizing
a run flips its status (finished or failed by exit code, killed on cancel), stamps
finishedAt, emits mlops-run-ingested, and resumes any workflow durably waiting on an
mlops-run:<pageId> hook.
mlops.local.status syncs a run page from the live container or PTY state and returns
contract LaunchStatus JSON; mlops.local.cancel (admin, gated) issues a docker stop or
kills the PTY child; mlops.local.logs tails docker logs or PTY scrollback (ungated).
mlops.local.capacity {page?} (ungated) reports the binding’s manual capacity.slots if
set, otherwise runs its probe argv: a bare integer output means that many slots, a
per-line GPU utilization reading treats each line under 20% utilization as one free slot,
and the default probe is nvidia-smi. A failing probe degrades to {slots: 0} rather than
erroring the caller.
The mlops.launch-run registered workflow wraps mlops.local.submit in the same
gate/claim/tree-rendering envelope as mlops.ingest-runs, taking workflowArgs {page?, config?, gitRef?, hypothesis?}; the agents/mlops-launch definition page rides it for
slash-invoke (/mlops-launch) on a binding page.
Weights & Biases Launch
Section titled “Weights & Biases Launch”The W&B tracker plugin (see trackers) is also a full launcher: submit, status, cancel, and capacity map onto W&B Launch queues, with capacity read from live queue depth rather than a local probe. Native sweep submission goes through the same launcher, so a sweep started from Subspace lands as a real W&B sweep, not the Cartesian polyfill. External submits are approval-gated and claimed per call id like every other launcher.
Dagster launcher
Section titled “Dagster launcher”The Dagster plugin’s launcher side speaks GraphQL directly: launchRun to submit,
terminateRun to cancel, and status polling against the same API. Capacity comes from a
live read of the coordinator’s active and queued runs against the binding’s
maxConcurrent, so the scheduler sees real headroom rather than a static number.
Feeding the research scheduler
Section titled “Feeding the research scheduler”Every launcher’s capacity group exists for one consumer above the MLOps layer: the
research scheduler drains a goal’s backlog against exactly the
capacity a bound launcher reports right now, whether that is a nvidia-smi probe on a
local box, a W&B queue depth, or Dagster’s run-queue count. Because capacity is a live
contract call rather than a config constant, the scheduler never over-submits into a
launcher that is actually full.