mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-30 01:32:25 +01:00
fix(tools): expose active Python environment in workspace namespace
This commit is contained in:
@@ -231,6 +231,7 @@ def _wrap_workspace_namespace(
|
|||||||
cwd: str,
|
cwd: str,
|
||||||
*,
|
*,
|
||||||
chdir: str = "/workspace",
|
chdir: str = "/workspace",
|
||||||
|
interpreter_prefix: str | None = None,
|
||||||
) -> str | None:
|
) -> str | None:
|
||||||
"""Run a shell command with the active workspace mounted at /workspace.
|
"""Run a shell command with the active workspace mounted at /workspace.
|
||||||
|
|
||||||
@@ -254,8 +255,27 @@ def _wrap_workspace_namespace(
|
|||||||
"--dir", "/tmp", "--tmpfs", "/tmp",
|
"--dir", "/tmp", "--tmpfs", "/tmp",
|
||||||
"--dev-bind", "/dev", "/dev", "--proc", "/proc",
|
"--dev-bind", "/dev", "/dev", "--proc", "/proc",
|
||||||
"--dir", "/workspace", "--bind", cwd, "/workspace",
|
"--dir", "/workspace", "--bind", cwd, "/workspace",
|
||||||
"--chdir", chdir, "/bin/bash", "-lc", content,
|
|
||||||
]
|
]
|
||||||
|
# setup-python installs interpreters under /opt, and local CI virtualenvs
|
||||||
|
# can live under /tmp. Those paths are hidden by the private root/tmpfs.
|
||||||
|
# Expose only the active interpreter environment, read-only, so Python
|
||||||
|
# tools keep their installed packages without exposing the host /tmp.
|
||||||
|
if interpreter_prefix:
|
||||||
|
prefix = os.path.abspath(interpreter_prefix)
|
||||||
|
mounted_roots = ("/usr", "/home", "/mnt")
|
||||||
|
if os.path.isdir(prefix) and not any(
|
||||||
|
prefix == root or prefix.startswith(root + os.sep)
|
||||||
|
for root in mounted_roots
|
||||||
|
):
|
||||||
|
parents = []
|
||||||
|
parent = os.path.dirname(prefix)
|
||||||
|
while parent not in ("/", "/tmp", "/etc", "/workspace", *mounted_roots):
|
||||||
|
parents.append(parent)
|
||||||
|
parent = os.path.dirname(parent)
|
||||||
|
for directory in reversed(parents):
|
||||||
|
args.extend(("--dir", directory))
|
||||||
|
args.extend(("--ro-bind", prefix, prefix))
|
||||||
|
args.extend(("--chdir", chdir, "/bin/bash", "-lc", content))
|
||||||
return shlex.join(args)
|
return shlex.join(args)
|
||||||
|
|
||||||
|
|
||||||
@@ -940,6 +960,7 @@ class PythonTool:
|
|||||||
python_command,
|
python_command,
|
||||||
agent_cwd(),
|
agent_cwd(),
|
||||||
chdir="/workspace",
|
chdir="/workspace",
|
||||||
|
interpreter_prefix=sys.prefix,
|
||||||
)
|
)
|
||||||
if needs_virtual_namespace
|
if needs_virtual_namespace
|
||||||
else None
|
else None
|
||||||
|
|||||||
@@ -304,7 +304,7 @@ async def test_write_file_dispatch_rejects_empty_directory_like_workspace_path(m
|
|||||||
security_context=NO_TOOL_SECURITY_CONTEXT,
|
security_context=NO_TOOL_SECURITY_CONTEXT,
|
||||||
)
|
)
|
||||||
assert desc == "write_file: /workspace/papers"
|
assert desc == "write_file: /workspace/papers"
|
||||||
assert "refusing to create an empty file at a directory-like path" in (
|
assert "content required; refusing to create an empty file" in (
|
||||||
result.get("error") or ""
|
result.get("error") or ""
|
||||||
)
|
)
|
||||||
assert result.get("exit_code") == 1
|
assert result.get("exit_code") == 1
|
||||||
|
|||||||
@@ -1037,14 +1037,14 @@ def test_visual_text_extraction_is_distinct_from_speech_transcription():
|
|||||||
|
|
||||||
|
|
||||||
def test_local_media_routes_select_dedicated_ocr_for_visual_text():
|
def test_local_media_routes_select_dedicated_ocr_for_visual_text():
|
||||||
|
import re
|
||||||
|
|
||||||
source = (Path(__file__).parents[1] / "src" / "agent_loop.py").read_text()
|
source = (Path(__file__).parents[1] / "src" / "agent_loop.py").read_text()
|
||||||
|
|
||||||
assert source.count(
|
assert source.count(
|
||||||
"_ocr_requested = _visual_text_extraction_requested(_last_user)"
|
"_ocr_requested = _visual_text_extraction_requested(_last_user)"
|
||||||
) == 3
|
) == 3
|
||||||
assert source.count(
|
assert len(re.findall(r'\{"extract_text"\}\s*\n\s*if _ocr_requested', source)) == 3
|
||||||
'{"extract_text"}\n if _ocr_requested'
|
|
||||||
) == 3
|
|
||||||
|
|
||||||
|
|
||||||
def test_workspace_paths_split_on_chinese_list_punctuation():
|
def test_workspace_paths_split_on_chinese_list_punctuation():
|
||||||
@@ -1656,7 +1656,8 @@ def test_local_media_is_exempt_from_pure_web_schema_and_round_clamps():
|
|||||||
pure_web_start = source.index(" _local_media_turn = bool(")
|
pure_web_start = source.index(" _local_media_turn = bool(")
|
||||||
pure_web_end = source.index("\n if (\n _pure_web_turn", pure_web_start)
|
pure_web_end = source.index("\n if (\n _pure_web_turn", pure_web_start)
|
||||||
assert "and not _local_media_turn" in source[pure_web_start:pure_web_end]
|
assert "and not _local_media_turn" in source[pure_web_start:pure_web_end]
|
||||||
assert source.count("if _pure_web_turn:") >= 3
|
assert source.count("if _pure_web_turn:") == 2
|
||||||
|
assert 'if _pure_web_turn and tool_surface != "full":' in source
|
||||||
|
|
||||||
|
|
||||||
def test_empty_local_media_round_nudges_export_instead_of_ending():
|
def test_empty_local_media_round_nudges_export_instead_of_ending():
|
||||||
@@ -2189,7 +2190,7 @@ def test_python_emits_one_final_bare_expression_without_duplicating_print():
|
|||||||
assert explicit["output"] == "once"
|
assert explicit["output"] == "once"
|
||||||
|
|
||||||
|
|
||||||
def test_python_loaded_code_sees_virtual_workspace_alias(monkeypatch):
|
def test_python_loaded_code_sees_virtual_workspace_alias(monkeypatch, tmp_path):
|
||||||
"""Absolute /workspace paths must work inside generated Python scripts."""
|
"""Absolute /workspace paths must work inside generated Python scripts."""
|
||||||
import asyncio
|
import asyncio
|
||||||
import shutil
|
import shutil
|
||||||
@@ -2201,7 +2202,7 @@ def test_python_loaded_code_sees_virtual_workspace_alias(monkeypatch):
|
|||||||
|
|
||||||
from src.agent_tools import subprocess_tools
|
from src.agent_tools import subprocess_tools
|
||||||
from src import tool_execution
|
from src import tool_execution
|
||||||
workspace = Path("/home/pewds/odysseus-tool-work")
|
workspace = tmp_path
|
||||||
script = workspace / ".python-workspace-alias-test.py"
|
script = workspace / ".python-workspace-alias-test.py"
|
||||||
output = workspace / ".python-workspace-alias-test.txt"
|
output = workspace / ".python-workspace-alias-test.txt"
|
||||||
script.write_text(
|
script.write_text(
|
||||||
@@ -2209,13 +2210,11 @@ def test_python_loaded_code_sees_virtual_workspace_alias(monkeypatch):
|
|||||||
)
|
)
|
||||||
monkeypatch.setattr(tool_execution, "agent_cwd", lambda: str(workspace))
|
monkeypatch.setattr(tool_execution, "agent_cwd", lambda: str(workspace))
|
||||||
result = asyncio.run(subprocess_tools.PythonTool().execute(
|
result = asyncio.run(subprocess_tools.PythonTool().execute(
|
||||||
f"import runpy; runpy.run_path('{script}', run_name='__main__')",
|
"import runpy; runpy.run_path('/workspace/.python-workspace-alias-test.py', run_name='__main__')",
|
||||||
{},
|
{},
|
||||||
))
|
))
|
||||||
assert result["exit_code"] == 0, result
|
assert result["exit_code"] == 0, result
|
||||||
assert output.read_text() == "ok"
|
assert output.read_text() == "ok"
|
||||||
script.unlink()
|
|
||||||
output.unlink()
|
|
||||||
|
|
||||||
|
|
||||||
def test_workspace_namespace_preserves_the_64_bit_dynamic_loader(monkeypatch):
|
def test_workspace_namespace_preserves_the_64_bit_dynamic_loader(monkeypatch):
|
||||||
|
|||||||
Reference in New Issue
Block a user