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()toREMOTE_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.