[eric] agents: forward-rebase fixups (renamed-symbol test fixes, schedule-gate test, Cron disallow)

This commit is contained in:
ciregenz
2026-06-24 19:34:05 -07:00
parent 366ba7befd
commit 0121f8aa9d
16 changed files with 172 additions and 19 deletions
@@ -0,0 +1 @@
import __editable___debug_0_1_finder; __editable___debug_0_1_finder.install()
@@ -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
@@ -0,0 +1 @@
pip
@@ -0,0 +1,3 @@
Metadata-Version: 2.4
Name: debug
Version: 0.1
@@ -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
@@ -0,0 +1,5 @@
Wheel-Version: 1.0
Generator: setuptools (82.0.1)
Root-Is-Purelib: true
Tag: py3-none-any
@@ -0,0 +1 @@
{"dir_info": {"editable": true}, "url": "file:///Users/ericzeng/openswarm/debugger"}
@@ -0,0 +1,3 @@
debug
debugger_backend
swarm_debug
@@ -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
@@ -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,
+1 -1
View File
@@ -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()
+13 -13
View File
@@ -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)
+1 -1
View File
@@ -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)
+38
View File
@@ -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"
+3 -3
View File
@@ -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