mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-19 06:43:17 +08:00
对齐 run_command 与 write_file 的默认路径行为。
将 run_command 在未传 cwd 时改为默认 data/workspace,并将 write_file 调整为相对路径写入 data/workspace、绝对路径直写(经路径守卫校验),同步更新测试与文档。 Made-with: Cursor
This commit is contained in:
parent
d0a66d190d
commit
e06184cc2f
5 changed files with 15 additions and 15 deletions
|
|
@ -39,3 +39,8 @@ This project exposes local atomic capabilities as shared `public` tools for all
|
||||||
|
|
||||||
- Adapter path: `runtime/tools/local_sdk/adapter.py`.
|
- Adapter path: `runtime/tools/local_sdk/adapter.py`.
|
||||||
- Uses a self-implemented local backend (cross-platform) with a stable tool contract.
|
- 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`.
|
||||||
|
|
|
||||||
|
|
@ -73,8 +73,9 @@ class LocalAdapter:
|
||||||
try:
|
try:
|
||||||
timeout_s = max(1, min(int(timeout or 30), 600))
|
timeout_s = max(1, min(int(timeout or 30), 600))
|
||||||
# run_command never follows adapter cd state.
|
# run_command never follows adapter cd state.
|
||||||
# It only uses explicit cwd; otherwise defaults to workspace root (".").
|
# It only uses explicit cwd; otherwise defaults to data/workspace.
|
||||||
workdir = str(resolve_workspace_path(cwd or "."))
|
workdir = str(resolve_workspace_path(cwd or "data/workspace"))
|
||||||
|
Path(workdir).mkdir(parents=True, exist_ok=True)
|
||||||
run_kwargs: dict[str, Any] = {
|
run_kwargs: dict[str, Any] = {
|
||||||
"cwd": workdir,
|
"cwd": workdir,
|
||||||
"shell": True,
|
"shell": True,
|
||||||
|
|
|
||||||
|
|
@ -8,24 +8,17 @@ from oclaw.runtime.tools.path_guard import resolve_workspace_path
|
||||||
|
|
||||||
|
|
||||||
def write_file_tool() -> ToolSpec:
|
def write_file_tool() -> ToolSpec:
|
||||||
def _sandbox_base_dir() -> Path:
|
|
||||||
return Path("data") / "workspace"
|
|
||||||
|
|
||||||
def _normalize_write_path(path: str) -> str:
|
def _normalize_write_path(path: str) -> str:
|
||||||
raw = str(path or "").strip().strip('"').strip("'")
|
raw = str(path or "").strip().strip('"').strip("'")
|
||||||
if not raw:
|
if not raw:
|
||||||
raise ValueError("path_required")
|
raise ValueError("path_required")
|
||||||
p = Path(raw)
|
p = Path(raw)
|
||||||
base = _sandbox_base_dir()
|
|
||||||
if p.is_absolute():
|
if p.is_absolute():
|
||||||
name = str(p.name or "").strip()
|
return raw
|
||||||
if not name:
|
|
||||||
raise ValueError("path_required")
|
|
||||||
return str(base / name)
|
|
||||||
rel = raw.lstrip("./\\")
|
rel = raw.lstrip("./\\")
|
||||||
if not rel:
|
if not rel:
|
||||||
raise ValueError("path_required")
|
raise ValueError("path_required")
|
||||||
return str(base / rel)
|
return str(Path("data") / "workspace" / rel)
|
||||||
|
|
||||||
def _handler(args: dict[str, Any]) -> dict[str, Any]:
|
def _handler(args: dict[str, Any]) -> dict[str, Any]:
|
||||||
path = str(args.get("path") or "").strip()
|
path = str(args.get("path") or "").strip()
|
||||||
|
|
|
||||||
|
|
@ -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:
|
def test_run_command_does_not_follow_cd_state(tmp_path: Path, monkeypatch) -> None:
|
||||||
monkeypatch.setenv("OPS_WORKSPACE_ROOT", str(tmp_path))
|
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").mkdir(parents=True, exist_ok=True)
|
||||||
(tmp_path / "subdir" / "echo_dir.py").write_text(
|
(tmp_path / "subdir" / "echo_dir.py").write_text(
|
||||||
"import os\nprint(os.path.basename(os.getcwd()))\n",
|
"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)
|
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
|
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.
|
# 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(str(tmp_path.name))
|
assert str(out_run.get("cwd") or "").replace("\\", "/").rstrip("/").endswith("/data/workspace")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -134,7 +134,7 @@ class WorkspacePathGuardTests(unittest.TestCase):
|
||||||
self.assertEqual(str(expected), str(r.get("path")))
|
self.assertEqual(str(expected), str(r.get("path")))
|
||||||
self.assertTrue(expected.exists())
|
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(
|
with mock.patch.dict(
|
||||||
os.environ,
|
os.environ,
|
||||||
{"OPS_WORKSPACE_ROOT": str(self.root), "OPS_WORKSPACE_EXTRA_ROOTS": "", "OPS_WORKSPACE_ALLOW_ANY_PATH": ""},
|
{"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"):
|
with workspace_path_access_scope(None, None), workspace_write_namespace_scope("ops"):
|
||||||
r = spec.handler({"path": abs_target, "content": "print('ok')\n", "mode": "overwrite"})
|
r = spec.handler({"path": abs_target, "content": "print('ok')\n", "mode": "overwrite"})
|
||||||
self.assertTrue(r.get("ok"), r)
|
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.assertEqual(str(expected), str(r.get("path")))
|
||||||
self.assertTrue(expected.exists())
|
self.assertTrue(expected.exists())
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue