From 0121f8aa9d928cab95ec64644827cddb5b5aec40 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Wed, 24 Jun 2026 19:29:10 -0700 Subject: [PATCH] [eric] agents: forward-rebase fixups (renamed-symbol test fixes, schedule-gate test, Cron disallow) --- .../site-packages/__editable__.debug-0.1.pth | 1 + .../__editable___debug_0_1_finder.py | 85 +++++++++++++++++++ .../debug-0.1.dist-info/INSTALLER | 1 + .../debug-0.1.dist-info/METADATA | 3 + .../site-packages/debug-0.1.dist-info/RECORD | 10 +++ .../debug-0.1.dist-info/REQUESTED | 0 .../site-packages/debug-0.1.dist-info/WHEEL | 5 ++ .../debug-0.1.dist-info/direct_url.json | 1 + .../debug-0.1.dist-info/top_level.txt | 3 + .../permissions/build_effective_tool_lists.py | 6 ++ .../agents/manager/permissions/gate_hooks.py | 2 +- backend/tests/conftest.py | 2 +- backend/tests/test_free_trial.py | 26 +++--- backend/tests/test_gate_hooks.py | 2 +- backend/tests/test_schedule_gate.py | 38 +++++++++ backend/tests/test_ssrf_guard.py | 6 +- 16 files changed, 172 insertions(+), 19 deletions(-) create mode 100644 .venv/lib/python3.12/site-packages/__editable__.debug-0.1.pth create mode 100644 .venv/lib/python3.12/site-packages/__editable___debug_0_1_finder.py create mode 100644 .venv/lib/python3.12/site-packages/debug-0.1.dist-info/INSTALLER create mode 100644 .venv/lib/python3.12/site-packages/debug-0.1.dist-info/METADATA create mode 100644 .venv/lib/python3.12/site-packages/debug-0.1.dist-info/RECORD create mode 100644 .venv/lib/python3.12/site-packages/debug-0.1.dist-info/REQUESTED create mode 100644 .venv/lib/python3.12/site-packages/debug-0.1.dist-info/WHEEL create mode 100644 .venv/lib/python3.12/site-packages/debug-0.1.dist-info/direct_url.json create mode 100644 .venv/lib/python3.12/site-packages/debug-0.1.dist-info/top_level.txt create mode 100644 backend/tests/test_schedule_gate.py diff --git a/.venv/lib/python3.12/site-packages/__editable__.debug-0.1.pth b/.venv/lib/python3.12/site-packages/__editable__.debug-0.1.pth new file mode 100644 index 00000000..51ea9e83 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/__editable__.debug-0.1.pth @@ -0,0 +1 @@ +import __editable___debug_0_1_finder; __editable___debug_0_1_finder.install() \ No newline at end of file diff --git a/.venv/lib/python3.12/site-packages/__editable___debug_0_1_finder.py b/.venv/lib/python3.12/site-packages/__editable___debug_0_1_finder.py new file mode 100644 index 00000000..6928740c --- /dev/null +++ b/.venv/lib/python3.12/site-packages/__editable___debug_0_1_finder.py @@ -0,0 +1,85 @@ +from __future__ import annotations +import sys +from importlib.machinery import ModuleSpec, PathFinder +from importlib.machinery import all_suffixes as module_suffixes +from importlib.util import spec_from_file_location +from itertools import chain +from pathlib import Path + +MAPPING: dict[str, str] = {'debug': '/Users/ericzeng/openswarm/debugger/debug', 'debugger_backend': '/Users/ericzeng/openswarm/debugger/debugger_backend', 'swarm_debug': '/Users/ericzeng/openswarm/debugger/swarm_debug'} +NAMESPACES: dict[str, list[str]] = {} +PATH_PLACEHOLDER = '__editable__.debug-0.1.finder' + ".__path_hook__" + + +class _EditableFinder: # MetaPathFinder + @classmethod + def find_spec(cls, fullname: str, path=None, target=None) -> ModuleSpec | None: # type: ignore + # Top-level packages and modules (we know these exist in the FS) + if fullname in MAPPING: + pkg_path = MAPPING[fullname] + return cls._find_spec(fullname, Path(pkg_path)) + + # Handle immediate children modules (required for namespaces to work) + # To avoid problems with case sensitivity in the file system we delegate + # to the importlib.machinery implementation. + parent, _, child = fullname.rpartition(".") + if parent and parent in MAPPING: + return PathFinder.find_spec(fullname, path=[MAPPING[parent]]) + + # Other levels of nesting should be handled automatically by importlib + # using the parent path. + return None + + @classmethod + def _find_spec(cls, fullname: str, candidate_path: Path) -> ModuleSpec | None: + init = candidate_path / "__init__.py" + candidates = (candidate_path.with_suffix(x) for x in module_suffixes()) + for candidate in chain([init], candidates): + if candidate.exists(): + return spec_from_file_location(fullname, candidate) + return None + + +class _EditableNamespaceFinder: # PathEntryFinder + @classmethod + def _path_hook(cls, path) -> type[_EditableNamespaceFinder]: + if path == PATH_PLACEHOLDER: + return cls + raise ImportError + + @classmethod + def _paths(cls, fullname: str) -> list[str]: + paths = NAMESPACES[fullname] + if not paths and fullname in MAPPING: + paths = [MAPPING[fullname]] + # Always add placeholder, for 2 reasons: + # 1. __path__ cannot be empty for the spec to be considered namespace. + # 2. In the case of nested namespaces, we need to force + # import machinery to query _EditableNamespaceFinder again. + return [*paths, PATH_PLACEHOLDER] + + @classmethod + def find_spec(cls, fullname: str, target=None) -> ModuleSpec | None: # type: ignore + if fullname in NAMESPACES: + spec = ModuleSpec(fullname, None, is_package=True) + spec.submodule_search_locations = cls._paths(fullname) + return spec + return None + + @classmethod + def find_module(cls, _fullname) -> None: + return None + + +def install(): + if not any(finder == _EditableFinder for finder in sys.meta_path): + sys.meta_path.append(_EditableFinder) + + if not NAMESPACES: + return + + if not any(hook == _EditableNamespaceFinder._path_hook for hook in sys.path_hooks): + # PathEntryFinder is needed to create NamespaceSpec without private APIS + sys.path_hooks.append(_EditableNamespaceFinder._path_hook) + if PATH_PLACEHOLDER not in sys.path: + sys.path.append(PATH_PLACEHOLDER) # Used just to trigger the path hook diff --git a/.venv/lib/python3.12/site-packages/debug-0.1.dist-info/INSTALLER b/.venv/lib/python3.12/site-packages/debug-0.1.dist-info/INSTALLER new file mode 100644 index 00000000..a1b589e3 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/debug-0.1.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/.venv/lib/python3.12/site-packages/debug-0.1.dist-info/METADATA b/.venv/lib/python3.12/site-packages/debug-0.1.dist-info/METADATA new file mode 100644 index 00000000..4fcd764d --- /dev/null +++ b/.venv/lib/python3.12/site-packages/debug-0.1.dist-info/METADATA @@ -0,0 +1,3 @@ +Metadata-Version: 2.4 +Name: debug +Version: 0.1 diff --git a/.venv/lib/python3.12/site-packages/debug-0.1.dist-info/RECORD b/.venv/lib/python3.12/site-packages/debug-0.1.dist-info/RECORD new file mode 100644 index 00000000..8c4d7e6c --- /dev/null +++ b/.venv/lib/python3.12/site-packages/debug-0.1.dist-info/RECORD @@ -0,0 +1,10 @@ +__editable__.debug-0.1.pth,sha256=3gb_QWGnQwMgBfr3v5ndUddKBDY-JleCRPUknZyNgaE,77 +__editable___debug_0_1_finder.py,sha256=NHhjhJuuOamrYv8PNHj5KlZkeF17gzsoI-dIDGbAZZY,3525 +__pycache__/__editable___debug_0_1_finder.cpython-312.pyc,, +debug-0.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +debug-0.1.dist-info/METADATA,sha256=huNnjSeuKc7zQtv_Ikqwslvtb-pbiEKHsDLaGTN4B7k,47 +debug-0.1.dist-info/RECORD,, +debug-0.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +debug-0.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91 +debug-0.1.dist-info/direct_url.json,sha256=I20zIUah6lwWoBLBJ2dL9xbVy-1x8dgDfuP99h1A7Zs,84 +debug-0.1.dist-info/top_level.txt,sha256=cFCDd6FctPi5vQnP1GRJvs04cgf7UNR7prAh1v_99VY,35 diff --git a/.venv/lib/python3.12/site-packages/debug-0.1.dist-info/REQUESTED b/.venv/lib/python3.12/site-packages/debug-0.1.dist-info/REQUESTED new file mode 100644 index 00000000..e69de29b diff --git a/.venv/lib/python3.12/site-packages/debug-0.1.dist-info/WHEEL b/.venv/lib/python3.12/site-packages/debug-0.1.dist-info/WHEEL new file mode 100644 index 00000000..14a883f2 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/debug-0.1.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: setuptools (82.0.1) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/.venv/lib/python3.12/site-packages/debug-0.1.dist-info/direct_url.json b/.venv/lib/python3.12/site-packages/debug-0.1.dist-info/direct_url.json new file mode 100644 index 00000000..fef2184e --- /dev/null +++ b/.venv/lib/python3.12/site-packages/debug-0.1.dist-info/direct_url.json @@ -0,0 +1 @@ +{"dir_info": {"editable": true}, "url": "file:///Users/ericzeng/openswarm/debugger"} \ No newline at end of file diff --git a/.venv/lib/python3.12/site-packages/debug-0.1.dist-info/top_level.txt b/.venv/lib/python3.12/site-packages/debug-0.1.dist-info/top_level.txt new file mode 100644 index 00000000..5af5bb2b --- /dev/null +++ b/.venv/lib/python3.12/site-packages/debug-0.1.dist-info/top_level.txt @@ -0,0 +1,3 @@ +debug +debugger_backend +swarm_debug diff --git a/backend/apps/agents/manager/permissions/build_effective_tool_lists.py b/backend/apps/agents/manager/permissions/build_effective_tool_lists.py index 5db6e029..f4dc9c45 100644 --- a/backend/apps/agents/manager/permissions/build_effective_tool_lists.py +++ b/backend/apps/agents/manager/permissions/build_effective_tool_lists.py @@ -8,6 +8,7 @@ from typing import Dict, List, Tuple from typeguard import typechecked from backend.apps.agents.core.models import AgentSession +from backend.apps.agents.manager.permissions import path_gate from backend.apps.agents.manager.prompt.tool_catalog import ( FULL_TOOLS, get_all_known_tool_names, @@ -98,4 +99,9 @@ def build_effective_tool_lists( for wt_name in ("WebSearch", "WebFetch"): if wt_name not in effective_disallowed: effective_disallowed.append(wt_name) + # Claude's internal Cron* scheduler is denied in favour of the visible native + # one; withhold it from the SDK so the model doesn't even reach for it. + for bt in path_gate.p_CLAUDE_INTERNAL_SCHEDULER_TOOLS: + if bt not in effective_disallowed: + effective_disallowed.append(bt) return effective_allowed, effective_disallowed diff --git a/backend/apps/agents/manager/permissions/gate_hooks.py b/backend/apps/agents/manager/permissions/gate_hooks.py index 442c4190..486577bc 100644 --- a/backend/apps/agents/manager/permissions/gate_hooks.py +++ b/backend/apps/agents/manager/permissions/gate_hooks.py @@ -15,7 +15,7 @@ from claude_agent_sdk import PermissionResultAllow, PermissionResultDeny from backend.apps.agents.core.ws_manager import ws_manager from backend.apps.settings.settings import load_settings from backend.apps.agents.manager.permissions import path_gate -from backend.apps.agents.manager.permissions.decision import effective_policy, request_user_approval +from backend.apps.agents.manager.permissions.decision import effective_policy from backend.apps.agents.manager.prompt.tool_catalog import gated_mcp_server_names from backend.apps.agents.manager.prompt.prompt_context import ( TOOLSEARCH_LOOP_THRESHOLD, diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 9c21e925..262c8b5d 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -41,7 +41,7 @@ def _isolate_browser_state(monkeypatch): # where ITS env var points, not where the first test's pointed try: from backend.apps.agents.browser import browser_metrics as _bm - _bm._metrics_dir_cache = None + _bm.p_metrics_dir_cache = None except Exception: pass _reset() diff --git a/backend/tests/test_free_trial.py b/backend/tests/test_free_trial.py index dafb1f2c..7cd73aa0 100644 --- a/backend/tests/test_free_trial.py +++ b/backend/tests/test_free_trial.py @@ -12,7 +12,7 @@ from backend.apps.agents.core.error_classify import ( ) from backend.apps.agents.providers.registry import resolve_model_id_for_sdk from backend.apps.subscription import free_trial as ft -from backend.apps.subscription.free_trial import _has_own_model, arm_free_trial, clear_free_trial +from backend.apps.subscription.free_trial import has_own_model, arm_free_trial, clear_free_trial def test_proxy_auth_for_each_mode(): @@ -62,11 +62,11 @@ def test_generic_cli_failure_uses_sdk_system_events_for_rate_limits(): ) -def test_has_own_model_never_shadows_a_real_provider(): - assert not _has_own_model(AppSettings(connection_mode="free-trial", free_trial_token="x")) - assert not _has_own_model(AppSettings()) - assert _has_own_model(AppSettings(anthropic_api_key="sk-ant-x")) - assert _has_own_model( +def testhas_own_model_never_shadows_a_real_provider(): + assert not has_own_model(AppSettings(connection_mode="free-trial", free_trial_token="x")) + assert not has_own_model(AppSettings()) + assert has_own_model(AppSettings(anthropic_api_key="sk-ant-x")) + assert has_own_model( AppSettings(connection_mode="openswarm-pro", openswarm_bearer_token="b") ) @@ -78,7 +78,7 @@ async def test_arm_waits_for_9router_before_shadowing_a_background_started_sub(m visible) BEFORE deciding, instead of arming the free trial over it.""" saved: list = [] monkeypatch.setattr(ft, "save_settings_async", _record(saved)) - monkeypatch.setattr(ft, "_sync_routing", _noop) + monkeypatch.setattr(ft, "p_sync_routing", _noop) started = {"called": False} @@ -91,7 +91,7 @@ async def test_arm_waits_for_9router_before_shadowing_a_background_started_sub(m import backend.apps.nine_router as nr monkeypatch.setattr(nr, "ensure_running", fake_ensure_running) - monkeypatch.setattr(ft, "_has_connected_subscription", sub_visible_after_start) + monkeypatch.setattr(ft, "p_has_connected_subscription", sub_visible_after_start) s = AppSettings() # no key, own_key mode: a subscription-only user out = await arm_free_trial(s) @@ -107,7 +107,7 @@ async def test_arm_tolerates_provider_load_lag(monkeypatch): """9Router's /api/providers can lag is_running on a cold start. arm must re-check a few times so a sub that loads a beat late is still caught, not shadowed.""" monkeypatch.setattr(ft, "save_settings_async", _noop) - monkeypatch.setattr(ft, "_sync_routing", _noop) + monkeypatch.setattr(ft, "p_sync_routing", _noop) async def fake_ensure_running(): return None @@ -119,7 +119,7 @@ async def test_arm_tolerates_provider_load_lag(monkeypatch): import backend.apps.nine_router as nr monkeypatch.setattr(nr, "ensure_running", fake_ensure_running) - monkeypatch.setattr(ft, "_has_connected_subscription", lagging_sub) + monkeypatch.setattr(ft, "p_has_connected_subscription", lagging_sub) s = AppSettings() res = await ft.arm_free_trial(s) @@ -140,10 +140,10 @@ async def test_arm_with_no_sub_is_bounded_and_falls_through_to_arm(monkeypatch): import time import backend.apps.nine_router as nr monkeypatch.setattr(nr, "ensure_running", fake_ensure_running) - monkeypatch.setattr(ft, "_has_connected_subscription", never_sub) + monkeypatch.setattr(ft, "p_has_connected_subscription", never_sub) # Short-circuit before the cloud mint so the test stays offline + deterministic; # reaching this branch proves arm did NOT falsely conclude has_model. - monkeypatch.setattr(ft, "_fingerprint", lambda _s: None) + monkeypatch.setattr(ft, "p_fingerprint", lambda _s: None) s = AppSettings() t = time.monotonic() @@ -156,7 +156,7 @@ async def test_arm_with_no_sub_is_bounded_and_falls_through_to_arm(monkeypatch): @pytest.mark.asyncio async def test_clear_reverts_forced_haiku_so_it_doesnt_outlive_the_trial(monkeypatch): monkeypatch.setattr(ft, "save_settings_async", _noop) - monkeypatch.setattr(ft, "_sync_routing", _noop) + monkeypatch.setattr(ft, "p_sync_routing", _noop) s = AppSettings(connection_mode="free-trial", free_trial_token="ftk", default_model="haiku") await clear_free_trial(s) diff --git a/backend/tests/test_gate_hooks.py b/backend/tests/test_gate_hooks.py index 48efbdfa..29ce1561 100644 --- a/backend/tests/test_gate_hooks.py +++ b/backend/tests/test_gate_hooks.py @@ -46,7 +46,7 @@ async def test_can_use_tool_deny_returns_deny(): async def test_can_use_tool_ask_routes_through_approval(): ctx = p_ctx() with patch.object(gate_hooks.path_gate, "maybe_override_policy", return_value=("ask", None)), \ - patch.object(gate_hooks, "request_user_approval", new=AsyncMock(return_value=ApprovalDecision(behavior="allow"))): + patch.object(gate_hooks, "p_resolve_ask", new=AsyncMock(return_value=ApprovalDecision(behavior="allow"))): result = await gate_hooks.can_use_tool(ctx, "Write", {"file_path": "/x"}, None) assert isinstance(result, PermissionResultAllow) diff --git a/backend/tests/test_schedule_gate.py b/backend/tests/test_schedule_gate.py new file mode 100644 index 00000000..514f27fc --- /dev/null +++ b/backend/tests/test_schedule_gate.py @@ -0,0 +1,38 @@ +"""The always-on openswarm-schedule MCP must never fall through to always_allow: +its committing tools force an approval and Claude's internal Cron* tools are denied, +even when the user set everything to always_allow. This is the unattended-widen guard +the scheduled-tasks PR shipped without a test, and the exact path most at risk in the +agent-manager decomposition (the gating moved from agent_manager into path_gate).""" + +from backend.apps.agents.manager.permissions import path_gate +from backend.apps.agents.manager.permissions.workflow_approval import p_is_claude_schedule_skill + + +def test_schedule_commit_tools_force_ask_even_when_always_allow(): + for tool in ( + "mcp__openswarm-schedule__ScheduleWorkflow", + "mcp__openswarm-schedule__UpdateScheduledWorkflow", + "mcp__openswarm-schedule__DeleteScheduledWorkflow", + "mcp__openswarm-schedule__PauseAllWorkflows", + ): + policy, _ = path_gate.maybe_override_policy("always_allow", tool, {}) + assert policy == "ask", f"{tool} must force an approval, not silently always_allow" + + +def test_claude_internal_cron_tools_denied(): + for tool in ("CronCreate", "CronList", "CronDelete"): + policy, _ = path_gate.maybe_override_policy("always_allow", tool, {}) + assert policy == "deny", f"{tool} must be denied in favour of the native scheduler" + + +def test_claude_schedule_skill_detected(): + assert p_is_claude_schedule_skill("Skill", {"skill": "schedule"}) + assert p_is_claude_schedule_skill("Skill", {"skill": "Schedule"}) + assert not p_is_claude_schedule_skill("Skill", {"skill": "other"}) + assert not p_is_claude_schedule_skill("Bash", {"skill": "schedule"}) + assert not p_is_claude_schedule_skill("Skill", "not a dict") + + +def test_normal_tool_unaffected_by_schedule_gate(): + policy, _ = path_gate.maybe_override_policy("always_allow", "Read", {}) + assert policy == "always_allow" diff --git a/backend/tests/test_ssrf_guard.py b/backend/tests/test_ssrf_guard.py index b8490afd..f98c046c 100644 --- a/backend/tests/test_ssrf_guard.py +++ b/backend/tests/test_ssrf_guard.py @@ -8,7 +8,7 @@ comprehension-gap probe against this module. import pytest -from apps.agents.tools.ssrf_guard import SSRFBlocked, _is_forbidden_ip, assert_safe_url +from backend.apps.agents.tools.ssrf_guard import SSRFBlocked, p_is_forbidden_ip, assert_safe_url @pytest.mark.parametrize( @@ -31,8 +31,8 @@ from apps.agents.tools.ssrf_guard import SSRFBlocked, _is_forbidden_ip, assert_s ("::ffff:8.8.8.8", False), # v4-mapped public stays allowed ], ) -def test_is_forbidden_ip(ip_str, forbidden): - assert _is_forbidden_ip(ip_str) is forbidden +def testp_is_forbidden_ip(ip_str, forbidden): + assert p_is_forbidden_ip(ip_str) is forbidden @pytest.mark.asyncio