diff --git a/examples/quickstart/training/task_config_claude_code_openyuanrong.yaml b/examples/quickstart/training/task_config_claude_code_openyuanrong.yaml new file mode 100644 index 000000000..56a952bc0 --- /dev/null +++ b/examples/quickstart/training/task_config_claude_code_openyuanrong.yaml @@ -0,0 +1,58 @@ +# Claude Code tasks running in an OpenYuanrong sandbox. The task image is +# remapped to the internal registry and the prebuilt Claude Code sidecar is +# mounted at /opt/claude-code. Its bin directory is placed on the sandbox PATH. +- name: swe_bench + sandbox: + provider: openyuanrong + runtime_timeout: 7200 + image_map: + - from: "swebench/**" + to: "swr.cn-east-3.myhuaweicloud.com/openyuanrong/swe-bench-verified/**:v2" + sandbox_kwargs: + env: + PATH: /opt/claude-code/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + proxy_port: 38197 + mounts: + - target: /opt/claude-code + image_url: swr.cn-east-3.myhuaweicloud.com/openyuanrong/claude-code-tool:latest + agent: + name: claude_code + max_turns: 200 + run_timeout: 4800 + model: + temperature: 1.0 + top_p: 1.0 + max_total_tokens: 131072 + prompt_template: &swe_claude_prompt + - role: user + content: |- + Read the following task description and resolve the issue in the current directory. + + Task description: + {problem_statement} + + Inspect the relevant code, make the minimal correct changes, and verify the result with appropriate tests or checks. Do not modify tests or commit changes. When finished, briefly summarize what changed and how it was verified. + +- name: swe_rebench + sandbox: + provider: openyuanrong + runtime_timeout: 7200 + image_map: + - from: "swerebench/sweb.eval.x86_64.**" + to: "swr.cn-east-3.myhuaweicloud.com/openyuanrong/swe-rebench/**:latest" + sandbox_kwargs: + env: + PATH: /opt/claude-code/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + proxy_port: 38197 + mounts: + - target: /opt/claude-code + image_url: swr.cn-east-3.myhuaweicloud.com/openyuanrong/claude-code-tool:latest + agent: + name: claude_code + max_turns: 200 + run_timeout: 4800 + model: + temperature: 1.0 + top_p: 1.0 + max_total_tokens: 131072 + prompt_template: *swe_claude_prompt diff --git a/tests/uni_agent/sandbox/test_exec_error_policy.py b/tests/uni_agent/sandbox/test_exec_error_policy.py index fc92744f7..a0ec501a0 100644 --- a/tests/uni_agent/sandbox/test_exec_error_policy.py +++ b/tests/uni_agent/sandbox/test_exec_error_policy.py @@ -19,6 +19,7 @@ from __future__ import annotations import asyncio +from types import SimpleNamespace import pytest @@ -240,6 +241,69 @@ def test_openyuanrong_recognizes_its_timeout(): assert sb._is_timeout_error(RuntimeError("other")) is False +@pytest.mark.cpu +@pytest.mark.level0 +def test_openyuanrong_exec_merges_sandbox_and_command_environments(): + from uni_agent.sandbox.openyuanrong import OpenyuanrongSandbox + + class _Commands: + def __init__(self): + self.call = None + + def run(self, command, *, envs, cwd, timeout): + self.call = {"command": command, "envs": envs, "cwd": cwd, "timeout": timeout} + return SimpleNamespace(exit_code=0, stdout="ok", stderr="") + + commands = _Commands() + sb = OpenyuanrongSandbox( + image="python:3.12", + env={"PATH": "/opt/claude-code/bin:/usr/bin", "SHARED": "sandbox"}, + ) + sb._sandbox = SimpleNamespace(commands=commands) + + result = asyncio.run( + sb._exec( + ["claude", "--version"], + timeout=42, + workdir="/testbed", + env={"ANTHROPIC_BASE_URL": "http://gateway", "SHARED": "command"}, + ) + ) + + assert result == ExecResult(exit_code=0, stdout="ok", stderr="") + assert commands.call == { + "command": "claude --version", + "envs": { + "PATH": "/opt/claude-code/bin:/usr/bin", + "SHARED": "command", + "ANTHROPIC_BASE_URL": "http://gateway", + }, + "cwd": "/testbed", + "timeout": 42, + } + + +@pytest.mark.cpu +@pytest.mark.level0 +def test_openyuanrong_exec_preserves_nonzero_status_and_output(): + from uni_agent.sandbox.openyuanrong import OpenyuanrongSandbox + + class _Commands: + def run(self, command, *, envs, cwd, timeout): + assert command == "claude -p fix" + assert envs is None + assert cwd == "/testbed" + assert timeout == 60 + return SimpleNamespace(exit_code=1, stdout="partial output", stderr="Claude Code failed") + + sb = OpenyuanrongSandbox(image="python:3.12") + sb._sandbox = SimpleNamespace(commands=_Commands()) + + result = asyncio.run(sb._exec(["claude", "-p", "fix"], timeout=60, workdir="/testbed")) + + assert result == ExecResult(exit_code=1, stdout="partial output", stderr="Claude Code failed") + + # --------------------------- provider is_alive() liveness --------------------------- diff --git a/uni_agent/sandbox/openyuanrong.py b/uni_agent/sandbox/openyuanrong.py index 44edc4aa9..188f34542 100644 --- a/uni_agent/sandbox/openyuanrong.py +++ b/uni_agent/sandbox/openyuanrong.py @@ -261,11 +261,26 @@ async def _exec( """Run ``argv`` once via akernel ``Commands.run``.""" sb = self._require() timeout_i = int(timeout) if timeout else 60 + # ``Sandbox(env=...)`` configures the created container, but the + # akernel Commands API does not inherit those values for individual + # commands. Replay the sandbox defaults here so a sidecar-mounted + # executable (for example /opt/claude-code/bin/claude) is on PATH. + # Per-command values take precedence, matching the other providers. + command_env = dict(self.env or {}) + if env: + command_env.update(env) # commands.run is a blocking SDK poll; run it off the event loop. - result = await asyncio.to_thread(sb.commands.run, shlex.join(argv), envs=env, cwd=workdir, timeout=timeout_i) + result = await asyncio.to_thread( + sb.commands.run, + shlex.join(argv), + envs=command_env or None, + cwd=workdir, + timeout=timeout_i, + ) exit_code = int(result.exit_code) stdout = _to_str(getattr(result, "stdout", "")) stderr = _to_str(getattr(result, "stderr", "")) - if exit_code != 0: - raise RuntimeError(stderr or f"command exited with {exit_code}") - return ExecResult(exit_code=0, stdout=stdout, stderr=stderr) + # A command failure is a normal data-plane result. Returning it keeps + # the original status and output rather than turning it into Sandbox + # exec()'s generic alive-sandbox error (exit code 127). + return ExecResult(exit_code=exit_code, stdout=stdout, stderr=stderr)