openreward 0.1.95.dev0__tar.gz → 0.1.96.dev1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/PKG-INFO +1 -1
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/api/environments/client.py +2 -2
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/toolsets/__init__.py +6 -6
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/toolsets/hermes.py +113 -14
- openreward-0.1.96.dev1/openreward/toolsets/openclaw.py +369 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward.egg-info/PKG-INFO +1 -1
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/pyproject.toml +1 -1
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/tests/test_session_toolset.py +32 -10
- openreward-0.1.95.dev0/openreward/toolsets/openclaw.py +0 -222
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/README.md +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/__init__.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/_version.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/api/__init__.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/api/_session/__init__.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/api/_session/http.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/api/_session/ping.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/api/_session/session.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/api/environments/__init__.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/api/environments/types.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/api/rollouts/__init__.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/api/rollouts/background.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/api/rollouts/rollout.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/api/rollouts/serializers/__init__.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/api/rollouts/serializers/ant.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/api/rollouts/serializers/base.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/api/rollouts/serializers/gdm.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/api/rollouts/serializers/models.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/api/rollouts/serializers/oai_completions.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/api/rollouts/serializers/oai_responses.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/api/rollouts/serializers/utils.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/api/sandboxes/__init__.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/api/sandboxes/client.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/api/sandboxes/secrets.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/api/sandboxes/types.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/cli.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/client.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/environments/__init__.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/environments/environment.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/environments/reconnect.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/environments/server.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/environments/session.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/environments/toolset.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/environments/types.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/environments/utils.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/http_client.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/log_utils.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/models.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/templates/__init__.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/templates/basic/Dockerfile +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/templates/basic/__init__.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/templates/basic/requirements.txt +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/templates/basic/requirements.txt.tmpl +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/templates/basic/server.py.tmpl +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/templates/sandbox/Dockerfile +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/templates/sandbox/__init__.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/templates/sandbox/requirements.txt +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/templates/sandbox/sandbox_env.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/templates/sandbox/server.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/toolsets/claude_code.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/toolsets/codex.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/toolsets/excel.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/toolsets/pdf.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/toolsets/powerpoint.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/toolsets/word.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward.egg-info/SOURCES.txt +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward.egg-info/dependency_links.txt +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward.egg-info/entry_points.txt +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward.egg-info/requires.txt +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward.egg-info/top_level.txt +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/setup.cfg +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/tests/test_environment.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/tests/test_errors.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/tests/test_rollout_info.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/tests/test_sanitise.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/tests/test_session.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/tests/test_tool_conversion.py +0 -0
- {openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/tests/test_toolsets.py +0 -0
|
@@ -12,8 +12,8 @@ from openreward.api._session.http import (
|
|
|
12
12
|
)
|
|
13
13
|
from openreward.api._session.session import BaseAsyncSession, SessionTerminatedError
|
|
14
14
|
|
|
15
|
-
BuiltinToolset = Literal["claude-code", "codex", "openclaw", "hermes"]
|
|
16
|
-
_VALID_BUILTIN_TOOLSETS = {"claude-code", "codex", "openclaw", "hermes"}
|
|
15
|
+
BuiltinToolset = Literal["claude-code", "codex", "openclaw-sandboxed", "hermes-sandboxed"]
|
|
16
|
+
_VALID_BUILTIN_TOOLSETS = {"claude-code", "codex", "openclaw-sandboxed", "hermes-sandboxed"}
|
|
17
17
|
from .types import (
|
|
18
18
|
ImageBlock,
|
|
19
19
|
JSONObject,
|
|
@@ -5,8 +5,8 @@ from openreward.environments.toolset import Toolset
|
|
|
5
5
|
from .claude_code import ClaudeCodeToolset
|
|
6
6
|
from .codex import CodexToolset
|
|
7
7
|
from .excel import ExcelToolset
|
|
8
|
-
from .hermes import
|
|
9
|
-
from .openclaw import
|
|
8
|
+
from .hermes import HermesSandboxedToolset
|
|
9
|
+
from .openclaw import OpenClawSandboxedToolset
|
|
10
10
|
from .pdf import PDFToolset
|
|
11
11
|
from .powerpoint import PowerPointToolset
|
|
12
12
|
from .word import WordToolset
|
|
@@ -18,8 +18,8 @@ from .word import WordToolset
|
|
|
18
18
|
BUILTIN_TOOLSETS: dict[str, type[Toolset]] = {
|
|
19
19
|
ClaudeCodeToolset.name(): ClaudeCodeToolset,
|
|
20
20
|
CodexToolset.name(): CodexToolset,
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
OpenClawSandboxedToolset.name(): OpenClawSandboxedToolset,
|
|
22
|
+
HermesSandboxedToolset.name(): HermesSandboxedToolset,
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
__all__ = [
|
|
@@ -27,8 +27,8 @@ __all__ = [
|
|
|
27
27
|
"ClaudeCodeToolset",
|
|
28
28
|
"CodexToolset",
|
|
29
29
|
"ExcelToolset",
|
|
30
|
-
"
|
|
31
|
-
"
|
|
30
|
+
"HermesSandboxedToolset",
|
|
31
|
+
"OpenClawSandboxedToolset",
|
|
32
32
|
"PowerPointToolset",
|
|
33
33
|
"WordToolset",
|
|
34
34
|
"PDFToolset",
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
-
"""Hermes Agent session toolset.
|
|
1
|
+
"""Hermes Agent sandboxed session toolset.
|
|
2
2
|
|
|
3
|
-
Provides the
|
|
4
|
-
(``terminal``, ``read_file``, ``write_file``, ``patch``),
|
|
5
|
-
``self.sandbox`` from the bound environment.
|
|
3
|
+
Provides the five built-in tools that Hermes Agent exposes for coding tasks
|
|
4
|
+
(``terminal``, ``read_file``, ``write_file``, ``search_files``, ``patch``),
|
|
5
|
+
each backed by ``self.sandbox`` from the bound environment. All file and
|
|
6
|
+
shell operations are routed through the sandbox so the agent runs in an
|
|
7
|
+
isolated environment.
|
|
8
|
+
|
|
9
|
+
For the non-sandboxed variant (``hermes``), Hermes uses its own native tools
|
|
10
|
+
and only environment-specific tools (e.g. ``submit_answer``) are exposed via
|
|
11
|
+
MCP — no SDK toolset is needed.
|
|
6
12
|
|
|
7
13
|
Tool names, parameter schemas, and descriptions match Hermes Agent's upstream
|
|
8
14
|
registry definitions (``nousresearch/hermes-agent``).
|
|
@@ -11,7 +17,7 @@ from __future__ import annotations
|
|
|
11
17
|
|
|
12
18
|
import base64
|
|
13
19
|
import os
|
|
14
|
-
from typing import Any, Optional
|
|
20
|
+
from typing import Any, List, Optional
|
|
15
21
|
|
|
16
22
|
from pydantic import BaseModel
|
|
17
23
|
|
|
@@ -57,6 +63,17 @@ class WriteFileParams(BaseModel, extra="forbid"):
|
|
|
57
63
|
content: str
|
|
58
64
|
|
|
59
65
|
|
|
66
|
+
class SearchFilesParams(BaseModel, extra="forbid"):
|
|
67
|
+
pattern: str
|
|
68
|
+
target: str = "content"
|
|
69
|
+
path: str = "."
|
|
70
|
+
file_glob: Optional[str] = None
|
|
71
|
+
limit: int = 50
|
|
72
|
+
offset: int = 0
|
|
73
|
+
output_mode: str = "content"
|
|
74
|
+
context: int = 0
|
|
75
|
+
|
|
76
|
+
|
|
60
77
|
class PatchParams(BaseModel, extra="forbid"):
|
|
61
78
|
mode: str = "replace"
|
|
62
79
|
path: Optional[str] = None
|
|
@@ -66,13 +83,14 @@ class PatchParams(BaseModel, extra="forbid"):
|
|
|
66
83
|
patch: Optional[str] = None
|
|
67
84
|
|
|
68
85
|
|
|
69
|
-
# ── Tool descriptions (matching Hermes registry
|
|
86
|
+
# ── Tool descriptions (matching Hermes upstream registry) ──
|
|
70
87
|
|
|
71
88
|
TERMINAL_DESCRIPTION = """\
|
|
72
89
|
Execute shell commands on a Linux environment. Filesystem usually persists between calls.
|
|
73
90
|
|
|
74
91
|
Do NOT use cat/head/tail to read files — use read_file instead.
|
|
75
92
|
Do NOT use grep/rg/find to search — use search_files instead.
|
|
93
|
+
Do NOT use ls to list directories — use search_files(target='files') instead.
|
|
76
94
|
Do NOT use sed/awk to edit files — use patch instead.
|
|
77
95
|
Do NOT use echo/cat heredoc to create files — use write_file instead.
|
|
78
96
|
Reserve terminal for: builds, installs, git, processes, scripts, network, package managers, and anything that needs a shell.
|
|
@@ -89,6 +107,17 @@ Write content to a file, completely replacing existing content. Use this instead
|
|
|
89
107
|
in terminal. Creates parent directories automatically. OVERWRITES the entire file — use 'patch' for \
|
|
90
108
|
targeted edits."""
|
|
91
109
|
|
|
110
|
+
SEARCH_FILES_DESCRIPTION = """\
|
|
111
|
+
Search file contents or find files by name using regex/glob patterns. Use this instead of \
|
|
112
|
+
grep/rg/find/ls in terminal.
|
|
113
|
+
|
|
114
|
+
target='content' (default): search inside file contents with regex. Returns matching lines with \
|
|
115
|
+
line numbers and optional context.
|
|
116
|
+
target='files': search for files by name/glob pattern. Returns matching file paths.
|
|
117
|
+
|
|
118
|
+
Use file_glob to filter which files to search (e.g., '*.py'). Use output_mode to control \
|
|
119
|
+
output format: 'content' (matching lines), 'files_only' (file paths), 'count' (match counts)."""
|
|
120
|
+
|
|
92
121
|
PATCH_DESCRIPTION = """\
|
|
93
122
|
Targeted find-and-replace edits in files. Use this instead of sed/awk in terminal.
|
|
94
123
|
|
|
@@ -101,12 +130,12 @@ Include enough surrounding context to ensure uniqueness."""
|
|
|
101
130
|
|
|
102
131
|
# ── Toolset ──
|
|
103
132
|
|
|
104
|
-
class
|
|
105
|
-
"""
|
|
133
|
+
class HermesSandboxedToolset(Toolset):
|
|
134
|
+
"""Sandboxed session toolset exposing the Hermes Agent five-tool coding surface.
|
|
106
135
|
|
|
107
136
|
The toolset is bound to a session by passing it to ``env.session(...)``::
|
|
108
137
|
|
|
109
|
-
with env.session(task=task, toolset="hermes") as session:
|
|
138
|
+
with env.session(task=task, toolset="hermes-sandboxed") as session:
|
|
110
139
|
session.call_tool("terminal", {"command": "ls"})
|
|
111
140
|
|
|
112
141
|
Requires the bound environment to define ``self.sandbox``.
|
|
@@ -114,7 +143,7 @@ class HermesToolset(Toolset):
|
|
|
114
143
|
|
|
115
144
|
@classmethod
|
|
116
145
|
def name(cls) -> str:
|
|
117
|
-
return "hermes"
|
|
146
|
+
return "hermes-sandboxed"
|
|
118
147
|
|
|
119
148
|
@tool
|
|
120
149
|
async def terminal(self, params: TerminalParams) -> ToolOutput:
|
|
@@ -188,6 +217,75 @@ class HermesToolset(Toolset):
|
|
|
188
217
|
finished=False,
|
|
189
218
|
)
|
|
190
219
|
|
|
220
|
+
@tool
|
|
221
|
+
async def search_files(self, params: SearchFilesParams) -> ToolOutput:
|
|
222
|
+
try:
|
|
223
|
+
if params.target == "files":
|
|
224
|
+
# File name search using find
|
|
225
|
+
cmd = f"find {params.path} -type f -name '{params.pattern}'"
|
|
226
|
+
output, code = await self.sandbox.run(cmd)
|
|
227
|
+
if code != 0:
|
|
228
|
+
return ToolOutput(
|
|
229
|
+
metadata={"error": output, "exit_code": code},
|
|
230
|
+
blocks=[TextBlock(text=f"search_files failed (exit {code}):\n{output}")],
|
|
231
|
+
finished=False,
|
|
232
|
+
)
|
|
233
|
+
# Apply offset/limit to results
|
|
234
|
+
lines = [l for l in output.splitlines() if l.strip()]
|
|
235
|
+
lines = lines[params.offset:params.offset + params.limit]
|
|
236
|
+
result = "\n".join(lines)
|
|
237
|
+
return ToolOutput(
|
|
238
|
+
metadata={"output": result, "exit_code": 0},
|
|
239
|
+
blocks=[TextBlock(text=result if result else "No files found.")],
|
|
240
|
+
reward=0.0,
|
|
241
|
+
finished=False,
|
|
242
|
+
)
|
|
243
|
+
else:
|
|
244
|
+
# Content search using grep
|
|
245
|
+
glob_flag = ""
|
|
246
|
+
if params.file_glob:
|
|
247
|
+
glob_flag = f" --include='{params.file_glob}'"
|
|
248
|
+
|
|
249
|
+
context_flag = ""
|
|
250
|
+
if params.context > 0:
|
|
251
|
+
context_flag = f" -C {params.context}"
|
|
252
|
+
|
|
253
|
+
if params.output_mode == "files_only":
|
|
254
|
+
mode_flag = " -l"
|
|
255
|
+
elif params.output_mode == "count":
|
|
256
|
+
mode_flag = " -c"
|
|
257
|
+
else:
|
|
258
|
+
mode_flag = " -n"
|
|
259
|
+
|
|
260
|
+
cmd = f"grep -r{mode_flag}{context_flag}{glob_flag} '{params.pattern}' {params.path}"
|
|
261
|
+
output, code = await self.sandbox.run(cmd)
|
|
262
|
+
|
|
263
|
+
# grep returns 1 for no matches — not an error
|
|
264
|
+
if code > 1:
|
|
265
|
+
return ToolOutput(
|
|
266
|
+
metadata={"error": output, "exit_code": code},
|
|
267
|
+
blocks=[TextBlock(text=f"search_files failed (exit {code}):\n{output}")],
|
|
268
|
+
finished=False,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
# Apply offset/limit to results
|
|
272
|
+
lines = output.splitlines()
|
|
273
|
+
lines = lines[params.offset:params.offset + params.limit]
|
|
274
|
+
result = "\n".join(lines)
|
|
275
|
+
|
|
276
|
+
return ToolOutput(
|
|
277
|
+
metadata={"output": result, "exit_code": 0},
|
|
278
|
+
blocks=[TextBlock(text=result if result else "No matches found.")],
|
|
279
|
+
reward=0.0,
|
|
280
|
+
finished=False,
|
|
281
|
+
)
|
|
282
|
+
except Exception as e:
|
|
283
|
+
return ToolOutput(
|
|
284
|
+
metadata={"error": str(e)},
|
|
285
|
+
blocks=[TextBlock(text=f"Error searching files: {str(e)}")],
|
|
286
|
+
finished=False,
|
|
287
|
+
)
|
|
288
|
+
|
|
191
289
|
@tool
|
|
192
290
|
async def patch(self, params: PatchParams) -> ToolOutput:
|
|
193
291
|
if params.mode == "replace":
|
|
@@ -289,7 +387,8 @@ class HermesToolset(Toolset):
|
|
|
289
387
|
|
|
290
388
|
# Assign descriptions onto each tool method's __doc__ so the framework's
|
|
291
389
|
# introspection picks them up.
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
390
|
+
HermesSandboxedToolset.terminal.__doc__ = TERMINAL_DESCRIPTION
|
|
391
|
+
HermesSandboxedToolset.read_file.__doc__ = READ_FILE_DESCRIPTION
|
|
392
|
+
HermesSandboxedToolset.write_file.__doc__ = WRITE_FILE_DESCRIPTION
|
|
393
|
+
HermesSandboxedToolset.search_files.__doc__ = SEARCH_FILES_DESCRIPTION
|
|
394
|
+
HermesSandboxedToolset.patch.__doc__ = PATCH_DESCRIPTION
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
"""OpenClaw sandboxed session toolset.
|
|
2
|
+
|
|
3
|
+
Provides the built-in tools that OpenClaw exposes for coding tasks
|
|
4
|
+
(``exec``, ``process``, ``read``, ``write``, ``edit``, ``apply_patch``),
|
|
5
|
+
each backed by
|
|
6
|
+
``self.sandbox`` from the bound environment. All file and shell operations
|
|
7
|
+
are routed through the sandbox so the agent runs in an isolated environment.
|
|
8
|
+
|
|
9
|
+
For the non-sandboxed variant (``openclaw``), OpenClaw uses its own native
|
|
10
|
+
tools and only environment-specific tools (e.g. ``submit_answer``) are
|
|
11
|
+
exposed via MCP — no SDK toolset is needed.
|
|
12
|
+
|
|
13
|
+
Tool names, parameter schemas, and descriptions match OpenClaw's upstream
|
|
14
|
+
definitions.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import base64
|
|
19
|
+
import os
|
|
20
|
+
from typing import Any, List, Optional
|
|
21
|
+
|
|
22
|
+
from pydantic import BaseModel, Field
|
|
23
|
+
|
|
24
|
+
from openreward.environments.environment import tool
|
|
25
|
+
from openreward.environments.toolset import Toolset
|
|
26
|
+
from openreward.environments.types import TextBlock, ToolOutput
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ── Sandbox text helpers (inlined; same as claude_code.py) ──
|
|
30
|
+
|
|
31
|
+
async def _download_text(sandbox: Any, path: str) -> str:
|
|
32
|
+
data = await sandbox.download(path)
|
|
33
|
+
return data.decode("utf-8")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def _upload_text(
|
|
37
|
+
sandbox: Any,
|
|
38
|
+
path: str,
|
|
39
|
+
content: str,
|
|
40
|
+
ensure_trailing_newline: bool = True,
|
|
41
|
+
) -> None:
|
|
42
|
+
if ensure_trailing_newline and not content.endswith("\n"):
|
|
43
|
+
content = content + "\n"
|
|
44
|
+
encoded = base64.b64encode(content.encode("utf-8")).decode("ascii")
|
|
45
|
+
await sandbox.check_run(f"echo '{encoded}' | base64 -d > {path}")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ── Pydantic parameter models ──
|
|
49
|
+
|
|
50
|
+
class ExecParams(BaseModel, extra="forbid"):
|
|
51
|
+
command: str
|
|
52
|
+
timeout: Optional[float] = 1800.0
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ReadParams(BaseModel, extra="forbid"):
|
|
56
|
+
path: str
|
|
57
|
+
offset: Optional[int] = None
|
|
58
|
+
limit: Optional[int] = None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class WriteParams(BaseModel, extra="forbid"):
|
|
62
|
+
path: str
|
|
63
|
+
content: str
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class EditItem(BaseModel, extra="forbid"):
|
|
67
|
+
oldText: str
|
|
68
|
+
newText: str
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class EditParams(BaseModel, extra="forbid"):
|
|
72
|
+
path: str
|
|
73
|
+
edits: List[EditItem]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class ApplyPatchParams(BaseModel, extra="forbid"):
|
|
77
|
+
input: str
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class ProcessParams(BaseModel, extra="forbid"):
|
|
81
|
+
action: str
|
|
82
|
+
sessionId: Optional[str] = None
|
|
83
|
+
data: Optional[str] = None
|
|
84
|
+
eof: Optional[bool] = None
|
|
85
|
+
offset: Optional[int] = None
|
|
86
|
+
limit: Optional[int] = None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# ── Tool descriptions (matching OpenClaw upstream) ──
|
|
90
|
+
|
|
91
|
+
EXEC_DESCRIPTION = """\
|
|
92
|
+
Execute a shell command and return its output and exit code. \
|
|
93
|
+
Set timeout in seconds (default 1800). \
|
|
94
|
+
Use for builds, installs, git, processes, scripts, network, package managers, \
|
|
95
|
+
and anything that needs a shell."""
|
|
96
|
+
|
|
97
|
+
READ_DESCRIPTION = """\
|
|
98
|
+
Read the contents of a file at the given path. \
|
|
99
|
+
Supports optional offset (1-indexed line number to start from) and limit \
|
|
100
|
+
(maximum number of lines to read) for pagination of large files."""
|
|
101
|
+
|
|
102
|
+
WRITE_DESCRIPTION = """\
|
|
103
|
+
Write content to a file, creating it if it doesn't exist or overwriting if it does. \
|
|
104
|
+
Creates parent directories automatically. \
|
|
105
|
+
Use the edit tool for targeted modifications to existing files."""
|
|
106
|
+
|
|
107
|
+
EDIT_DESCRIPTION = """\
|
|
108
|
+
Apply one or more targeted text replacements to a file. \
|
|
109
|
+
Each edit specifies an oldText to find and a newText to replace it with. \
|
|
110
|
+
oldText must be unique in the file for each edit."""
|
|
111
|
+
|
|
112
|
+
PROCESS_DESCRIPTION = """\
|
|
113
|
+
Manage background processes. Use exec with background=true to start a process, \
|
|
114
|
+
then use this tool to interact with it.
|
|
115
|
+
|
|
116
|
+
Actions:
|
|
117
|
+
- list: show running and finished background sessions
|
|
118
|
+
- poll: check for new output from a session (requires sessionId)
|
|
119
|
+
- log: read session output with optional offset/limit pagination (requires sessionId)
|
|
120
|
+
- write: send data to a session's stdin (requires sessionId and data)
|
|
121
|
+
- kill: terminate a background session (requires sessionId)
|
|
122
|
+
- remove: kill if running, clear if finished (requires sessionId)"""
|
|
123
|
+
|
|
124
|
+
APPLY_PATCH_DESCRIPTION = """\
|
|
125
|
+
Apply file modifications using a structured patch format, designed for multiple file \
|
|
126
|
+
or multi-hunk edits where individual edit calls would be fragile.
|
|
127
|
+
|
|
128
|
+
The input must include '*** Begin Patch' and '*** End Patch' markers. Supported \
|
|
129
|
+
operations: '*** Add File:', '*** Update File:' (with optional '*** Move to:'), \
|
|
130
|
+
'*** Delete File:', and '*** End of File' for EOF-only insertions."""
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# ── Toolset ──
|
|
134
|
+
|
|
135
|
+
class OpenClawSandboxedToolset(Toolset):
|
|
136
|
+
"""Sandboxed session toolset exposing the OpenClaw six-tool coding surface.
|
|
137
|
+
|
|
138
|
+
The toolset is bound to a session by passing it to ``env.session(...)``::
|
|
139
|
+
|
|
140
|
+
with env.session(task=task, toolset="openclaw-sandboxed") as session:
|
|
141
|
+
session.call_tool("exec", {"command": "ls"})
|
|
142
|
+
|
|
143
|
+
Requires the bound environment to define ``self.sandbox``.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
@classmethod
|
|
147
|
+
def name(cls) -> str:
|
|
148
|
+
return "openclaw-sandboxed"
|
|
149
|
+
|
|
150
|
+
@tool
|
|
151
|
+
async def exec(self, params: ExecParams) -> ToolOutput:
|
|
152
|
+
try:
|
|
153
|
+
output, code = await self.sandbox.run(params.command.strip())
|
|
154
|
+
return ToolOutput(
|
|
155
|
+
blocks=[TextBlock(text=f"{output}\n\n(exit {code})")],
|
|
156
|
+
metadata={"output": output, "exit_code": code},
|
|
157
|
+
reward=0.0,
|
|
158
|
+
finished=False,
|
|
159
|
+
)
|
|
160
|
+
except Exception as e:
|
|
161
|
+
return ToolOutput(
|
|
162
|
+
metadata={"error": str(e)},
|
|
163
|
+
blocks=[TextBlock(text=f"Error executing command: {str(e)}")],
|
|
164
|
+
finished=False,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
@tool
|
|
168
|
+
async def process(self, params: ProcessParams) -> ToolOutput:
|
|
169
|
+
try:
|
|
170
|
+
action = params.action
|
|
171
|
+
sid = params.sessionId or ""
|
|
172
|
+
|
|
173
|
+
if action == "list":
|
|
174
|
+
output, code = await self.sandbox.run("ps aux --no-headers 2>/dev/null || ps aux")
|
|
175
|
+
return ToolOutput(
|
|
176
|
+
blocks=[TextBlock(text=output if output.strip() else "No background processes.")],
|
|
177
|
+
metadata={"output": output, "exit_code": code},
|
|
178
|
+
reward=0.0,
|
|
179
|
+
finished=False,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
if not sid:
|
|
183
|
+
return ToolOutput(
|
|
184
|
+
metadata={"error": "sessionId is required for this action"},
|
|
185
|
+
blocks=[TextBlock(text=f"Error: sessionId is required for action '{action}'.")],
|
|
186
|
+
finished=False,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
if action in ("poll", "log"):
|
|
190
|
+
# Read output from a background process log file
|
|
191
|
+
tail_n = params.limit or 200
|
|
192
|
+
cmd = f"cat /tmp/_oc_proc_{sid}.log 2>/dev/null || echo 'No output available for session {sid}'"
|
|
193
|
+
if params.offset is not None:
|
|
194
|
+
cmd = f"tail -n +{params.offset} /tmp/_oc_proc_{sid}.log 2>/dev/null | head -n {tail_n}"
|
|
195
|
+
elif params.limit:
|
|
196
|
+
cmd = f"tail -n {tail_n} /tmp/_oc_proc_{sid}.log 2>/dev/null"
|
|
197
|
+
output, code = await self.sandbox.run(cmd)
|
|
198
|
+
return ToolOutput(
|
|
199
|
+
blocks=[TextBlock(text=output)],
|
|
200
|
+
metadata={"output": output, "exit_code": code},
|
|
201
|
+
reward=0.0,
|
|
202
|
+
finished=False,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
if action == "write":
|
|
206
|
+
data = params.data or ""
|
|
207
|
+
# Write data to the process's stdin via a named pipe or file
|
|
208
|
+
output, code = await self.sandbox.run(
|
|
209
|
+
f"echo '{data}' >> /tmp/_oc_proc_{sid}.stdin 2>/dev/null"
|
|
210
|
+
)
|
|
211
|
+
return ToolOutput(
|
|
212
|
+
blocks=[TextBlock(text=f"Sent data to session {sid}")],
|
|
213
|
+
metadata={"output": output, "exit_code": code},
|
|
214
|
+
reward=0.0,
|
|
215
|
+
finished=False,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
if action in ("kill", "remove"):
|
|
219
|
+
output, code = await self.sandbox.run(
|
|
220
|
+
f"kill $(cat /tmp/_oc_proc_{sid}.pid 2>/dev/null) 2>/dev/null; "
|
|
221
|
+
f"rm -f /tmp/_oc_proc_{sid}.pid /tmp/_oc_proc_{sid}.log /tmp/_oc_proc_{sid}.stdin"
|
|
222
|
+
)
|
|
223
|
+
return ToolOutput(
|
|
224
|
+
blocks=[TextBlock(text=f"Session {sid} terminated and cleaned up.")],
|
|
225
|
+
metadata={"output": output, "exit_code": code},
|
|
226
|
+
reward=0.0,
|
|
227
|
+
finished=False,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
return ToolOutput(
|
|
231
|
+
metadata={"error": f"Unknown action: {action}"},
|
|
232
|
+
blocks=[TextBlock(text=f"Error: unknown process action '{action}'. "
|
|
233
|
+
"Use list, poll, log, write, kill, or remove.")],
|
|
234
|
+
finished=False,
|
|
235
|
+
)
|
|
236
|
+
except Exception as e:
|
|
237
|
+
return ToolOutput(
|
|
238
|
+
metadata={"error": str(e)},
|
|
239
|
+
blocks=[TextBlock(text=f"Error managing process: {str(e)}")],
|
|
240
|
+
finished=False,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
@tool
|
|
244
|
+
async def read(self, params: ReadParams) -> ToolOutput:
|
|
245
|
+
try:
|
|
246
|
+
content = await _download_text(self.sandbox, params.path)
|
|
247
|
+
lines = content.splitlines()
|
|
248
|
+
|
|
249
|
+
if params.offset is not None or params.limit is not None:
|
|
250
|
+
start = (params.offset or 1) - 1 # Convert 1-indexed to 0-indexed
|
|
251
|
+
if params.limit is not None:
|
|
252
|
+
lines = lines[start:start + params.limit]
|
|
253
|
+
else:
|
|
254
|
+
lines = lines[start:]
|
|
255
|
+
|
|
256
|
+
output = "\n".join(lines)
|
|
257
|
+
return ToolOutput(
|
|
258
|
+
metadata={"output": output, "exit_code": 0},
|
|
259
|
+
blocks=[TextBlock(text=output)],
|
|
260
|
+
reward=0.0,
|
|
261
|
+
finished=False,
|
|
262
|
+
)
|
|
263
|
+
except Exception as e:
|
|
264
|
+
return ToolOutput(
|
|
265
|
+
metadata={"error": str(e)},
|
|
266
|
+
blocks=[TextBlock(text=f"Error reading file: {str(e)}")],
|
|
267
|
+
finished=False,
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
@tool
|
|
271
|
+
async def write(self, params: WriteParams) -> ToolOutput:
|
|
272
|
+
try:
|
|
273
|
+
dir_name = os.path.dirname(params.path)
|
|
274
|
+
if dir_name:
|
|
275
|
+
await self.sandbox.run(f"mkdir -p {dir_name}")
|
|
276
|
+
await _upload_text(
|
|
277
|
+
self.sandbox,
|
|
278
|
+
params.path,
|
|
279
|
+
params.content,
|
|
280
|
+
ensure_trailing_newline=True,
|
|
281
|
+
)
|
|
282
|
+
return ToolOutput(
|
|
283
|
+
metadata={"output": "", "exit_code": 0},
|
|
284
|
+
blocks=[TextBlock(text=f"Successfully wrote to {params.path}")],
|
|
285
|
+
reward=0.0,
|
|
286
|
+
finished=False,
|
|
287
|
+
)
|
|
288
|
+
except Exception as e:
|
|
289
|
+
return ToolOutput(
|
|
290
|
+
metadata={"error": str(e)},
|
|
291
|
+
blocks=[TextBlock(text=f"Error writing file: {str(e)}")],
|
|
292
|
+
finished=False,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
@tool
|
|
296
|
+
async def edit(self, params: EditParams) -> ToolOutput:
|
|
297
|
+
try:
|
|
298
|
+
content = await _download_text(self.sandbox, params.path)
|
|
299
|
+
|
|
300
|
+
for item in params.edits:
|
|
301
|
+
count = content.count(item.oldText)
|
|
302
|
+
if count == 0:
|
|
303
|
+
return ToolOutput(
|
|
304
|
+
metadata={"error": f"oldText not found in {params.path}"},
|
|
305
|
+
blocks=[TextBlock(text=f"Error: oldText not found in {params.path}: {item.oldText[:100]}")],
|
|
306
|
+
finished=False,
|
|
307
|
+
)
|
|
308
|
+
if count > 1:
|
|
309
|
+
return ToolOutput(
|
|
310
|
+
metadata={"error": f"oldText appears {count} times; must be unique"},
|
|
311
|
+
blocks=[TextBlock(text=f"Error: oldText appears {count} times in {params.path}. Must be unique.")],
|
|
312
|
+
finished=False,
|
|
313
|
+
)
|
|
314
|
+
content = content.replace(item.oldText, item.newText, 1)
|
|
315
|
+
|
|
316
|
+
await _upload_text(self.sandbox, params.path, content, ensure_trailing_newline=True)
|
|
317
|
+
|
|
318
|
+
return ToolOutput(
|
|
319
|
+
metadata={"output": "", "exit_code": 0},
|
|
320
|
+
blocks=[TextBlock(text=f"Successfully edited {params.path}")],
|
|
321
|
+
reward=0.0,
|
|
322
|
+
finished=False,
|
|
323
|
+
)
|
|
324
|
+
except Exception as e:
|
|
325
|
+
return ToolOutput(
|
|
326
|
+
metadata={"error": str(e)},
|
|
327
|
+
blocks=[TextBlock(text=f"Error editing file: {str(e)}")],
|
|
328
|
+
finished=False,
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
@tool
|
|
332
|
+
async def apply_patch(self, params: ApplyPatchParams) -> ToolOutput:
|
|
333
|
+
try:
|
|
334
|
+
# Upload patch content to a temp file and apply via patch command
|
|
335
|
+
patch_tmp = "/tmp/_openclaw_patch.diff"
|
|
336
|
+
await _upload_text(self.sandbox, patch_tmp, params.input, ensure_trailing_newline=True)
|
|
337
|
+
|
|
338
|
+
output, code = await self.sandbox.run(f"patch -p1 < {patch_tmp}")
|
|
339
|
+
if code != 0:
|
|
340
|
+
await self.sandbox.run(f"rm -f {patch_tmp}")
|
|
341
|
+
return ToolOutput(
|
|
342
|
+
metadata={"error": output, "exit_code": code},
|
|
343
|
+
blocks=[TextBlock(text=f"apply_patch failed (exit {code}):\n{output}")],
|
|
344
|
+
finished=False,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
await self.sandbox.run(f"rm -f {patch_tmp}")
|
|
348
|
+
return ToolOutput(
|
|
349
|
+
metadata={"output": output, "exit_code": 0},
|
|
350
|
+
blocks=[TextBlock(text=f"Successfully applied patch:\n{output}")],
|
|
351
|
+
reward=0.0,
|
|
352
|
+
finished=False,
|
|
353
|
+
)
|
|
354
|
+
except Exception as e:
|
|
355
|
+
return ToolOutput(
|
|
356
|
+
metadata={"error": str(e)},
|
|
357
|
+
blocks=[TextBlock(text=f"Error applying patch: {str(e)}")],
|
|
358
|
+
finished=False,
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
# Assign descriptions onto each tool method's __doc__ so the framework's
|
|
363
|
+
# introspection picks them up.
|
|
364
|
+
OpenClawSandboxedToolset.exec.__doc__ = EXEC_DESCRIPTION
|
|
365
|
+
OpenClawSandboxedToolset.process.__doc__ = PROCESS_DESCRIPTION
|
|
366
|
+
OpenClawSandboxedToolset.read.__doc__ = READ_DESCRIPTION
|
|
367
|
+
OpenClawSandboxedToolset.write.__doc__ = WRITE_DESCRIPTION
|
|
368
|
+
OpenClawSandboxedToolset.edit.__doc__ = EDIT_DESCRIPTION
|
|
369
|
+
OpenClawSandboxedToolset.apply_patch.__doc__ = APPLY_PATCH_DESCRIPTION
|
|
@@ -287,14 +287,14 @@ async def test_session_toolset_warns_on_shadow(monkeypatch):
|
|
|
287
287
|
|
|
288
288
|
# ── OpenClaw toolset ──
|
|
289
289
|
|
|
290
|
-
OPENCLAW_TOOLS = {"exec", "read", "write", "edit"}
|
|
290
|
+
OPENCLAW_TOOLS = {"exec", "process", "read", "write", "edit", "apply_patch"}
|
|
291
291
|
|
|
292
292
|
|
|
293
293
|
@pytest.mark.asyncio
|
|
294
294
|
async def test_openclaw_toolset_tools(client: AsyncOpenReward, server: str):
|
|
295
295
|
env = client.environments.get("envwithsandbox", variant="envwithsandbox", base_url=server)
|
|
296
296
|
tasks = await env.list_tasks(split="train")
|
|
297
|
-
async with env.session(tasks[0], toolset="openclaw") as session:
|
|
297
|
+
async with env.session(tasks[0], toolset="openclaw-sandboxed") as session:
|
|
298
298
|
tools = await session.list_tools()
|
|
299
299
|
toolset_names = {t.name for t in tools}
|
|
300
300
|
for name in OPENCLAW_TOOLS:
|
|
@@ -310,7 +310,7 @@ async def test_openclaw_toolset_tools(client: AsyncOpenReward, server: str):
|
|
|
310
310
|
async def test_openclaw_exec_routes_to_sandbox(client: AsyncOpenReward, server: str):
|
|
311
311
|
env = client.environments.get("envwithsandbox", variant="envwithsandbox", base_url=server)
|
|
312
312
|
tasks = await env.list_tasks(split="train")
|
|
313
|
-
async with env.session(tasks[0], toolset="openclaw") as session:
|
|
313
|
+
async with env.session(tasks[0], toolset="openclaw-sandboxed") as session:
|
|
314
314
|
result = await session.call_tool("exec", {"command": "echo hi"})
|
|
315
315
|
assert "ran: echo hi" in result.blocks[0].text
|
|
316
316
|
|
|
@@ -319,7 +319,7 @@ async def test_openclaw_exec_routes_to_sandbox(client: AsyncOpenReward, server:
|
|
|
319
319
|
async def test_openclaw_write_then_read_roundtrip(client: AsyncOpenReward, server: str):
|
|
320
320
|
env = client.environments.get("envwithsandbox", variant="envwithsandbox", base_url=server)
|
|
321
321
|
tasks = await env.list_tasks(split="train")
|
|
322
|
-
async with env.session(tasks[0], toolset="openclaw") as session:
|
|
322
|
+
async with env.session(tasks[0], toolset="openclaw-sandboxed") as session:
|
|
323
323
|
await session.call_tool("write", {"path": "/tmp/oc.txt", "content": "hello openclaw"})
|
|
324
324
|
result = await session.call_tool("read", {"path": "/tmp/oc.txt"})
|
|
325
325
|
assert "hello openclaw" in result.blocks[0].text
|
|
@@ -329,7 +329,7 @@ async def test_openclaw_write_then_read_roundtrip(client: AsyncOpenReward, serve
|
|
|
329
329
|
async def test_openclaw_edit_with_edits_array(client: AsyncOpenReward, server: str):
|
|
330
330
|
env = client.environments.get("envwithsandbox", variant="envwithsandbox", base_url=server)
|
|
331
331
|
tasks = await env.list_tasks(split="train")
|
|
332
|
-
async with env.session(tasks[0], toolset="openclaw") as session:
|
|
332
|
+
async with env.session(tasks[0], toolset="openclaw-sandboxed") as session:
|
|
333
333
|
await session.call_tool("write", {"path": "/tmp/oc_edit.txt", "content": "foo bar baz"})
|
|
334
334
|
result = await session.call_tool("edit", {
|
|
335
335
|
"path": "/tmp/oc_edit.txt",
|
|
@@ -341,16 +341,28 @@ async def test_openclaw_edit_with_edits_array(client: AsyncOpenReward, server: s
|
|
|
341
341
|
assert "bar" not in read_result.blocks[0].text
|
|
342
342
|
|
|
343
343
|
|
|
344
|
+
@pytest.mark.asyncio
|
|
345
|
+
async def test_openclaw_apply_patch(client: AsyncOpenReward, server: str):
|
|
346
|
+
env = client.environments.get("envwithsandbox", variant="envwithsandbox", base_url=server)
|
|
347
|
+
tasks = await env.list_tasks(split="train")
|
|
348
|
+
async with env.session(tasks[0], toolset="openclaw-sandboxed") as session:
|
|
349
|
+
result = await session.call_tool("apply_patch", {
|
|
350
|
+
"input": "*** Begin Patch\n*** Update File: /tmp/test.txt\n--- old\n+++ new\n*** End Patch",
|
|
351
|
+
})
|
|
352
|
+
# apply_patch runs patch under the hood; mock sandbox returns "ran: ..."
|
|
353
|
+
assert result.blocks[0].text is not None
|
|
354
|
+
|
|
355
|
+
|
|
344
356
|
# ── Hermes toolset ──
|
|
345
357
|
|
|
346
|
-
HERMES_TOOLS = {"terminal", "read_file", "write_file", "patch"}
|
|
358
|
+
HERMES_TOOLS = {"terminal", "read_file", "write_file", "search_files", "patch"}
|
|
347
359
|
|
|
348
360
|
|
|
349
361
|
@pytest.mark.asyncio
|
|
350
362
|
async def test_hermes_toolset_tools(client: AsyncOpenReward, server: str):
|
|
351
363
|
env = client.environments.get("envwithsandbox", variant="envwithsandbox", base_url=server)
|
|
352
364
|
tasks = await env.list_tasks(split="train")
|
|
353
|
-
async with env.session(tasks[0], toolset="hermes") as session:
|
|
365
|
+
async with env.session(tasks[0], toolset="hermes-sandboxed") as session:
|
|
354
366
|
tools = await session.list_tools()
|
|
355
367
|
toolset_names = {t.name for t in tools}
|
|
356
368
|
for name in HERMES_TOOLS:
|
|
@@ -364,7 +376,7 @@ async def test_hermes_toolset_tools(client: AsyncOpenReward, server: str):
|
|
|
364
376
|
async def test_hermes_terminal_routes_to_sandbox(client: AsyncOpenReward, server: str):
|
|
365
377
|
env = client.environments.get("envwithsandbox", variant="envwithsandbox", base_url=server)
|
|
366
378
|
tasks = await env.list_tasks(split="train")
|
|
367
|
-
async with env.session(tasks[0], toolset="hermes") as session:
|
|
379
|
+
async with env.session(tasks[0], toolset="hermes-sandboxed") as session:
|
|
368
380
|
result = await session.call_tool("terminal", {"command": "echo hi"})
|
|
369
381
|
assert "ran: echo hi" in result.blocks[0].text
|
|
370
382
|
|
|
@@ -373,7 +385,7 @@ async def test_hermes_terminal_routes_to_sandbox(client: AsyncOpenReward, server
|
|
|
373
385
|
async def test_hermes_write_then_read_roundtrip(client: AsyncOpenReward, server: str):
|
|
374
386
|
env = client.environments.get("envwithsandbox", variant="envwithsandbox", base_url=server)
|
|
375
387
|
tasks = await env.list_tasks(split="train")
|
|
376
|
-
async with env.session(tasks[0], toolset="hermes") as session:
|
|
388
|
+
async with env.session(tasks[0], toolset="hermes-sandboxed") as session:
|
|
377
389
|
await session.call_tool("write_file", {"path": "/tmp/hm.txt", "content": "hello hermes"})
|
|
378
390
|
result = await session.call_tool("read_file", {"path": "/tmp/hm.txt"})
|
|
379
391
|
# Hermes read_file uses LINE_NUM|CONTENT format
|
|
@@ -381,11 +393,21 @@ async def test_hermes_write_then_read_roundtrip(client: AsyncOpenReward, server:
|
|
|
381
393
|
assert "1|" in result.blocks[0].text
|
|
382
394
|
|
|
383
395
|
|
|
396
|
+
@pytest.mark.asyncio
|
|
397
|
+
async def test_hermes_search_files_content(client: AsyncOpenReward, server: str):
|
|
398
|
+
env = client.environments.get("envwithsandbox", variant="envwithsandbox", base_url=server)
|
|
399
|
+
tasks = await env.list_tasks(split="train")
|
|
400
|
+
async with env.session(tasks[0], toolset="hermes-sandboxed") as session:
|
|
401
|
+
result = await session.call_tool("search_files", {"pattern": "hello", "path": "/tmp"})
|
|
402
|
+
# search_files runs grep under the hood; mock sandbox returns "ran: ..."
|
|
403
|
+
assert result.blocks[0].text is not None
|
|
404
|
+
|
|
405
|
+
|
|
384
406
|
@pytest.mark.asyncio
|
|
385
407
|
async def test_hermes_patch_replace_mode(client: AsyncOpenReward, server: str):
|
|
386
408
|
env = client.environments.get("envwithsandbox", variant="envwithsandbox", base_url=server)
|
|
387
409
|
tasks = await env.list_tasks(split="train")
|
|
388
|
-
async with env.session(tasks[0], toolset="hermes") as session:
|
|
410
|
+
async with env.session(tasks[0], toolset="hermes-sandboxed") as session:
|
|
389
411
|
await session.call_tool("write_file", {"path": "/tmp/hm_patch.txt", "content": "foo bar baz"})
|
|
390
412
|
result = await session.call_tool("patch", {
|
|
391
413
|
"mode": "replace",
|
|
@@ -1,222 +0,0 @@
|
|
|
1
|
-
"""OpenClaw session toolset.
|
|
2
|
-
|
|
3
|
-
Provides the four built-in tools that OpenClaw exposes for coding tasks
|
|
4
|
-
(``exec``, ``read``, ``write``, ``edit``), each backed by ``self.sandbox``
|
|
5
|
-
from the bound environment.
|
|
6
|
-
|
|
7
|
-
Tool names, parameter schemas, and descriptions match OpenClaw's upstream
|
|
8
|
-
TypeBox definitions (``@mariozechner/pi-coding-agent``).
|
|
9
|
-
"""
|
|
10
|
-
from __future__ import annotations
|
|
11
|
-
|
|
12
|
-
import base64
|
|
13
|
-
import os
|
|
14
|
-
from typing import Any, List, Optional
|
|
15
|
-
|
|
16
|
-
from pydantic import BaseModel, Field
|
|
17
|
-
|
|
18
|
-
from openreward.environments.environment import tool
|
|
19
|
-
from openreward.environments.toolset import Toolset
|
|
20
|
-
from openreward.environments.types import TextBlock, ToolOutput
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
# ── Sandbox text helpers (inlined; same as claude_code.py) ──
|
|
24
|
-
|
|
25
|
-
async def _download_text(sandbox: Any, path: str) -> str:
|
|
26
|
-
data = await sandbox.download(path)
|
|
27
|
-
return data.decode("utf-8")
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
async def _upload_text(
|
|
31
|
-
sandbox: Any,
|
|
32
|
-
path: str,
|
|
33
|
-
content: str,
|
|
34
|
-
ensure_trailing_newline: bool = True,
|
|
35
|
-
) -> None:
|
|
36
|
-
if ensure_trailing_newline and not content.endswith("\n"):
|
|
37
|
-
content = content + "\n"
|
|
38
|
-
encoded = base64.b64encode(content.encode("utf-8")).decode("ascii")
|
|
39
|
-
await sandbox.check_run(f"echo '{encoded}' | base64 -d > {path}")
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
# ── Pydantic parameter models ──
|
|
43
|
-
|
|
44
|
-
class ExecParams(BaseModel, extra="forbid"):
|
|
45
|
-
command: str
|
|
46
|
-
timeout: Optional[float] = 1800.0
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
class ReadParams(BaseModel, extra="forbid"):
|
|
50
|
-
path: str
|
|
51
|
-
offset: Optional[int] = None
|
|
52
|
-
limit: Optional[int] = None
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
class WriteParams(BaseModel, extra="forbid"):
|
|
56
|
-
path: str
|
|
57
|
-
content: str
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
class EditItem(BaseModel, extra="forbid"):
|
|
61
|
-
oldText: str
|
|
62
|
-
newText: str
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
class EditParams(BaseModel, extra="forbid"):
|
|
66
|
-
path: str
|
|
67
|
-
edits: List[EditItem]
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
# ── Tool descriptions (matching OpenClaw TypeBox style) ──
|
|
71
|
-
|
|
72
|
-
EXEC_DESCRIPTION = """\
|
|
73
|
-
Execute a shell command and return its output and exit code. \
|
|
74
|
-
Set timeout in seconds (default 1800). \
|
|
75
|
-
Use for builds, installs, git, processes, scripts, network, package managers, \
|
|
76
|
-
and anything that needs a shell."""
|
|
77
|
-
|
|
78
|
-
READ_DESCRIPTION = """\
|
|
79
|
-
Read the contents of a file at the given path. \
|
|
80
|
-
Supports optional offset (1-indexed line number to start from) and limit \
|
|
81
|
-
(maximum number of lines to read) for pagination of large files."""
|
|
82
|
-
|
|
83
|
-
WRITE_DESCRIPTION = """\
|
|
84
|
-
Write content to a file, creating it if it doesn't exist or overwriting if it does. \
|
|
85
|
-
Creates parent directories automatically. \
|
|
86
|
-
Use the edit tool for targeted modifications to existing files."""
|
|
87
|
-
|
|
88
|
-
EDIT_DESCRIPTION = """\
|
|
89
|
-
Apply one or more targeted text replacements to a file. \
|
|
90
|
-
Each edit specifies an oldText to find and a newText to replace it with. \
|
|
91
|
-
oldText must be unique in the file for each edit."""
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
# ── Toolset ──
|
|
95
|
-
|
|
96
|
-
class OpenClawToolset(Toolset):
|
|
97
|
-
"""Session toolset exposing the OpenClaw four-tool coding surface.
|
|
98
|
-
|
|
99
|
-
The toolset is bound to a session by passing it to ``env.session(...)``::
|
|
100
|
-
|
|
101
|
-
with env.session(task=task, toolset="openclaw") as session:
|
|
102
|
-
session.call_tool("exec", {"command": "ls"})
|
|
103
|
-
|
|
104
|
-
Requires the bound environment to define ``self.sandbox``.
|
|
105
|
-
"""
|
|
106
|
-
|
|
107
|
-
@classmethod
|
|
108
|
-
def name(cls) -> str:
|
|
109
|
-
return "openclaw"
|
|
110
|
-
|
|
111
|
-
@tool
|
|
112
|
-
async def exec(self, params: ExecParams) -> ToolOutput:
|
|
113
|
-
try:
|
|
114
|
-
output, code = await self.sandbox.run(params.command.strip())
|
|
115
|
-
return ToolOutput(
|
|
116
|
-
blocks=[TextBlock(text=f"{output}\n\n(exit {code})")],
|
|
117
|
-
metadata={"output": output, "exit_code": code},
|
|
118
|
-
reward=0.0,
|
|
119
|
-
finished=False,
|
|
120
|
-
)
|
|
121
|
-
except Exception as e:
|
|
122
|
-
return ToolOutput(
|
|
123
|
-
metadata={"error": str(e)},
|
|
124
|
-
blocks=[TextBlock(text=f"Error executing command: {str(e)}")],
|
|
125
|
-
finished=False,
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
@tool
|
|
129
|
-
async def read(self, params: ReadParams) -> ToolOutput:
|
|
130
|
-
try:
|
|
131
|
-
content = await _download_text(self.sandbox, params.path)
|
|
132
|
-
lines = content.splitlines()
|
|
133
|
-
|
|
134
|
-
if params.offset is not None or params.limit is not None:
|
|
135
|
-
start = (params.offset or 1) - 1 # Convert 1-indexed to 0-indexed
|
|
136
|
-
if params.limit is not None:
|
|
137
|
-
lines = lines[start:start + params.limit]
|
|
138
|
-
else:
|
|
139
|
-
lines = lines[start:]
|
|
140
|
-
|
|
141
|
-
output = "\n".join(lines)
|
|
142
|
-
return ToolOutput(
|
|
143
|
-
metadata={"output": output, "exit_code": 0},
|
|
144
|
-
blocks=[TextBlock(text=output)],
|
|
145
|
-
reward=0.0,
|
|
146
|
-
finished=False,
|
|
147
|
-
)
|
|
148
|
-
except Exception as e:
|
|
149
|
-
return ToolOutput(
|
|
150
|
-
metadata={"error": str(e)},
|
|
151
|
-
blocks=[TextBlock(text=f"Error reading file: {str(e)}")],
|
|
152
|
-
finished=False,
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
@tool
|
|
156
|
-
async def write(self, params: WriteParams) -> ToolOutput:
|
|
157
|
-
try:
|
|
158
|
-
dir_name = os.path.dirname(params.path)
|
|
159
|
-
if dir_name:
|
|
160
|
-
await self.sandbox.run(f"mkdir -p {dir_name}")
|
|
161
|
-
await _upload_text(
|
|
162
|
-
self.sandbox,
|
|
163
|
-
params.path,
|
|
164
|
-
params.content,
|
|
165
|
-
ensure_trailing_newline=True,
|
|
166
|
-
)
|
|
167
|
-
return ToolOutput(
|
|
168
|
-
metadata={"output": "", "exit_code": 0},
|
|
169
|
-
blocks=[TextBlock(text=f"Successfully wrote to {params.path}")],
|
|
170
|
-
reward=0.0,
|
|
171
|
-
finished=False,
|
|
172
|
-
)
|
|
173
|
-
except Exception as e:
|
|
174
|
-
return ToolOutput(
|
|
175
|
-
metadata={"error": str(e)},
|
|
176
|
-
blocks=[TextBlock(text=f"Error writing file: {str(e)}")],
|
|
177
|
-
finished=False,
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
@tool
|
|
181
|
-
async def edit(self, params: EditParams) -> ToolOutput:
|
|
182
|
-
try:
|
|
183
|
-
content = await _download_text(self.sandbox, params.path)
|
|
184
|
-
|
|
185
|
-
for item in params.edits:
|
|
186
|
-
count = content.count(item.oldText)
|
|
187
|
-
if count == 0:
|
|
188
|
-
return ToolOutput(
|
|
189
|
-
metadata={"error": f"oldText not found in {params.path}"},
|
|
190
|
-
blocks=[TextBlock(text=f"Error: oldText not found in {params.path}: {item.oldText[:100]}")],
|
|
191
|
-
finished=False,
|
|
192
|
-
)
|
|
193
|
-
if count > 1:
|
|
194
|
-
return ToolOutput(
|
|
195
|
-
metadata={"error": f"oldText appears {count} times; must be unique"},
|
|
196
|
-
blocks=[TextBlock(text=f"Error: oldText appears {count} times in {params.path}. Must be unique.")],
|
|
197
|
-
finished=False,
|
|
198
|
-
)
|
|
199
|
-
content = content.replace(item.oldText, item.newText, 1)
|
|
200
|
-
|
|
201
|
-
await _upload_text(self.sandbox, params.path, content, ensure_trailing_newline=True)
|
|
202
|
-
|
|
203
|
-
return ToolOutput(
|
|
204
|
-
metadata={"output": "", "exit_code": 0},
|
|
205
|
-
blocks=[TextBlock(text=f"Successfully edited {params.path}")],
|
|
206
|
-
reward=0.0,
|
|
207
|
-
finished=False,
|
|
208
|
-
)
|
|
209
|
-
except Exception as e:
|
|
210
|
-
return ToolOutput(
|
|
211
|
-
metadata={"error": str(e)},
|
|
212
|
-
blocks=[TextBlock(text=f"Error editing file: {str(e)}")],
|
|
213
|
-
finished=False,
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
# Assign descriptions onto each tool method's __doc__ so the framework's
|
|
218
|
-
# introspection picks them up.
|
|
219
|
-
OpenClawToolset.exec.__doc__ = EXEC_DESCRIPTION
|
|
220
|
-
OpenClawToolset.read.__doc__ = READ_DESCRIPTION
|
|
221
|
-
OpenClawToolset.write.__doc__ = WRITE_DESCRIPTION
|
|
222
|
-
OpenClawToolset.edit.__doc__ = EDIT_DESCRIPTION
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/api/rollouts/serializers/__init__.py
RENAMED
|
File without changes
|
{openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/api/rollouts/serializers/ant.py
RENAMED
|
File without changes
|
{openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/api/rollouts/serializers/base.py
RENAMED
|
File without changes
|
{openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/api/rollouts/serializers/gdm.py
RENAMED
|
File without changes
|
{openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/api/rollouts/serializers/models.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/api/rollouts/serializers/utils.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/templates/basic/requirements.txt
RENAMED
|
File without changes
|
{openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/templates/basic/requirements.txt.tmpl
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/templates/sandbox/requirements.txt
RENAMED
|
File without changes
|
{openreward-0.1.95.dev0 → openreward-0.1.96.dev1}/openreward/templates/sandbox/sandbox_env.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|