fix(tooling): resolve package-owned tool allowlist

This commit is contained in:
rookiestar28
2026-06-04 14:55:36 +08:00
parent db9067a380
commit 9784328e40
6 changed files with 291 additions and 14 deletions
+1 -1
View File
@@ -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]
+1 -1
View File
@@ -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]
+137
View File
@@ -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)
+7 -6
View File
@@ -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()
+2 -6
View File
@@ -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()