Wrapping Thirty Coding Agents Inside a Single Container: Inside HarnessRouter's Protocol and Process Boundaries

Back to blog
Mehran Mozaffari·

The Agent Gateway Façade: Multiplexing CLI Harnesses Behind OpenAI Responses

Building software with autonomous coding agents currently forces infrastructure teams into an awkward architectural trade-off. Upstream agent projects rarely expose clean, uniform daemon APIs. Instead, tools like Claude Code, OpenAI Codex, Hermes, PI, and DSH arrive packaged as interactive terminal CLIs built around human-facing pseudoterminals, proprietary event formats, and bespoke interactive confirmation loops. When you want to wire thirty different coding harnesses into a shared CI pipeline, an evaluation platform, or an internal review bot, the standard approach has been to write and maintain thirty brittle subprocess wrappers.

Screenshot of https://github.com/HarnessRouter/harnessrouter
The HarnessRouter repository root containing orchestration source and container configurations. (1) Root architectural documentation and setup instructions

HarnessRouter attacks this fragmentation by acting as an open-source, Apache-2.0 reverse proxy and execution manager packaged into a single container image. Rather than requiring client applications to understand the unique execution conventions of every underlying CLI, HarnessRouter exposes a standard OpenAI Responses-compatible HTTP and streaming interface. A caller issues a standard POST /v1/responses request, specifies the target harness through a metadata payload, and receives back a normalized stream of token chunks and lifecycle updates.

Behind this façade, HarnessRouter operates as a parasitic wrapper. It does not rewrite agent prompt loops or implement native tool-calling engines from scratch. Instead, it swallows the complexity of managing CLI event loops, converting messy pseudoterminal output and raw stdout streams into structured events conforming to the Unified Harness Protocol.

sequenceDiagram
    autonumber
    participant Client as API Caller
    participant Console as Console (:3000)
    participant Gateway as Gateway (:8080)
    participant Runner as Runner (:8081)
    participant CLI as Harness Subprocess (e.g. Codex)

    Client->>Console: POST /api/harness/v1/responses
    Console->>Gateway: Reverse proxy request
    Gateway->>Runner: Initialize session & allocate workspace
    Runner->>CLI: Spawn child process & pipe prompt to stdin
    loop Execution & Tool Runs
        CLI-->>Runner: Stream stdout/stderr & tool events
        Runner-->>Gateway: Normalize to UHP events
        Gateway-->>Console: Stream Server-Sent Events
        Console-->>Client: Stream response tokens & status updates
    end
    CLI-->>Runner: Process exit
    Runner-->>Gateway: Persist file artifacts & session completion
    Gateway-->>Client: Final response payload & close connection

From an operational perspective, the primary design tension centers on whether consolidating multiple terminal drivers behind an HTTP proxy justifies the operational weight of a monolithic container. In my view, wrapping heterogeneous command-line tools into an HTTP boundary is a pragmatic intermediate fix, but it does not erase upstream volatility. When an upstream harness modifies its terminal control sequences or interactive prompt confirmations, the gateway parser breaks unless updated. As I have argued when discussing how to handle coordination across standalone CLIs in How AutoSwarm Turns Isolated AI CLIs into a Coordinated Swarm Without a Shared Runtime, decoupling orchestration from runtime processes prevents harness quirks from destabilizing the entire platform. Adopting HarnessRouter means accepting that convenience comes bundled with thirty distinct upstream release schedules.

Tracing a Run from HTTP POST to Headless CLI Execution

The lifecycle of an execution begins when an external client dispatches an HTTP request to the publicly exposed entrypoint on port 3000. Under the hood, HarnessRouter splits responsibilities between an outer edge layer, an internal routing gateway, and a local execution runner.

Screenshot of https://github.com/HarnessRouter/harnessrouter/tree/HEAD/protocol/conformance
The Unified Harness Protocol conformance suite specifying lifecycle and streaming contracts. (1) Test suites validating normalized harness behavior

export HARNESSROUTER_BASE_URL=http://localhost:3000/api/harness

curl --fail-with-body -sS "$HARNESSROUTER_BASE_URL/v1/responses" \
  -H "Authorization: Bearer ${HARNESSROUTER_API_KEY:?}" \
  -H 'content-type: application/json' \
  -d '{
    "input":"Reply with exactly: it works.",
    "metadata":{"harness_id":"codex"},
    "model":"gpt-5.4-mini",
    "stream":false
  }'

This payload routes a normalized prompt to the underlying Codex harness while specifying the requested model and execution parameters over standard HTTP.

When the published Console port receives this request, it reverse-proxies the payload over loopback to the internal Gateway service listening on port 8080. The Gateway validates the bearer token, parses the input message, and inspects the metadata.harness_id parameter to determine which CLI executable must handle the task. Once authenticated, the Gateway forwards the execution order to the Runner daemon on port 8081.

The Runner allocates a fresh workspace path beneath the mounted /data directory tree. It provisions the environment variables, copies any initial repository context, and invokes the selected harness binary as a child subprocess. Because coding agents expect an interactive shell, the Runner attaches the subprocess to a headless pseudoterminal stream, feeding the input prompt directly into stdin.

IDEMPOTENCY_KEY=$(python3 -c 'import uuid; print(uuid.uuid4())')

for attempt in 1 2; do
  curl --fail-with-body -sS "$HARNESSROUTER_BASE_URL/v1/responses" \
    -H "Authorization: Bearer ${HARNESSROUTER_API_KEY:?}" \
    -H 'content-type: application/json' \
    -H "Idempotency-Key: $IDEMPOTENCY_KEY" \
    -d '{
      "input":"Reply with exactly: it works.",
      "metadata":{"harness_id":"codex"},
      "model":"gpt-5.4-mini",
      "stream":false
    }'
  printf '\n'
done

Supplying a unique idempotency key guarantees that duplicate network dispatches reuse existing task execution state rather than spawning redundant subprocess trees.

As the agent executes commands and runs tools, the Runner consumes raw stdout and stderr output, stripping terminal escape sequences and mapping progress updates into structured event chunks. These chunks stream backward through the Gateway to the client as Server-Sent Events. Tracking filesystem diffs and session records locally allows the Runner to capture modified files directly from the mounted volume when the child process terminates, saving final artifacts before closing the HTTP response.

Component Runtime Role Interface / Protocol Persistent Storage Artifacts
Console (:3000) Single exposed edge ingress and reverse proxy HTTP / HTTPS, Server-Sent Events Session cookies, web console assets
Gateway (:8080) API contract enforcement, auth check, lifecycle dispatch Loopback HTTP, Unified Harness Protocol Request metadata, idempotency indexes
Runner (:8081) Subprocess management, pseudoterminal I/O parsing Loopback RPC / IPC, child process stdin/stdout Active process IDs, streaming log buffers
Storage (/data) State coordination and working file storage Filesystem mount, POSIX file I/O SQLite database, cloned git repos, generated file diffs

State, Streaming, and the Monolithic Container Compromise

The core engineering decision in HarnessRouter is radical colocation: packaging the API gateway, execution runner, database, and every target CLI harness inside a single Docker container. While this eliminates distributed coordination and lets operators stand up a functional system with a single volume mount, it introduces severe mechanical bottlenecks under sustained production loads.

flowchart TD
    subgraph ClientLayer["Client Layer"]
        Req["HTTP POST /v1/responses"]
    end

    subgraph ContainerRuntime["HarnessRouter Container Runtime"]
        GW["Gateway Daemon (:8080)"]
        Runner["Runner Daemon (:8081)"]
        SubProc["Harness Subprocess Tree"]
    end

    subgraph StorageVolume["Mounted /data Volume"]
        DB[("SQLite Database<br/>(Sessions, Tokens, States)")]
        Workspaces["Workspace Directories<br/>(Git Clones, Temp Files, Diffs)"]
    end

    Req -->|Dispatch Task| GW
    GW -->|Initialize Session| DB
    GW -->|Spawn Execution| Runner
    Runner -->|Create Directory| Workspaces
    Runner -->|Fork & Exec| SubProc
    SubProc -->|File Mutations & Git Commits| Workspaces
    SubProc -->|Stdout / Stderr Streams| Runner
    Runner -->|Flush Event Logs & Checkpoints| DB
    Runner -->|Stream Chunks| GW
    GW -->|SSE Output| Req

State coordination relies entirely on an embedded SQLite database written directly to the /data directory alongside workspace files. When a handful of sessions run sequentially, SQLite easily handles metadata tracking. However, autonomous agents do not behave like typical CRUD web users. A single active agent run can emit dozens of bash executions, code linting passes, and file writes every minute. When twenty concurrent sessions attempt to stream high-frequency event updates into a single SQLite file, transaction throughput collapses under database lock contention. Write locks block incoming HTTP requests, creating latency spikes at the gateway layer.

The shared filesystem introduces an equally punishing bottleneck: IOPS thrashing. If multiple agent sessions concurrently perform dependency installations—such as npm install pulling thousands of files or heavy language compilation steps—the underlying container volume experiences severe read/write starvation. Because the gateway and runner run inside this same filesystem namespace, disk latency directly degrades HTTP API responsiveness.

Finally, bundling thirty autonomous CLIs into one Docker image creates an unmaintainable dependency graph across Node.js, Python runtimes, and native compilers. An agent harness is not an unopinionated binary; it relies on specific interpreter versions, global packages, and environment configurations. A dependency bump required by one agent risks breaking the execution runtime of another. As I warned in An Agent That Edits Its Own Harness Must Not Be Its Own Kernel, failing to strictly isolate execution environments from management infrastructure turns minor software conflicts into systemic cluster failures. Placing multiple complex runtimes into one container makes image upgrades exceptionally fragile.

Testing the Isolation Boundary: Why User Accounts Cannot Sandbox Arbitrary Bash

A common architectural assumption when scaling agent runners is that provisioning separate Linux user accounts for concurrent sessions delivers adequate runtime isolation. If Session A runs under one UID and Session B under another, standard filesystem permissions appear to keep tasks compartmentalized. But examining HarnessRouter directly complicates this narrative. The published project artifacts show a container image serving an HTTP and streaming interface, not an autonomous container management daemon. Nowhere in the published material do we find dynamic useradd provisioning, setuid privilege dropping, or dedicated per-session user lifecycle routines.

Even if per-session Linux accounts are active behind the runner, standard POSIX discretionary access control does not constitute a secure multi-tenant boundary for arbitrary bash execution. Autonomous coding agents are designed to execute arbitrary shell scripts, pull third-party packages, run compilers, and invoke native build toolchains. In a shared container environment, standard Unix user separation fails across three distinct vectors: shared storage, process visibility, and resource contention.

First, standard temporary and scratch locations like /tmp, /var/tmp, and /dev/shm remain shared across all local users unless isolated mount namespaces are created for every task. A child process running under Session A can inspect world-readable intermediate build outputs, lockfiles, and scratch scripts left behind by Session B. Second, unless /proc is explicitly mounted with hidepid=2—a configuration standard container engines do not enable by default—every unprivileged user can read the process hierarchy of neighboring sessions. An agent invoking a standard process listing command can see every active command line across the entire container, trivially harvesting repository tokens, external API keys, or private internal endpoints passed via CLI flags.

Finally, Linux cgroup memory and CPU limits inside standard Docker runtimes are enforced at the container perimeter, not subdivided dynamically across ad-hoc system users. If an agent task in Session A triggers an unconstrained build, launches a memory-leaking language server, or runs an aggressive test suite with dozens of parallel workers, it consumes the shared memory pool of the container. When the host runs low on memory, the kernel Out-Of-Memory killer steps in, often terminating the Gateway process or dropping neighboring sessions without warning.

To verify whether your deployment actually isolates concurrent runs, operators should execute an outside check directly against the host or container runtime. Launch two concurrent tasks against distinct harnesses, attach a shell to the running container, and inspect the process listing under the proc filesystem across all numeric process IDs. Check whether processes spawned under one task can read the command line arguments and environment files of another task. If the environment variables and argument arrays of Session B are readable from the process table of Session A, your boundary is entirely illusory.

Where to Place HarnessRouter in the Production Stack

Navigating the landscape of agent infrastructure requires drawing sharp boundaries between model routing, execution sandboxing, and CLI orchestration. Teams often confuse these layers because each promises some form of abstraction.

Screenshot of https://harnessrouter.ai/open-source
HarnessRouter Community Edition architecture overview and deployment specifications. (1) Self-hosted runtime architecture guidelines

At the top layer sit inference gateways like LiteLLM or Portkey. These tools operate purely at the token and API contract level: they ingest prompts, balance traffic across LLM providers, and return completion chunks over HTTP. They never execute code, touch local filesystems, or manage terminal subprocesses. At the opposite end of the spectrum sit microVM sandboxes such as E2B and Modal. These systems treat the Linux kernel boundary as the atomic unit of execution, spinning up hardware-virtualized environments with ephemeral filesystems and dedicated network stacks for every single session.

HarnessRouter sits between these two poles as an execution-layer multiplexer. It does not replace model routing, nor does it provide hardware virtualization. Instead, it acts as a headless terminal driver that standardizes thirty divergent CLI harnesses into a single OpenAI Responses interface. This distinct placement defines where it belongs in your platform architecture.

For local development, automated integration test suites, and internal developer tooling, HarnessRouter delivers genuine leverage. Building bespoke headless PTY drivers, ANSI escape parsers, and cancellation routines for Codex, Claude Code, and Hermes burns engineering quarters on undifferentiated plumbing. In trusted enterprise environments where developers already possess repository access and shell privileges, placing HarnessRouter on a dedicated internal runner removes integration friction.

However, deploying HarnessRouter as a bare, multi-tenant execution tier exposed to untrusted external workloads is an architectural mistake. Because its processes share a container filesystem and kernel namespace, it cannot defend against adversarial tenants. As I explored when evaluating agent runtimes in Should You Run OpenDuet in Production? Steal the Chat Patterns, Leave the Runtime, adopting an agent's orchestration surface should never mean inheriting its uncontained execution defaults. The safe operational pattern is to treat HarnessRouter as the internal engine inside an ephemeral MicroVM, discarding the instance after every task.

Three Systems You Can Build with Unified Agent Harnesses

Unifying heterogeneous coding agents behind a standardized HTTP interface allows engineering teams to design higher-order evaluation and automation systems that were previously impractical to maintain.

One compelling architecture is an Automated Cross-Harness PR Benchmark Evaluator. In this setup, incoming pull requests trigger GitHub webhooks routed to an evaluation orchestrator. The orchestrator dispatches the identical problem prompt across Claude Code, Codex, and Hermes via parallel calls to the HarnessRouter OpenAI Responses API endpoint. Each run executes against an ephemeral workspace checkout on disk. Once the harnesses complete their edits, a downstream test runner executes the test suite against each patch to compute objective fix rates. The primary failure mode to guard against is upstream CLI volatility: whenever an upstream vendor pushes a CLI update that modifies interactive confirmation prompts or ANSI output sequences, the runner's parsing routines can stall mid-evaluation.

A second pattern is an Internal Multi-Agent Triage Bot for Monorepos. Internal developers interact through Slack or a terminal CLI to request automated bug investigations. An edge triage router inspects the incoming issue, determines the repository path and language domain, and calls the HarnessRouter session management API to dispatch the task to the best-suited harness—routing Python tasks to one CLI while delegating TypeScript refactors to another. The runner attaches to shared local git workspace volumes, executes the investigation, and passes diff proposals to an approval callback listener. Here, the critical risk is resource monopolization from parallel language servers and background build tools triggering container-level out-of-memory crashes. Mitigating this requires active process tracking and aggressive cleanup hooks that terminate orphaned subprocesses before new tasks spin up.

Finally, teams handling untrusted customer code can build a Sandboxed Coding Sandbox Farm with MicroVM Isolation. An edge API gateway ingests requests and dynamically provisions ephemeral MicroVM instances—such as Firecracker guests—each running a single-tenant HarnessRouter container image. The task runs in complete isolation, uploads generated diffs and session logs to an external artifact bucket, and immediately self-terminates. The key trade-off to manage in this architecture is image pull latency: packaging dozens of agent toolchains, language runtimes, and compilers into a single container image creates a substantial footprint that lengthens cold-start provisioning times. Pre-baking the container image directly into the base MicroVM rootfs solves this startup bottleneck effectively.

Resources

Updated 2026-10-28 by Mehran Mozaffari.

Related posts