From e06184cc2fff7b416b1e5d6b1fea83bd1bc5a30a Mon Sep 17 00:00:00 2001 From: oliver Date: Fri, 1 May 2026 01:10:03 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=B9=E9=BD=90=20run=5Fcommand=20=E4=B8=8E?= =?UTF-8?q?=20write=5Ffile=20=E7=9A=84=E9=BB=98=E8=AE=A4=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E8=A1=8C=E4=B8=BA=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 run_command 在未传 cwd 时改为默认 data/workspace,并将 write_file 调整为相对路径写入 data/workspace、绝对路径直写(经路径守卫校验),同步更新测试与文档。 Made-with: Cursor --- docs/LOCAL_PUBLIC_TOOLS.md | 5 +++++ runtime/tools/local_sdk/adapter.py | 5 +++-- runtime/tools/public/write_file_tool.py | 11 ++--------- tests/test_local_public_tools.py | 5 +++-- tests/test_workspace_path_guard.py | 4 ++-- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/LOCAL_PUBLIC_TOOLS.md b/docs/LOCAL_PUBLIC_TOOLS.md index a2d92840..b49e117a 100644 --- a/docs/LOCAL_PUBLIC_TOOLS.md +++ b/docs/LOCAL_PUBLIC_TOOLS.md @@ -39,3 +39,8 @@ This project exposes local atomic capabilities as shared `public` tools for all - Adapter path: `runtime/tools/local_sdk/adapter.py`. - Uses a self-implemented local backend (cross-platform) with a stable tool contract. + +## Path behavior defaults + +- `run_command`: when `cwd` is omitted, it runs in `data/workspace`. +- `write_file`: absolute path is used directly; relative path is written under `data/workspace`. diff --git a/runtime/tools/local_sdk/adapter.py b/runtime/tools/local_sdk/adapter.py index 5561bc53..d278b383 100644 --- a/runtime/tools/local_sdk/adapter.py +++ b/runtime/tools/local_sdk/adapter.py @@ -73,8 +73,9 @@ class LocalAdapter: try: timeout_s = max(1, min(int(timeout or 30), 600)) # run_command never follows adapter cd state. - # It only uses explicit cwd; otherwise defaults to workspace root ("."). - workdir = str(resolve_workspace_path(cwd or ".")) + # It only uses explicit cwd; otherwise defaults to data/workspace. + workdir = str(resolve_workspace_path(cwd or "data/workspace")) + Path(workdir).mkdir(parents=True, exist_ok=True) run_kwargs: dict[str, Any] = { "cwd": workdir, "shell": True, diff --git a/runtime/tools/public/write_file_tool.py b/runtime/tools/public/write_file_tool.py index f35633f4..b4f08feb 100644 --- a/runtime/tools/public/write_file_tool.py +++ b/runtime/tools/public/write_file_tool.py @@ -8,24 +8,17 @@ from oclaw.runtime.tools.path_guard import resolve_workspace_path def write_file_tool() -> ToolSpec: - def _sandbox_base_dir() -> Path: - return Path("data") / "workspace" - def _normalize_write_path(path: str) -> str: raw = str(path or "").strip().strip('"').strip("'") if not raw: raise ValueError("path_required") p = Path(raw) - base = _sandbox_base_dir() if p.is_absolute(): - name = str(p.name or "").strip() - if not name: - raise ValueError("path_required") - return str(base / name) + return raw rel = raw.lstrip("./\\") if not rel: raise ValueError("path_required") - return str(base / rel) + return str(Path("data") / "workspace" / rel) def _handler(args: dict[str, Any]) -> dict[str, Any]: path = str(args.get("path") or "").strip() diff --git a/tests/test_local_public_tools.py b/tests/test_local_public_tools.py index 0965afef..54794c5c 100644 --- a/tests/test_local_public_tools.py +++ b/tests/test_local_public_tools.py @@ -178,6 +178,7 @@ def test_p1_p2_read_tools_smoke(tmp_path: Path, monkeypatch) -> None: def test_run_command_does_not_follow_cd_state(tmp_path: Path, monkeypatch) -> None: monkeypatch.setenv("OPS_WORKSPACE_ROOT", str(tmp_path)) + (tmp_path / "data" / "workspace").mkdir(parents=True, exist_ok=True) (tmp_path / "subdir").mkdir(parents=True, exist_ok=True) (tmp_path / "subdir" / "echo_dir.py").write_text( "import os\nprint(os.path.basename(os.getcwd()))\n", @@ -190,6 +191,6 @@ def test_run_command_does_not_follow_cd_state(tmp_path: Path, monkeypatch) -> No out_run = adapter.run_command(command='python -c "import os; print(os.path.basename(os.getcwd()))"', timeout=20) assert out_run.get("ok") is True, out_run - # If run_command follows cd state this would be "subdir"; we expect workspace root name instead. - assert str(out_run.get("cwd") or "").replace("\\", "/").rstrip("/").endswith(str(tmp_path.name)) + # If run_command follows cd state this would be "subdir"; default should be data/workspace. + assert str(out_run.get("cwd") or "").replace("\\", "/").rstrip("/").endswith("/data/workspace") diff --git a/tests/test_workspace_path_guard.py b/tests/test_workspace_path_guard.py index 498b3980..ff47d0a4 100644 --- a/tests/test_workspace_path_guard.py +++ b/tests/test_workspace_path_guard.py @@ -134,7 +134,7 @@ class WorkspacePathGuardTests(unittest.TestCase): self.assertEqual(str(expected), str(r.get("path"))) self.assertTrue(expected.exists()) - def test_write_file_absolute_path_is_forced_into_workspace_sandbox(self) -> None: + def test_write_file_absolute_path_is_kept_when_within_workspace(self) -> None: with mock.patch.dict( os.environ, {"OPS_WORKSPACE_ROOT": str(self.root), "OPS_WORKSPACE_EXTRA_ROOTS": "", "OPS_WORKSPACE_ALLOW_ANY_PATH": ""}, @@ -146,7 +146,7 @@ class WorkspacePathGuardTests(unittest.TestCase): with workspace_path_access_scope(None, None), workspace_write_namespace_scope("ops"): r = spec.handler({"path": abs_target, "content": "print('ok')\n", "mode": "overwrite"}) self.assertTrue(r.get("ok"), r) - expected = (self.root / "data" / "workspace" / "count_items.py").resolve() + expected = (self.root / "count_items.py").resolve() self.assertEqual(str(expected), str(r.get("path"))) self.assertTrue(expected.exists())