test(agents): capture inline sandbox __import__ escape regression (#107) #110

Open
CoreRasurae wants to merge 1 commits from tdd/m1-inline-sandbox-import-restriction into master
2 changed files with 98 additions and 0 deletions
@@ -0,0 +1,63 @@
Feature: Inline-code sandbox restricts imports to the json module only
As a host operating CleverAgents in safe mode
I want inline tool code to be unable to import any module other than json
So that a "type: tool" agent's code body or the python_exec built-in tool
cannot reach the filesystem, network, or process facilities that
§13.2.1's Restricted Built-ins table and §13.2.3's prohibited-capabilities
list were written to keep out
# Regression test for issue #107:
# ToolAgent._execute_python_code (cleveractors.agents.tool) builds the
# inline-code sandbox's __builtins__ dict per docs/index.md §13.2.1's
# "Restricted Built-ins for Inline Code" table, which lists a single
# module-shaped facility json and states that table is exhaustive
# ("no other built-in facilities MAY be exposed"). §13.2.3 additionally
# prohibits, without qualification, "dynamic import of modules other than
# those explicitly listed".
#
# Despite this, safe_globals["__builtins__"]["__import__"] is bound
# directly to the real, unrestricted __import__ builtin (with the
# accompanying comment "Allow imports for json"). Because __import__ is
# the actual mechanism the `import` statement invokes, inline code can
# write `import os` (or any other module) and reach exactly the
# facilities the standard was written to keep out. Both documented
# entry points into _execute_python_code share this gap: a "type: tool"
# agent's inline `code:` body, and the exec_python-gated `python_exec`
# built-in tool.
#
# Will be fixed by replacing the raw __import__ reference with a
# restricted shim on bugfix/m1-inline-sandbox-import-restriction per
# issue #107.
@tdd_issue @tdd_issue_107 @tdd_expected_fail
Scenario: python_exec tool rejects "import os" instead of executing it
Given a ToolAgent is configured with name "python_exec_import_escape_agent" and config
"""
{
"tools": ["python_exec"],
"exec_python": true
}
"""
When I create the ToolAgent
And I process a JSON message with the ToolAgent:
"""
{"tool": "python_exec", "args": {"code": "import os\nresult = os.getcwd()"}}
"""
Then the inline code's import of "os" should have been rejected by the sandbox
@tdd_issue @tdd_issue_107 @tdd_expected_fail
Scenario: A tool agent's inline code body rejects "import os" instead of executing it
Given a ToolAgent is configured with name "inline_code_import_escape_agent" and config
"""
{
"tools": [
{
"name": "escape_tool",
"code": "import os\nresult = os.getcwd()"
}
]
}
"""
When I create the ToolAgent
And I process a message with the ToolAgent: "irrelevant trigger message"
Then the inline code's import of "os" should have been rejected by the sandbox
@@ -0,0 +1,35 @@
"""Step definitions for the inline-code sandbox import-escape regression (issue #107).
Reuses the shared ToolAgent configuration/execution steps from
``tool_agent_steps.py`` and adds the one assertion specific to this
regression: that importing a non-``json`` module from inline code is
rejected with an :class:`ExecutionError`, the same category
``ToolAgent._execute_python_code`` already uses for other prohibited-name
access, rather than succeeding outright. See
``inline_sandbox_import_restriction.feature`` for the scenarios this backs.
"""
from __future__ import annotations
from typing import Any
from behave import then
from cleveractors.core.exceptions import ExecutionError
@then(
'the inline code\'s import of "{module_name}" should have been rejected by the sandbox'
)
def step_assert_import_rejected(context: Any, module_name: str) -> None:
assert isinstance(context.error, ExecutionError), (
f"Expected inline code executing 'import {module_name}' to be "
"rejected with an ExecutionError — the category "
"ToolAgent._execute_python_code already uses for other "
"prohibited-name access (docs/index.md §13.2.1 lists no import "
"facility beyond `json`; §13.2.3 prohibits dynamic import of any "
f"other module) — but got {context.error!r} with result "
f"{context.result!r} instead. The sandbox's __import__ builtin is "
f"the real, unrestricted import machinery and let {module_name!r} "
"through."
)