Skip to content

feat: propose pluggable agent dispatch with a docker backend - #232

Merged
outofcoffee merged 16 commits into
mainfrom
docker-agent-dispatch
Sep 19, 2026
Merged

outofcoffee merged 16 commits into
mainfrom
docker-agent-dispatch

Conversation

@outofcoffee

Copy link
Copy Markdown
Collaborator

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

  • Planning only — no implementation yet. openspec/changes/docker-agent-dispatch/ carries the proposal, the new agent-dispatch spec, a design doc, and a task breakdown.
  • Dispatcher.Launch's one hardcoded path (a bare exec.Command) becomes a Launcher interface with two backends: bare (today's behaviour, unchanged) and docker, chosen with a new --dispatch flag on spinloop orchestrator.
  • The docker backend runs an item's agent in a container from a new official spinloop agent image (opencode, Pi, 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.
  • A new harness.yaml (found beside the items file by default, or named with --harness-config) lets an operator set an env map and startup/shutdown shell scripts around either backend, without building a new image.

Implementation details

  • The interface sits at the whole Launch call, 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 forwards SIGTERM to the harness child, then still runs shutdown before it exits, so Abort's existing wait on the child needs no change.
  • Host networking is a deliberate trade-off for now — it gives up the docker backend's network isolation in exchange for a loopback-bound gateway working exactly as it does for a bare launch, with no new configuration.

Full reasoning and the open questions already resolved are in design.md.

@outofcoffee outofcoffee added enhancement New feature or request go Pull requests that update go code labels Sep 17, 2026
@outofcoffee
outofcoffee marked this pull request as ready for review September 19, 2026 09:52
outofcoffee and others added 15 commits September 19, 2026 10:55
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.
…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.
@outofcoffee
outofcoffee merged commit a5ead4d into main Sep 19, 2026
3 checks passed
@outofcoffee
outofcoffee deleted the docker-agent-dispatch branch September 19, 2026 10:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request go Pull requests that update go code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant