feat: propose pluggable agent dispatch with a docker backend - #232
Merged
Merged
Conversation
outofcoffee
marked this pull request as ready for review
September 19, 2026 09:52
Proposes making the orchestrator's agent dispatch pluggable: today's bare-process launch becomes one backend among others, with a docker backend as the first alternative — host networking, a scoped per-launch config mount, an official agent image (opencode, Pi, gh), and a harness.yaml an operator can use to set environment variables and startup/shutdown shell hooks around either backend without building a new image.
Dispatcher.Launch's one hardcoded path — a bare exec.Command — becomes a Launcher interface behind which the bare backend (unchanged behaviour) and a new docker backend both sit, chosen with --dispatch on `spinloop orchestrator`. The docker backend runs an item's agent in a container from the official spinloop agent image (opencode, Pi, gh), on the host's network, with the item's directory mounted as the workspace and a scoped, per-launch config carrying just that launch's provider — rendered by a new harness.ConfigRenderer, never the host's own harness config. harness.yaml (found beside the items file, or named with --harness-config) lets an operator add environment variables and startup/shutdown shell scripts around either backend, without building a new image: both backends run a small generated wrapper script in place of the harness whenever one is named, forwarding an abort's stop signal to the harness child so shutdown still runs, and a sentinel exit distinguishes a failing startup script from the harness's own outcome. Task 5's Dockerfile and CI publishing job are written but the image build itself could not be verified end to end in this environment: apt-get update inside any container fails here with a GPG signature error, reproduced against the unmodified upstream base image alone, so it is this sandbox's network rather than the Dockerfile. The two build-time tasks are left unchecked in tasks.md for a real build to confirm.
The earlier apt GPG failure was Docker Desktop's disk, not the network or the Dockerfile: after a docker system prune, the image builds cleanly, opencode/pi/gh all report their pinned versions, and a container started from it carries no provider config for either harness. All 26 tasks now checked off.
opencode's one-shot launch had no terminal to answer a permission prompt with: one either blocked the item forever, or was auto-rejected off a terminal, which silently stopped the agent from doing the item's own work rather than failing loudly. Add --auto to opencode's one-shot form, trusting every tool the item's instructions call for. This is the same fix under either dispatch backend, since neither has a terminal for opencode to prompt. Pi's one-shot form is left alone: its own --help lists no equivalent flag, and nothing observed so far suggests it prompts at all in --print mode.
A one-item backlog against examples/gateway-docker's real (fake-engined) gateway, showing harness.yaml's env and startup/shutdown hooks under both --dispatch backends. Verified end to end: bare and docker both reach the fake engine over the gateway, run startup and shutdown in the right order around the agent's own output, and the docker path's scoped per-launch config mount lands where expected.
…atch: --harness-config had no completion registered, so it fell through to the default (no candidates) instead of offering paths the way --items and --fleet already do. harness.yaml also gains a dispatch: field, naming the backend the way --dispatch does — useful for keeping the choice with the rest of an item's environment. An explicit --dispatch still wins outright; where the flag is not given, harness.yaml's is the run's choice.
…ss.yaml's dispatch field proposal.md, design.md, the agent-dispatch spec, and tasks.md now cover harness.yaml's dispatch: field and its precedence against an explicit --dispatch, matching the code already merged into this branch.
…ess.yaml Makes the field explicit rather than implicit, and the "Trying --dispatch docker" section's flag now visibly overrides it — the precedence rule in the example itself, not just described in prose.
… not --network host --network host only puts a container on the real host's network namespace on native Linux docker; Docker Desktop's Mac and Windows builds keep it in their own VM unless an operator has turned on a setting most do not have on, so a fleet.yaml naming a loopback gateway (the common case for a gateway run on the same machine) was unreachable from the docker backend's containers. The docker backend now runs on the default bridge network and rewrites a loopback gateway address to host.docker.internal for the container's own rendered config alone — the fleet file and the gateway flag are untouched, and the bare backend's resolvePlan calls are unaffected. A gateway already on a routable address is unchanged. --add-host host.docker.internal:host-gateway makes the address resolve on native Linux docker too, where it is not automatic the way it is on Docker Desktop. Verified against a real loopback gateway (examples/gateway-docker, --gateway http://localhost:4000): the container's rendered config now names host.docker.internal, and the item reaches the gateway and completes.
…ctories An item's own directory (`dir` in the items file) is now the storage location for that item's launch, not the harness's working directory directly: a `workspace/` subdirectory (created fresh on every launch, under either backend) is where the harness actually runs, and, under the docker backend, a `config/` subdirectory holds the launch's scoped provider config. The docker backend mounts both under one container root (/item), redirecting each harness's own config resolution there (XDG_CONFIG_HOME for opencode, PI_CODING_AGENT_DIR for Pi) rather than into the image's fixed $HOME. Verified end to end against a real gateway-docker stack for both --dispatch bare and --dispatch docker.
…r for items Adds two fields to harness.yaml, alongside dispatch: - harness: names the harness the run uses, the way --harness/-H does. An explicit --harness still wins outright; where it is not given, this is tried ahead of the HARNESS environment variable and the stored preference. - baseDir: the directory an item's own relative dir resolves against, in place of the directory the orchestrator command happens to be started from. An absolute dir is unaffected. A relative baseDir resolves against harness.yaml's own directory, so the file stays portable together with the items it describes. Verified end to end against a real gateway-docker stack: harness.yaml naming pi, an explicit --harness opencode overriding it, and baseDir placing an item's workspace/ under a directory other than the one the command was started from.
… harness.yaml Pins the walkthrough to opencode explicitly and demonstrates baseDir as a (here, no-op) example of keeping an item's relative dir resolving against this directory regardless of where the command is run from.
Once an admitted item's agent ends, under either backend, its kept log is now also copied into the item's own directory (<dir>/log), alongside workspace/ and config/. This is a static copy, written once Wait returns, not a live mirror: work logs and the work list API still read the canonical copy beside the items file, which streams while the agent runs. work remove takes the copy out with the rest of the item's kept output. Verified end to end against a real gateway-docker stack under --dispatch docker.
…hive - Fix stale docker run -d/docker wait and bareLauncher wording in design.md/tasks.md — the actual code runs docker run in the foreground and Dispatcher itself is the bare Launcher, no separate wrapper type. - Add the item-dir log copy to proposal.md's What Changes, and expand task 8.1 to name everything docs/commands/orchestrator.md now covers. - Add fleet-orchestrator as a modified capability: "Running an item" now says the agent works in a workspace subdirectory of the item's own directory, not the item's directory directly.
Creates openspec/specs/agent-dispatch/spec.md (new capability) and updates fleet-orchestrator's "Running an item" requirement to say the agent runs in a workspace subdirectory of the item's own directory, matching the docker-agent-dispatch change now archived.
outofcoffee
force-pushed
the
docker-agent-dispatch
branch
from
September 19, 2026 09:59
2ad403d to
d1171fb
Compare
…pace/ The orchestrator now runs an item's agent in a workspace/ subdirectory of the item's own directory rather than the directory itself, so the stub agents' args.txt/key.txt/reply.txt/launches.txt land there too.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Proposes an openspec change making the orchestrator's agent dispatch pluggable, with a docker backend as the first alternative to today's bare-process launch.
Summary
openspec/changes/docker-agent-dispatch/carries the proposal, the newagent-dispatchspec, a design doc, and a task breakdown.Dispatcher.Launch's one hardcoded path (a bareexec.Command) becomes aLauncherinterface with two backends:bare(today's behaviour, unchanged) anddocker, chosen with a new--dispatchflag onspinloop orchestrator.gh), on the host's network, with two mounts: the item's directory as the workspace, and a scoped, per-launch config directory carrying only that launch's provider — never the host's own harness config.harness.yaml(found beside the items file by default, or named with--harness-config) lets an operator set anenvmap andstartup/shutdownshell scripts around either backend, without building a new image.Implementation details
Launchcall, not lower, because the docker backend changes more than how a process starts — mounts, an image, no host config write.harness.yaml's "shutdown always runs, even on abort" requirement is met with a small generated wrapper script both backends run in place of the harness whenever startup/shutdown are named: it forwardsSIGTERMto the harness child, then still runs shutdown before it exits, soAbort's existing wait on the child needs no change.Full reasoning and the open questions already resolved are in
design.md.