Skip to content

Commit 1c9a6bd

Browse files
sjarmakclaude
andcommitted
fix: work around Harbor shlex.quote bug for OpenHands task instructions
Harbor's OpenHands agent uses shlex.quote() to escape the --task argument, but this breaks when instructions contain single quotes, backticks, or other shell metacharacters — causing bash syntax errors and 0% agent execution. Fix: base64-encode the instruction on the host, decode it to a file inside the container, then read it back with $(cat ...) for the --task argument. Base64 output is shell-safe (alphanumeric + /+=). This affected 491/550 OpenHands tasks in the overnight run (all scored 0.0 because the agent command never executed). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8da3d2c commit 1c9a6bd

File tree

1 file changed

+55
-4
lines changed

1 file changed

+55
-4
lines changed

agents/harnesses/openhands/agent.py

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
"""OpenHands harness agent wired to Harbor's OpenHands CLI with shared baseline tooling."""
22

3+
import base64
34
import json
45
import logging
56
import os
7+
import shlex
68

79
from harbor.agents import utils as harbor_utils
10+
from harbor.agents.installed.base import ExecInput
811
from harbor.environments.base import BaseEnvironment
912
from harbor.agents.installed.openhands import OpenHands
1013

@@ -86,6 +89,9 @@ def __init__(self, *args, **kwargs):
8689
"exit $_oh_rc"
8790
)
8891

92+
# Path inside the container where the instruction file is written.
93+
_TASK_FILE = "/tmp/oh_task_instruction.txt"
94+
8995
def create_run_agent_commands(self, instruction: str):
9096
instruction = self._resolve_instruction_text(instruction)
9197
instruction = self._prepare_instruction(instruction)
@@ -96,10 +102,55 @@ def create_run_agent_commands(self, instruction: str):
96102
):
97103
instruction = f"{self.OPENHANDS_WORKSPACE_PREAMBLE}\n\n{instruction}"
98104
self._save_instruction_artifact(instruction)
99-
exec_inputs = OpenHands.create_run_agent_commands(self, instruction)
100-
# Append daemon cleanup so Daytona session exits cleanly
101-
for ei in exec_inputs:
102-
ei.command = f"{{ {ei.command} }}{self._CLEANUP_SUFFIX}"
105+
106+
# --- Work around Harbor bug: shlex.quote() breaks on instructions
107+
# containing single quotes, backticks, etc. Instead of passing the
108+
# instruction as a shell-quoted CLI argument, we base64-encode it and
109+
# decode it inside the container to a temp file, then read it back
110+
# with $(cat ...). Base64 is shell-safe (no quotes to break). ---
111+
b64_instruction = base64.b64encode(instruction.encode()).decode()
112+
113+
# Build env dict the same way upstream OpenHands does, but skip the
114+
# broken --task= quoting. We call the parent's method to get env
115+
# setup, then replace the command that uses shlex.quote.
116+
upstream_inputs = OpenHands.create_run_agent_commands(self, instruction)
117+
118+
# The upstream returns 1-2 ExecInputs:
119+
# [0] (optional): MCP config.toml write
120+
# [-1]: the actual openhands.core.main command (the broken one)
121+
# We keep everything except the last command and rebuild it.
122+
env = upstream_inputs[-1].env or {}
123+
124+
exec_inputs = upstream_inputs[:-1] # keep MCP config setup if present
125+
126+
# Write instruction file via base64 decode (shell-safe, no quoting)
127+
exec_inputs.append(ExecInput(
128+
command=f"echo '{b64_instruction}' | base64 -d > {self._TASK_FILE}",
129+
env=env,
130+
))
131+
132+
# Build the openhands command reading task from file
133+
mcp_config = self._build_mcp_config_toml()
134+
config_file_path = "~/.openhands/config.toml"
135+
136+
commands = [
137+
"SANDBOX_VOLUMES=${PWD}:/workspace:rw",
138+
"/opt/openhands-venv/bin/python -m openhands.core.main",
139+
f'--task="$(cat {self._TASK_FILE})"',
140+
]
141+
if mcp_config:
142+
commands.append(f"--config-file={config_file_path}")
143+
144+
main_cmd = (
145+
" ".join(commands)
146+
+ " 2>&1 </dev/null | stdbuf -oL tee /logs/agent/openhands.txt"
147+
)
148+
149+
exec_inputs.append(ExecInput(
150+
command=f"{{ {main_cmd} }}{self._CLEANUP_SUFFIX}",
151+
env=env,
152+
))
153+
103154
return exec_inputs
104155

105156
def _build_workspace_guidance(self, workdir: str) -> str:

0 commit comments

Comments
 (0)