Skip to content

Commit 4b5d7ca

Browse files
committed
ensure tool calls and reserved names aren't overwritten
1 parent 90bd477 commit 4b5d7ca

8 files changed

Lines changed: 94 additions & 4 deletions

File tree

‎rlm/environments/base_env.py‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,16 @@
88
# Custom Tools Support
99
# =============================================================================
1010

11-
# Reserved names that cannot be overridden by custom tools.
12-
# These are the built-in REPL functions that all environments must provide.
11+
# Reserved names: cannot be overridden by custom tools, and are restored after each
12+
# code execution to prevent namespace corruption (e.g. context = "...", llm_query = ...).
1313
RESERVED_TOOL_NAMES: frozenset[str] = frozenset(
1414
{
1515
"llm_query",
1616
"llm_query_batched",
1717
"FINAL_VAR",
1818
"SHOW_VARS",
1919
"context",
20+
"history",
2021
}
2122
)
2223

@@ -173,7 +174,7 @@ class SupportsCustomTools(Protocol):
173174
The following names cannot be used as custom tool names:
174175
- llm_query, llm_query_batched: Single LM completion functions (no tool access)
175176
- FINAL_VAR, SHOW_VARS: Built-in helper functions
176-
- context: The input context variable
177+
- context, history: The input context and conversation history variables
177178
178179
EXAMPLE:
179180
custom_tools = {

‎rlm/environments/daytona_repl.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,12 @@ def SHOW_VARS():
349349
sys.stdout = old_stdout
350350
sys.stderr = old_stderr
351351
352+
# Restore scaffold aliases if overwritten by executed code
353+
if "context_0" in _locals:
354+
_locals["context"] = _locals["context_0"]
355+
if "history_0" in _locals:
356+
_locals["history"] = _locals["history_0"]
357+
352358
save_state(_locals)
353359
354360
result = {{

‎rlm/environments/docker_repl.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,12 @@ def SHOW_VARS():
173173
finally:
174174
sys.stdout, sys.stderr = old_stdout, old_stderr
175175
176+
# Restore scaffold aliases if overwritten by executed code
177+
if "context_0" in _locals:
178+
_locals["context"] = _locals["context_0"]
179+
if "history_0" in _locals:
180+
_locals["history"] = _locals["history_0"]
181+
176182
save_state(_locals)
177183
print(json.dumps({{"stdout": stdout_buf.getvalue(), "stderr": stderr_buf.getvalue(), "locals": {{k: repr(v) for k, v in _locals.items() if not k.startswith("_")}}}}, ensure_ascii=False))
178184
'''

‎rlm/environments/e2b_repl.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,12 @@ def FINAL_VAR(variable_name):
241241
sys.stdout = old_stdout
242242
sys.stderr = old_stderr
243243
244+
# Restore scaffold aliases if overwritten by executed code
245+
if "context_0" in _locals:
246+
_locals["context"] = _locals["context_0"]
247+
if "history_0" in _locals:
248+
_locals["history"] = _locals["history_0"]
249+
244250
save_state(_locals)
245251
246252
result = {{

‎rlm/environments/local_repl.py‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,12 @@
1313

1414
from rlm.core.comms_utils import LMRequest, send_lm_request, send_lm_request_batched
1515
from rlm.core.types import REPLResult, RLMChatCompletion
16-
from rlm.environments.base_env import NonIsolatedEnv, extract_tool_value, validate_custom_tools
16+
from rlm.environments.base_env import (
17+
RESERVED_TOOL_NAMES,
18+
NonIsolatedEnv,
19+
extract_tool_value,
20+
validate_custom_tools,
21+
)
1722

1823
# =============================================================================
1924
# Safe Builtins
@@ -375,6 +380,22 @@ def _temp_cwd(self):
375380
finally:
376381
os.chdir(old_cwd)
377382

383+
def _restore_scaffold(self) -> None:
384+
"""Restore scaffold names after execution so overwrites (e.g. context = 'x') don't persist."""
385+
for name in RESERVED_TOOL_NAMES:
386+
if name == "llm_query":
387+
self.globals["llm_query"] = self._llm_query
388+
elif name == "llm_query_batched":
389+
self.globals["llm_query_batched"] = self._llm_query_batched
390+
elif name == "FINAL_VAR":
391+
self.globals["FINAL_VAR"] = self._final_var
392+
elif name == "SHOW_VARS":
393+
self.globals["SHOW_VARS"] = self._show_vars
394+
elif name == "context" and "context_0" in self.locals:
395+
self.locals["context"] = self.locals["context_0"]
396+
elif name == "history" and "history_0" in self.locals:
397+
self.locals["history"] = self.locals["history_0"]
398+
378399
def execute_code(self, code: str) -> REPLResult:
379400
"""Execute code in the persistent namespace and return result."""
380401
start_time = time.perf_counter()
@@ -392,6 +413,9 @@ def execute_code(self, code: str) -> REPLResult:
392413
if key not in self.globals and not key.startswith("_"):
393414
self.locals[key] = value
394415

416+
# Restore scaffold so model overwrites (context = ..., llm_query = ...) don't persist
417+
self._restore_scaffold()
418+
395419
stdout = stdout_buf.getvalue()
396420
stderr = stderr_buf.getvalue()
397421
except Exception as e:

‎rlm/environments/modal_repl.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,12 @@ def SHOW_VARS():
261261
sys.stdout = old_stdout
262262
sys.stderr = old_stderr
263263
264+
# Restore scaffold aliases if overwritten by executed code
265+
if "context_0" in _locals:
266+
_locals["context"] = _locals["context_0"]
267+
if "history_0" in _locals:
268+
_locals["history"] = _locals["history_0"]
269+
264270
save_state(_locals)
265271
266272
result = {{

‎rlm/environments/prime_repl.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,12 @@ def SHOW_VARS():
260260
sys.stdout = old_stdout
261261
sys.stderr = old_stderr
262262
263+
# Restore scaffold aliases if overwritten by executed code
264+
if "context_0" in _locals:
265+
_locals["context"] = _locals["context_0"]
266+
if "history_0" in _locals:
267+
_locals["history"] = _locals["history_0"]
268+
263269
save_state(_locals)
264270
265271
result = {{

‎tests/test_local_repl.py‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,41 @@ def test_list_context(self):
175175
repl.cleanup()
176176

177177

178+
class TestLocalREPLScaffoldRestoration:
179+
"""Tests that overwriting scaffold names (context, llm_query, etc.) is reverted after each execution."""
180+
181+
def test_context_restored_after_overwrite(self):
182+
"""If the model does context = 'something', the next execution still sees the real context."""
183+
repl = LocalREPL(context_payload="original context content")
184+
assert repl.locals["context"] == "original context content"
185+
186+
repl.execute_code('context = "hijacked"')
187+
assert repl.locals["context"] == "original context content"
188+
189+
out = repl.execute_code("print(context)")
190+
assert "original context content" in out.stdout
191+
repl.cleanup()
192+
193+
def test_llm_query_restored_after_overwrite(self):
194+
"""If the model does llm_query = lambda x: 'hijacked', the next execution still has real llm_query."""
195+
repl = LocalREPL()
196+
repl.execute_code("llm_query = lambda x: 'hijacked'")
197+
198+
repl.execute_code("r = llm_query('test')")
199+
assert "Error" in repl.locals["r"]
200+
repl.cleanup()
201+
202+
def test_final_var_restored_after_overwrite(self):
203+
"""If the model overwrites FINAL_VAR, the next execution still has the real FINAL_VAR."""
204+
repl = LocalREPL()
205+
repl.execute_code("answer = 42")
206+
repl.execute_code("FINAL_VAR = lambda x: 'overwritten'")
207+
208+
repl.execute_code("result = FINAL_VAR('answer')")
209+
assert repl.locals["result"] == "42"
210+
repl.cleanup()
211+
212+
178213
class TestLocalREPLCleanup:
179214
"""Tests for cleanup behavior."""
180215

0 commit comments

Comments
 (0)