Skip to content

Commit fa35ef4

Browse files
sjarmakclaude
andcommitted
fix: move OpenHands command entirely into Python launcher
The shell command for running OpenHands still had shell metacharacters (braces, pipes, single quotes in pkill patterns) that broke under Daytona's double-wrapping (session shell -> bash -c -> shlex.quote). Now the entire execution — subprocess launch, output tee, and daemon cleanup — lives in a Python script. The exec command is just "/opt/openhands-venv/bin/python /tmp/oh_launcher.py" with zero shell metacharacters. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 969f29a commit fa35ef4

File tree

1 file changed

+22
-14
lines changed

1 file changed

+22
-14
lines changed

agents/harnesses/openhands/agent.py

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -129,22 +129,36 @@ def create_run_agent_commands(self, instruction: str):
129129
env=env,
130130
))
131131

132-
# Build a Python launcher script that reads the task from file and
133-
# passes it as a proper argv element to openhands.core.main.
134-
# This completely bypasses shell quoting/expansion issues.
132+
# Build a Python launcher script that reads the task from file,
133+
# runs openhands.core.main, pipes output to a log file, and cleans up
134+
# orphan daemons. Everything stays in Python — no shell quoting at all.
135135
mcp_config = self._build_mcp_config_toml()
136136
config_file_path = "~/.openhands/config.toml"
137137

138138
launcher_lines = [
139-
"import sys, subprocess",
139+
"import os, signal, subprocess, sys",
140+
f"os.environ['SANDBOX_VOLUMES'] = os.getcwd() + ':/workspace:rw'",
140141
f"task = open('{self._TASK_FILE}').read()",
141142
"cmd = [sys.executable, '-m', 'openhands.core.main', '--task=' + task]",
142143
]
143144
if mcp_config:
144145
launcher_lines.append(f"cmd.append('--config-file={config_file_path}')")
145-
launcher_lines.append("sys.exit(subprocess.call(cmd, stdin=subprocess.DEVNULL))")
146-
147-
launcher_script = "; ".join(launcher_lines)
146+
launcher_lines.extend([
147+
"log = open('/logs/agent/openhands.txt', 'wb')",
148+
"proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL)",
149+
"for line in proc.stdout:",
150+
" sys.stdout.buffer.write(line)",
151+
" sys.stdout.buffer.flush()",
152+
" log.write(line)",
153+
"log.close()",
154+
"rc = proc.wait()",
155+
# Kill known OpenHands daemons that outlive the main process
156+
"for pat in ['jupyter-kernelgateway', 'ipykernel_launcher', 'openhands.runtime.action_execution_server', 'tmux']:",
157+
" subprocess.run(['pkill', '-f', pat], capture_output=True)",
158+
"sys.exit(rc)",
159+
])
160+
161+
launcher_script = "\n".join(launcher_lines)
148162
launcher_b64 = base64.b64encode(launcher_script.encode()).decode()
149163

150164
# Write launcher, then execute it — two separate commands to avoid
@@ -155,14 +169,8 @@ def create_run_agent_commands(self, instruction: str):
155169
env=env,
156170
))
157171

158-
main_cmd = (
159-
f"SANDBOX_VOLUMES=${{PWD}}:/workspace:rw "
160-
f"/opt/openhands-venv/bin/python {launcher_path}"
161-
" 2>&1 | stdbuf -oL tee /logs/agent/openhands.txt"
162-
)
163-
164172
exec_inputs.append(ExecInput(
165-
command=f"{{ {main_cmd} }}{self._CLEANUP_SUFFIX}",
173+
command=f"/opt/openhands-venv/bin/python {launcher_path}",
166174
env=env,
167175
))
168176

0 commit comments

Comments
 (0)