Skip to content

Feature: protocol: subprocess for registered workers (eliminates HTTP bridge shim) #35

Description

@addadi

Feature: protocol: "subprocess" for registered workers

Summary

Add a new worker protocol that dispatches work by spawning a subprocess per task, capturing stdout as the response. Removes the need for a separate HTTP bridge shim (e.g., a Python wrapper around opencode run --pure).

Motivation

Today, integrating CLI agents (opencode, Pi, custom scripts) as nullboiler workers requires running a separate HTTP server (webhook bridge) that:

  1. Listens for nullboiler webhook POST
  2. Spawns the CLI agent as subprocess
  3. Captures stdout
  4. Returns it as webhook response JSON

This is extra infrastructure for no architectural value — nullboiler already spawns subprocesses for tracker-managed workflows (tracker.subprocess.command). Extending that to registered workers eliminates the bridge.

Proposal

Add "subprocess" to the Protocol enum (src/worker_protocol.zig). Worker config:

{
  "id": "opencode-worker",
  "protocol": "subprocess",
  "command": ["opencode", "run", "--pure"],
  "prompt_arg_index": 1,
  "output_format": "stdout",
  "cwd": "/optional/working/directory",
  "env": {"TERM": "dumb"},
  "timeout_ms": 600000,
  "tags": ["implementer"],
  "max_concurrent": 1
}

Engine behavior on dispatch:

  1. Spawn command with the rendered prompt inserted at prompt_arg_index (or appended if prompt_arg_index is null/missing)
  2. Apply timeout_ms (already enforced for sync HTTP per finding Migration system re-runs all migrations on every startup (no version tracking) #4 fix)
  3. Capture stdout (strip ANSI escapes by default? configurable)
  4. If rc == 0: return {response: stdout} (matches existing worker_response.parse shape)
  5. If rc != 0: return failure with stderr in error_text
  6. Respect cwd and env overrides

Use cases

  1. opencode as a graph worker: command: ["opencode", "run", "--pure"]. Already verified via webhook bridge — same semantics, no bridge.
  2. Pi as a graph worker: command: ["pi", "--print"]. Pi has CLI print mode.
  3. Custom scripts as workers: any script that takes prompt-as-arg + writes to stdout. Useful for deterministic transforms (no LLM cost) — regex parsers, API calls, file lookups.
  4. Local LLM scripts: command: ["llama-cli", "-m", "/path/to/model.gguf"] for fully offline workers.

Why this is worth doing

  • Eliminates infrastructure. One fewer service to run, monitor, debug.
  • Generic. Any CLI becomes a worker. No per-agent bridge code.
  • Composable. Subprocess workers can call other tools (curl, file ops) without HTTP intermediation.
  • Test-friendly. Subprocess dispatch is easy to mock in tests.

Existing precedent

  • tracker.subprocess.command already exists for nullclaw spawning in tracker-mode workflows. This proposal extends the same pattern to the worker registry.
  • protocol: "webhook" is essentially "subprocess + HTTP" — a long-running bridge that wraps a subprocess. Direct subprocess dispatch skips the HTTP layer.

Implementation sketch

In src/dispatch.zig dispatchStepWithOpts:

if (protocol == .subprocess) {
    return dispatchSubprocess(allocator, worker_command, worker_args, run_id, step_id, rendered_prompt, timeout_ms);
}

dispatchSubprocess would mirror doFetchTimed shape: spawn child, write prompt to argv, poll for exit, capture stdout/stderr, return DispatchResult.

Config parsing additions to WorkerConfig (src/config.zig):

  • command: []const []const u8 (required for subprocess protocol)
  • prompt_arg_index: ?u32 (default = append to argv)
  • output_format: []const u8 (default = "stdout")
  • cwd: ?[]const u8
  • env: ?std.json.Value

Compatibility

No breaking changes. Existing protocols unaffected. subprocess is purely additive.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions