Skip to content

Harbor agents

trialdesignbench.harbor_agents

Harbor agent classes tdb run launches: Harbor's adapters with a file-based instruction transport.

This module is a Harbor plugin, not a core module. Harbor imports it through the import_path of each agent in the job.yaml that tdb run writes: tdb run copies this file beside the task copies as tdb_harbor_agents.py and puts that directory on PYTHONPATH for the harbor process. It imports Harbor and the standard library only, never the rest of this package, and no tdb command imports it, so the core modules still never import Harbor.

Why

Harbor's adapters for Claude Code, Codex CLI, Grok Build, and OpenCode hand the rendered instruction to the CLI inside one docker compose exec call: Claude Code as an environment variable (-e NAME=<instruction>), the other three as a quoted positional argument inside bash -c "<command>". Both cross an execve boundary on the host, and the argument crosses it again inside the container, where Linux caps a single argument or environment string at MAX_ARG_STRLEN (128 KiB; 32 KiB on Windows). An instruction with a protocol or SAP of a few hundred kilobytes therefore kills the trial before the agent starts: OSError: [Errno 7] Argument list too long: 'docker'. Harbor 0.23.0 and 0.24.0 both behave this way; the upstream fixes proposed in harbor-framework/harbor#3050 and #3433 are open, and #3433 keeps the environment variable transport, which still crosses the host boundary.

What changes

Each class below subclasses Harbor's adapter and changes only the transport. run() records the rendered instruction, and _exec(), the choke point for every command an installed agent runs, relocates it when a command or its environment carries it:

  • The instruction is written to a temporary file and uploaded with environment.upload_file() to REMOTE_INSTRUCTION_PATH (docker compose cp, which has no such limit).
  • An environment variable holding the instruction (Claude Code, and the transport of #3433) is dropped and re-created as a shell variable read from the file: NAME=$(cat FILE; printf x); NAME=${NAME%x} keeps every byte, including trailing newlines, and shell variables are not subject to the limit as long as only builtins (printf) see them.
  • A quoted positional argument is replaced by the CLI's own way of reading the prompt from a file or stdin: codex exec -- - <FILE, opencode run -- <FILE, grok --prompt-file FILE.

What the CLI receives is byte-identical to the instruction Harbor rendered. Every other upstream behavior (credentials, settings files, skills, sessions, cleanup, trajectories, name(), version()) is inherited unchanged, so the trial's result.json still records the agent as claude-code, codex, grok-build, or opencode. A command that does not carry the instruction (for example once upstream moves to a file transport) passes through untouched; one that carries it in a shape this module does not know fails the trial loudly with InstructionTransportError instead of launching with the old transport.

REMOTE_INSTRUCTION_PATH = PurePosixPath('/installed-agent/tdb-instruction.md') module-attribute

Where the rendered instruction is uploaded in the agent container.

/installed-agent is created by Harbor's BaseInstalledAgent.setup() (root, mode 755) before any agent runs; the file is uploaded with mode 644 so the agent user can read it.

ClaudeCode

Bases: FileInstructionMixin, ClaudeCode

Claude Code. Harbor passes the instruction in a per-launch environment variable and pipes it into claude --print with the builtin printf; the variable becomes a shell variable read from the uploaded file.

Codex

Bases: FileInstructionMixin, Codex

Codex CLI. codex exec ... -- '<instruction>' 2>&1 </dev/null becomes codex exec ... -- - 2>&1 <FILE: - reads the prompt from stdin.

FileInstructionMixin

Relocate the rendered instruction from exec argv/env to an uploaded file.

Mix in before a harbor.agents.installed.base.BaseInstalledAgent subclass. Adapters that pass the instruction as a command argument override _tdb_rewrite_argv with the CLI's file or stdin option.

GrokBuild

Bases: FileInstructionMixin, GrokBuild

Grok Build. grok --single '<instruction>' becomes grok --prompt-file FILE, its single-turn prompt from a file.

InstructionTransportError

Bases: RuntimeError

A launch command carries the instruction in a shape this module cannot relocate. Raised instead of launching with Harbor's original transport.

OpenCode

Bases: FileInstructionMixin, OpenCode

OpenCode. opencode run ... -- '<instruction>' 2>&1 </dev/null becomes opencode run ... -- 2>&1 <FILE: without a message argument OpenCode (0.2.2 and later) reads the prompt from its piped stdin.

shell_read_file(name, path)

Shell snippet assigning the content of path to the variable name.

$(...) strips trailing newlines, so a sentinel x is appended by the builtin printf and removed again; the variable then holds the file byte for byte.