mirror of
https://github.com/rookiestar28/ComfyUI-OpenClaw.git
synced 2026-08-14 00:48:07 +00:00
fix(tooling): resolve package-owned tool allowlist
This commit is contained in:
@@ -3,8 +3,8 @@ Debug script for S35 Transform Isolation.
|
||||
Verifies that the correct executor (TransformProcessRunner) is allowed/loaded.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Ensure project root is in path
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
@@ -3,8 +3,8 @@ Verify S30 Security Doctor output.
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add project root to path
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
"""
|
||||
Dependency-light runtime dependency/cache hygiene contract.
|
||||
|
||||
This module intentionally avoids importing config, ComfyUI, or stateful runtime
|
||||
services. It documents ownership boundaries and provides path helpers that work
|
||||
in source checkouts and packaged layouts.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional, Union
|
||||
|
||||
PathLike = Union[str, os.PathLike[str]]
|
||||
|
||||
RUNTIME_DEPENDENCY_HYGIENE_CONTRACT_VERSION = 1
|
||||
|
||||
_PACKAGE_RESOURCES: Dict[str, str] = {
|
||||
"tools_allowlist": "data/tools_allowlist.json",
|
||||
}
|
||||
|
||||
_STATE_RUNTIME_PATHS: Dict[str, str] = {
|
||||
"runtime_cache": "cache",
|
||||
"tool_sandbox": "tool_sandbox",
|
||||
}
|
||||
|
||||
_RUNTIME_DEPENDENCY_HYGIENE_CONTRACT: Dict[str, Any] = {
|
||||
"version": RUNTIME_DEPENDENCY_HYGIENE_CONTRACT_VERSION,
|
||||
"package_resources": [
|
||||
{
|
||||
"id": "tools_allowlist",
|
||||
"path": _PACKAGE_RESOURCES["tools_allowlist"],
|
||||
"owner": "package",
|
||||
"mutable": False,
|
||||
"default_for": "services.tool_runner.ToolRunner",
|
||||
"override": "OPENCLAW_TOOLS_CONFIG_PATH",
|
||||
"rationale": (
|
||||
"shipped safe defaults must not be masked by mutable state-dir "
|
||||
"or bind-mounted source artifacts"
|
||||
),
|
||||
},
|
||||
],
|
||||
"state_owned_runtime_paths": [
|
||||
{
|
||||
"id": "runtime_cache",
|
||||
"path": _STATE_RUNTIME_PATHS["runtime_cache"],
|
||||
"owner": "state_dir",
|
||||
"tracked": False,
|
||||
"cleanup": "preserve_unless_operator_requests_state_cleanup",
|
||||
"rationale": "runtime cache belongs under configured OpenClaw state",
|
||||
},
|
||||
{
|
||||
"id": "tool_sandbox",
|
||||
"path": _STATE_RUNTIME_PATHS["tool_sandbox"],
|
||||
"owner": "state_dir",
|
||||
"tracked": False,
|
||||
"cleanup": "preserve_unless_operator_requests_state_cleanup",
|
||||
"rationale": "tool execution scratch space belongs under state-dir",
|
||||
},
|
||||
],
|
||||
"repo_local_generated_caches": [
|
||||
{
|
||||
"id": "validation_tool_cache",
|
||||
"path": ".tmp/",
|
||||
"owner": "validation_tooling",
|
||||
"tracked": False,
|
||||
"cleanup": "safe_to_delete_when_tools_are_not_running",
|
||||
},
|
||||
{
|
||||
"id": "python_venv_windows",
|
||||
"path": ".venv/",
|
||||
"owner": "local_python_environment",
|
||||
"tracked": False,
|
||||
"cleanup": "recreate_from_project_dependencies",
|
||||
},
|
||||
{
|
||||
"id": "frontend_dependencies",
|
||||
"path": "node_modules/",
|
||||
"owner": "npm",
|
||||
"tracked": False,
|
||||
"cleanup": "recreate_with_npm_ci",
|
||||
},
|
||||
],
|
||||
"managed_runtime_dependency_cache": {
|
||||
"status": "not_implemented",
|
||||
"automatic_repair": False,
|
||||
"operator_action": "manual",
|
||||
"rationale": (
|
||||
"OpenClaw must not delete, migrate, or repair runtime dependency "
|
||||
"caches without an explicit future implementation and acceptance gate"
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _package_root_from_module() -> Path:
|
||||
return Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def resolve_package_resource_path(
|
||||
resource_id: str, package_root: Optional[PathLike] = None
|
||||
) -> str:
|
||||
"""Return an absolute path for a package-owned resource."""
|
||||
try:
|
||||
relative_path = _PACKAGE_RESOURCES[resource_id]
|
||||
except KeyError as exc:
|
||||
known = ", ".join(sorted(_PACKAGE_RESOURCES))
|
||||
raise KeyError(f"unknown package resource '{resource_id}'; known: {known}") from exc
|
||||
|
||||
root = (
|
||||
Path(package_root).resolve()
|
||||
if package_root is not None
|
||||
else _package_root_from_module()
|
||||
)
|
||||
return str((root / relative_path).resolve())
|
||||
|
||||
|
||||
def resolve_state_owned_runtime_path(
|
||||
path_id: str, state_dir: PathLike, create: bool = False
|
||||
) -> str:
|
||||
"""Return an absolute path for a state-dir-owned runtime path."""
|
||||
try:
|
||||
relative_path = _STATE_RUNTIME_PATHS[path_id]
|
||||
except KeyError as exc:
|
||||
known = ", ".join(sorted(_STATE_RUNTIME_PATHS))
|
||||
raise KeyError(f"unknown runtime path '{path_id}'; known: {known}") from exc
|
||||
|
||||
resolved = (Path(state_dir).resolve() / relative_path).resolve()
|
||||
if create:
|
||||
resolved.mkdir(parents=True, exist_ok=True)
|
||||
return str(resolved)
|
||||
|
||||
|
||||
def get_runtime_dependency_hygiene_contract() -> Dict[str, Any]:
|
||||
return copy.deepcopy(_RUNTIME_DEPENDENCY_HYGIENE_CONTRACT)
|
||||
@@ -140,14 +140,15 @@ class ToolRunner:
|
||||
self._sandbox_runtime_issue: Optional[str] = None
|
||||
self._config_path = config_path or os.environ.get("OPENCLAW_TOOLS_CONFIG_PATH")
|
||||
if not self._config_path:
|
||||
# Default to data/tools_allowlist.json (shipped default)
|
||||
# IMPORTANT: default allowlist is package-owned, not state-dir-owned.
|
||||
try:
|
||||
from config import DATA_DIR
|
||||
from .runtime_dependency_hygiene import resolve_package_resource_path
|
||||
except Exception:
|
||||
from services.runtime_dependency_hygiene import ( # type: ignore
|
||||
resolve_package_resource_path,
|
||||
)
|
||||
|
||||
self._config_path = os.path.join(DATA_DIR, "tools_allowlist.json")
|
||||
except ImportError:
|
||||
# Fallback for unconnected tests
|
||||
self._config_path = "data/tools_allowlist.json"
|
||||
self._config_path = resolve_package_resource_path("tools_allowlist")
|
||||
|
||||
self.reload_config()
|
||||
|
||||
|
||||
@@ -71,9 +71,7 @@ class R173PackageHygieneTests(unittest.TestCase):
|
||||
self.assertEqual(retained_artifacts["package-lock.json"]["owner"], "frontend")
|
||||
self.assertIn("npm ci", retained_artifacts["package-lock.json"]["rationale"])
|
||||
|
||||
cache_owners = {
|
||||
cache["id"]: cache for cache in contract["cache_ownership"]
|
||||
}
|
||||
cache_owners = {cache["id"]: cache for cache in contract["cache_ownership"]}
|
||||
self.assertEqual(cache_owners["runtime_state_cache"]["owner"], "state_dir")
|
||||
self.assertFalse(cache_owners["runtime_state_cache"]["tracked"])
|
||||
self.assertEqual(
|
||||
@@ -81,9 +79,7 @@ class R173PackageHygieneTests(unittest.TestCase):
|
||||
"safe_to_delete_when_tools_are_not_running",
|
||||
)
|
||||
|
||||
helper_paths = {
|
||||
helper["path"] for helper in contract["developer_helpers"]
|
||||
}
|
||||
helper_paths = {helper["path"] for helper in contract["developer_helpers"]}
|
||||
self.assertEqual(
|
||||
helper_paths,
|
||||
{
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from services.tool_runner import ToolRunner
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class TestR191RuntimeDependencyHygiene(unittest.TestCase):
|
||||
def test_package_allowlist_resource_resolves_under_package_root(self):
|
||||
try:
|
||||
from services.runtime_dependency_hygiene import (
|
||||
resolve_package_resource_path,
|
||||
)
|
||||
except ImportError as exc:
|
||||
self.fail(f"missing runtime dependency hygiene helper: {exc}")
|
||||
|
||||
allowlist_path = Path(
|
||||
resolve_package_resource_path("tools_allowlist", package_root=ROOT)
|
||||
)
|
||||
|
||||
self.assertEqual(allowlist_path, ROOT / "data" / "tools_allowlist.json")
|
||||
self.assertTrue(allowlist_path.exists())
|
||||
|
||||
def test_package_resource_resolution_supports_packaged_layouts(self):
|
||||
try:
|
||||
from services.runtime_dependency_hygiene import (
|
||||
resolve_package_resource_path,
|
||||
)
|
||||
except ImportError as exc:
|
||||
self.fail(f"missing runtime dependency hygiene helper: {exc}")
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
package_root = Path(temp_dir) / "openclaw_package"
|
||||
package_data = package_root / "data"
|
||||
package_data.mkdir(parents=True)
|
||||
expected = package_data / "tools_allowlist.json"
|
||||
expected.write_text('{"tools": []}', encoding="utf-8")
|
||||
|
||||
resolved = Path(
|
||||
resolve_package_resource_path(
|
||||
"tools_allowlist", package_root=package_root
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(resolved, expected)
|
||||
|
||||
def test_tool_runner_default_uses_package_allowlist_not_state_dir(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
state_dir = Path(temp_dir) / "state"
|
||||
bind_mount_cwd = Path(temp_dir) / "bind_mount"
|
||||
state_dir.mkdir()
|
||||
bind_mount_cwd.mkdir()
|
||||
original_cwd = os.getcwd()
|
||||
|
||||
with patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"OPENCLAW_STATE_DIR": str(state_dir),
|
||||
"MOLTBOT_STATE_DIR": "",
|
||||
"OPENCLAW_TOOLS_CONFIG_PATH": "",
|
||||
},
|
||||
):
|
||||
try:
|
||||
os.chdir(bind_mount_cwd)
|
||||
runner = ToolRunner()
|
||||
finally:
|
||||
os.chdir(original_cwd)
|
||||
|
||||
self.assertEqual(
|
||||
Path(runner._config_path),
|
||||
ROOT / "data" / "tools_allowlist.json",
|
||||
)
|
||||
self.assertIn("example_echo", {tool["name"] for tool in runner.list_tools()})
|
||||
|
||||
def test_explicit_tools_config_path_still_overrides_default(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
custom_allowlist = Path(temp_dir) / "custom_tools.json"
|
||||
custom_allowlist.write_text(
|
||||
"""
|
||||
{
|
||||
"tools": [
|
||||
{
|
||||
"name": "custom_echo",
|
||||
"command": ["echo", "{message}"],
|
||||
"args": {"message": "^[a-z]+$"}
|
||||
}
|
||||
]
|
||||
}
|
||||
""",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
with patch.dict(
|
||||
os.environ,
|
||||
{"OPENCLAW_TOOLS_CONFIG_PATH": str(custom_allowlist)},
|
||||
):
|
||||
runner = ToolRunner()
|
||||
|
||||
self.assertEqual(Path(runner._config_path), custom_allowlist)
|
||||
self.assertEqual({"custom_echo"}, {tool["name"] for tool in runner.list_tools()})
|
||||
|
||||
def test_runtime_cache_contract_keeps_generated_paths_separate(self):
|
||||
try:
|
||||
from services.runtime_dependency_hygiene import (
|
||||
get_runtime_dependency_hygiene_contract,
|
||||
resolve_state_owned_runtime_path,
|
||||
)
|
||||
except ImportError as exc:
|
||||
self.fail(f"missing runtime dependency hygiene helper: {exc}")
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
state_dir = Path(temp_dir) / "state"
|
||||
runtime_cache = Path(
|
||||
resolve_state_owned_runtime_path("runtime_cache", state_dir=state_dir)
|
||||
)
|
||||
tool_sandbox = Path(
|
||||
resolve_state_owned_runtime_path("tool_sandbox", state_dir=state_dir)
|
||||
)
|
||||
|
||||
contract = get_runtime_dependency_hygiene_contract()
|
||||
generated_repo_cache_paths = {
|
||||
item["path"] for item in contract["repo_local_generated_caches"]
|
||||
}
|
||||
|
||||
self.assertEqual(runtime_cache, state_dir / "cache")
|
||||
self.assertEqual(tool_sandbox, state_dir / "tool_sandbox")
|
||||
self.assertIn(".tmp/", generated_repo_cache_paths)
|
||||
self.assertEqual(
|
||||
contract["managed_runtime_dependency_cache"]["status"],
|
||||
"not_implemented",
|
||||
)
|
||||
self.assertFalse(
|
||||
contract["managed_runtime_dependency_cache"]["automatic_repair"]
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user