mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-22 09:34:53 +02:00
[eric] agents: forward-rebase fixups (renamed-symbol test fixes, schedule-gate test, Cron disallow)
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user