From 10aef1ff18b2adb565a13d85339390d6ad0d4613 Mon Sep 17 00:00:00 2001 From: ciregenz Date: Tue, 23 Jun 2026 20:35:06 -0700 Subject: [PATCH] [eric] swarm+nine_router: leading-_ -> p_/public across swarm entities/closure/redact/ziputil + nine_router process/sync/oauth; promote cross-file public, fix monkeypatch refs --- backend/apps/nine_router/oauth.py | 46 ++++----- backend/apps/nine_router/process.py | 120 +++++++++++----------- backend/apps/nine_router/sync.py | 22 ++-- backend/apps/nine_router/sync_custom.py | 40 ++++---- backend/apps/swarm/closure.py | 62 +++++------ backend/apps/swarm/entities/apps.py | 16 +-- backend/apps/swarm/entities/dashboards.py | 20 ++-- backend/apps/swarm/entities/modes.py | 14 +-- backend/apps/swarm/entities/sessions.py | 10 +- backend/apps/swarm/entities/skills.py | 18 ++-- backend/apps/swarm/entities/workflows.py | 26 ++--- backend/apps/swarm/redact.py | 8 +- backend/apps/swarm/swarm.py | 30 +++--- backend/apps/swarm/ziputil.py | 12 +-- backend/tests/test_skills_folders.py | 4 +- backend/tests/test_swarm_bundle.py | 12 +-- 16 files changed, 230 insertions(+), 230 deletions(-) diff --git a/backend/apps/nine_router/oauth.py b/backend/apps/nine_router/oauth.py index d7b34666..291ef033 100644 --- a/backend/apps/nine_router/oauth.py +++ b/backend/apps/nine_router/oauth.py @@ -22,9 +22,9 @@ logger = logging.getLogger(__name__) # 1455 that serves the same postMessage/BroadcastChannel/localStorage relay so # the frontend's existing popup + msgHandler flow works unchanged. -_CODEX_CALLBACK_PORT = 1455 -_CODEX_CALLBACK_PATH = "/auth/callback" -_CODEX_CALLBACK_HTML = b""" +P_CODEX_CALLBACK_PORT = 1455 +P_CODEX_CALLBACK_PATH = "/auth/callback" +P_CODEX_CALLBACK_HTML = b""" Authorization Complete """ -async def _start_codex_callback_listener(timeout: float = 300.0) -> asyncio.base_events.Server | None: +async def p_start_codex_callback_listener(timeout: float = 300.0) -> asyncio.base_events.Server | None: """Spawn a one-shot HTTP listener on 127.0.0.1:1455 for the Codex OAuth callback. - Serves GET /auth/callback with _CODEX_CALLBACK_HTML. After serving the + Serves GET /auth/callback with P_CODEX_CALLBACK_HTML. After serving the callback (or after `timeout` seconds with no callback) the listener closes itself in a background task. Safe to call even if 1455 is busy , logs the collision and returns None so start_oauth can still proceed and @@ -96,7 +96,7 @@ async def _start_codex_callback_listener(timeout: float = 300.0) -> asyncio.base path = parts[1] if len(parts) >= 2 else "" method = parts[0] if parts else "" - if method == "GET" and path.startswith(_CODEX_CALLBACK_PATH): + if method == "GET" and path.startswith(P_CODEX_CALLBACK_PATH): # Parse code/state out of the query string and exchange # server-side before serving the HTML. Duplicate exchanges # are harmless (single-use auth codes fail the second call, @@ -136,7 +136,7 @@ async def _start_codex_callback_listener(timeout: float = 300.0) -> asyncio.base except Exception as e: logger.debug(f"Codex callback listener pre-exchange error: {e}") - body = _CODEX_CALLBACK_HTML + body = P_CODEX_CALLBACK_HTML response = ( b"HTTP/1.1 200 OK\r\n" b"Content-Type: text/html; charset=utf-8\r\n" @@ -165,12 +165,12 @@ async def _start_codex_callback_listener(timeout: float = 300.0) -> asyncio.base pass try: - server = await asyncio.start_server(_handle, "127.0.0.1", _CODEX_CALLBACK_PORT) + server = await asyncio.start_server(_handle, "127.0.0.1", P_CODEX_CALLBACK_PORT) except OSError as e: # Port already in use; probably another Codex connect attempt still # running, or an actual Codex CLI process holding 1455. Log and bail. logger.warning( - f"Could not start Codex callback listener on port {_CODEX_CALLBACK_PORT}: {e}. " + f"Could not start Codex callback listener on port {P_CODEX_CALLBACK_PORT}: {e}. " "If another connection attempt is in progress, wait for it to finish or time out." ) return None @@ -194,7 +194,7 @@ async def _start_codex_callback_listener(timeout: float = 300.0) -> asyncio.base pass asyncio.create_task(_lifecycle()) - logger.info(f"Started Codex callback listener on http://localhost:{_CODEX_CALLBACK_PORT}{_CODEX_CALLBACK_PATH}") + logger.info(f"Started Codex callback listener on http://localhost:{P_CODEX_CALLBACK_PORT}{P_CODEX_CALLBACK_PATH}") return server @@ -211,15 +211,15 @@ async def _start_codex_callback_listener(timeout: float = 300.0) -> asyncio.base # in one cookie jar. # The callback for gemini-cli/antigravity lands on /api/subscriptions/callback # and runs the exchange server-side; codex uses its fixed 1455 listener; claude -# is special-cased in _callback_uri_for_provider below. -_EXTERNAL_BROWSER_PROVIDERS: set[str] = {"gemini-cli", "antigravity", "codex", "claude"} +# is special-cased in p_callback_uri_for_provider below. +P_EXTERNAL_BROWSER_PROVIDERS: set[str] = {"gemini-cli", "antigravity", "codex", "claude"} -def _should_use_external_browser(provider: str) -> bool: - return provider in _EXTERNAL_BROWSER_PROVIDERS +def p_should_use_external_browser(provider: str) -> bool: + return provider in P_EXTERNAL_BROWSER_PROVIDERS -def _backend_port() -> int: +def p_backend_port() -> int: """Best-effort lookup of the OpenSwarm backend HTTP port. Falls back to 8324 (the default in backend/main.py) if OPENSWARM_PORT @@ -233,27 +233,27 @@ def _backend_port() -> int: return 8324 -def _callback_uri_for_provider(provider: str) -> str: +def p_callback_uri_for_provider(provider: str) -> str: """Return the redirect URI to pass to 9Router's authorize endpoint. Most providers accept 9Router's built-in callback page at port 20128. Special cases: - Codex/OpenAI's OAuth client is bound to a fixed http://localhost:1455/auth/callback URI; handled by - _start_codex_callback_listener above. + p_start_codex_callback_listener above. - Gemini/Google's OAuth consent page rejects embedded browsers, so we route the callback through OpenSwarm's backend endpoint at /api/subscriptions/callback (backend/main.py) which runs the exchange itself. """ if provider == "codex": - return f"http://localhost:{_CODEX_CALLBACK_PORT}{_CODEX_CALLBACK_PATH}" + return f"http://localhost:{P_CODEX_CALLBACK_PORT}{P_CODEX_CALLBACK_PATH}" # Anthropic's OAuth client only whitelists localhost:20128/callback; # 9router_gpt5_patch.js 302-rewrites the hit to the backend handler. if provider == "claude": return f"http://localhost:{NINE_ROUTER_PORT}/callback" - if provider in _EXTERNAL_BROWSER_PROVIDERS: - return f"http://localhost:{_backend_port()}/api/subscriptions/callback" + if provider in P_EXTERNAL_BROWSER_PROVIDERS: + return f"http://localhost:{p_backend_port()}/api/subscriptions/callback" return f"http://localhost:{NINE_ROUTER_PORT}/callback" @@ -279,9 +279,9 @@ async def start_oauth(provider: str) -> dict: except Exception: pass - callback_url = _callback_uri_for_provider(provider) + callback_url = p_callback_uri_for_provider(provider) if provider == "codex": - await _start_codex_callback_listener() + await p_start_codex_callback_listener() r = await client.get( f"{NINE_ROUTER_API}/oauth/{provider}/authorize", @@ -295,7 +295,7 @@ async def start_oauth(provider: str) -> dict: "code_verifier": data.get("codeVerifier", ""), "state": data.get("state", ""), "redirect_uri": callback_url, - "use_external_browser": _should_use_external_browser(provider), + "use_external_browser": p_should_use_external_browser(provider), } diff --git a/backend/apps/nine_router/process.py b/backend/apps/nine_router/process.py index 6e63119a..453eb052 100644 --- a/backend/apps/nine_router/process.py +++ b/backend/apps/nine_router/process.py @@ -63,43 +63,43 @@ NINE_ROUTER_NPM_VERSION = os.environ.get("OPENSWARM_ROUTER_VERSION", "0.3.60") # 1. rotate that log before we spawn 9Router when it gets large, so growth can't run away; # 2. give node an explicit, generous heap ceiling for legitimate large multimodal bodies. # Neither touches routing, so WebSearch/WebFetch translation and the 0.3.60 pin are unaffected. -_REQUEST_LOG_PATH = os.path.expanduser("~/.9router/request-details.json") -_REQUEST_LOG_MAX_BYTES = 5 * 1024 * 1024 -_NODE_HEAP_MB = 4096 +P_REQUEST_LOG_PATH = os.path.expanduser("~/.9router/request-details.json") +P_REQUEST_LOG_MAX_BYTES = 5 * 1024 * 1024 +P_NODE_HEAP_MB = 4096 -def _rotate_request_log() -> None: +def p_rotate_request_log() -> None: """Rotate ~/.9router/request-details.json to a single .0 backup when it grows past the cap, BEFORE 9Router is spawned (never racing a live writer). 9Router recreates a fresh file, exactly like a clean install. The only consumer is the 'most recent 5' reasoning-token lookup, which already tolerates an empty/missing file, so no feature loses data it depends on.""" try: - if os.path.exists(_REQUEST_LOG_PATH) and os.path.getsize(_REQUEST_LOG_PATH) > _REQUEST_LOG_MAX_BYTES: - os.replace(_REQUEST_LOG_PATH, _REQUEST_LOG_PATH + ".0") + if os.path.exists(P_REQUEST_LOG_PATH) and os.path.getsize(P_REQUEST_LOG_PATH) > P_REQUEST_LOG_MAX_BYTES: + os.replace(P_REQUEST_LOG_PATH, P_REQUEST_LOG_PATH + ".0") logger.info( "9Router request log rotated (exceeded %d MB) to avoid the router OOM", - _REQUEST_LOG_MAX_BYTES // (1024 * 1024), + P_REQUEST_LOG_MAX_BYTES // (1024 * 1024), ) except Exception as e: logger.debug("9Router request-log rotation skipped: %s", e) -_process: subprocess.Popen | None = None +p_process: subprocess.Popen | None = None # Serializes ensure_running() so a background auto-start and a concurrent # dispatch-time ensure can't both spawn 9Router (double-bind on :20128). Lazily # created so module import doesn't require a running event loop. -_start_lock: "asyncio.Lock | None" = None +p_start_lock: "asyncio.Lock | None" = None # Short TTL cache for positive is_running() results. The probe is a sync # httpx.get that blocks the event loop, and under load (9Router busy # streaming inference) it can exceed its 2s timeout and return False even # though 9Router is fine. Caching a recent True result avoids those false -# negatives without masking a real crash for more than _IS_RUNNING_TTL seconds. +# negatives without masking a real crash for more than P_IS_RUNNING_TTL seconds. # Negative results are NOT cached so startup detection in ensure_running() # remains correct. -_IS_RUNNING_TTL = 10.0 -_is_running_last_ok: float = 0.0 +P_IS_RUNNING_TTL = 10.0 +p_is_running_last_ok: float = 0.0 def is_running() -> bool: @@ -115,9 +115,9 @@ def is_running() -> bool: first; a down 9Router is detected in <~0.3s instead of ~7s. Only when the port is open do we do the HTTP confirm. 9Router binds 0.0.0.0 (the warm app reaches it via 127.0.0.1 today), so this changes timing, not reachability.""" - global _is_running_last_ok + global p_is_running_last_ok now = time.monotonic() - if now - _is_running_last_ok < _IS_RUNNING_TTL: + if now - p_is_running_last_ok < P_IS_RUNNING_TTL: return True try: with socket.create_connection(("127.0.0.1", NINE_ROUTER_PORT), timeout=0.3): @@ -127,14 +127,14 @@ def is_running() -> bool: try: r = httpx.get(f"http://127.0.0.1:{NINE_ROUTER_PORT}/v1/models", timeout=2.0) if r.status_code == 200: - _is_running_last_ok = now + p_is_running_last_ok = now return True return False except Exception: return False -def _nine_router_data_dir() -> str: +def p_nine_router_data_dir() -> str: """Where 9Router persists machine-id + auth/cli-secret, the two files we hash into the /api/* auth token on 0.4.x. Mirrors 9Router's own default (DATA_DIR env, else ~/.9router on unix, %APPDATA%/9router on win) so we read @@ -151,7 +151,7 @@ def _nine_router_data_dir() -> str: return os.path.join(os.path.expanduser("~"), ".9router") -_cli_token_cache: str | None = None +p_cli_token_cache: str | None = None def cli_auth_token() -> str | None: @@ -162,13 +162,13 @@ def cli_auth_token() -> str | None: when missing so connect/sync can auth before that self-call. Returns None on 0.3.60 (no machine-id) or when 9Router isn't up, so the caller sends no header and the old auth-free path is untouched. Never raises.""" - global _cli_token_cache - if _cli_token_cache: - return _cli_token_cache + global p_cli_token_cache + if p_cli_token_cache: + return p_cli_token_cache if not is_running(): return None try: - data_dir = _nine_router_data_dir() + data_dir = p_nine_router_data_dir() try: with open(os.path.join(data_dir, "machine-id"), encoding="utf-8") as f: machine_id = f.read().strip() @@ -198,7 +198,7 @@ def cli_auth_token() -> str | None: tok = hashlib.sha256( (machine_id + "9r-cli-auth" + cli_secret).encode("utf-8") ).hexdigest()[:16] - _cli_token_cache = tok + p_cli_token_cache = tok return tok except Exception: return None @@ -211,7 +211,7 @@ def cli_auth_headers() -> dict[str, str]: return {"x-9r-cli-token": tok} if tok else {} -def _find_9router_dir() -> str | None: +def p_find_9router_dir() -> str | None: """Locate the bundled 9Router directory (works in both dev and packaged mode).""" _is_packaged = os.environ.get("OPENSWARM_PACKAGED") == "1" @@ -231,7 +231,7 @@ def _find_9router_dir() -> str | None: return None -def _gpt5_patch_path() -> str | None: +def p_gpt5_patch_path() -> str | None: """Absolute path to backend/apps/agents/9router_gpt5_patch.js, used as `node --require ` when spawning 9router. @@ -254,7 +254,7 @@ def _gpt5_patch_path() -> str | None: return candidate if os.path.exists(candidate) else None -def _find_node() -> str | None: +def p_find_node() -> str | None: """Find a Node.js binary (works in both dev and packaged mode). Priority order: @@ -284,7 +284,7 @@ def _find_node() -> str | None: return None -def _dev_router_cache_dir() -> str: +def p_dev_router_cache_dir() -> str: """Cache dir for the npm 9router package used in dev mode. Pinned per version so bumping NINE_ROUTER_NPM_VERSION triggers a fresh @@ -296,7 +296,7 @@ def _dev_router_cache_dir() -> str: return os.path.join(base, "openswarm-router", NINE_ROUTER_NPM_VERSION) -def _ensure_router_cached() -> str | None: +def p_ensure_router_cached() -> str | None: """Ensure the npm 9router package is installed in the dev cache. Returns the absolute path to `app/server.js` on success, or None if @@ -308,7 +308,7 @@ def _ensure_router_cached() -> str | None: no update-check spinner, and no accidental-quit foot-gun when a non-developer right-clicks the "9" tray icon and picks Quit. """ - cache_dir = _dev_router_cache_dir() + cache_dir = p_dev_router_cache_dir() server_js = os.path.join(cache_dir, "node_modules", "9router", "app", "server.js") if os.path.exists(server_js): return server_js @@ -348,7 +348,7 @@ def _ensure_router_cached() -> str | None: return server_js if os.path.exists(server_js) else None -def _read_capture_tail(path: str, limit: int = 6000) -> str: +def p_read_capture_tail(path: str, limit: int = 6000) -> str: """Tail of the 9Router start-capture file, where the real spawn error lands. Best-effort; empty string on any hiccup so telemetry never breaks boot.""" try: @@ -361,7 +361,7 @@ def _read_capture_tail(path: str, limit: int = 6000) -> str: return "" -def _report_start_failure(reason: str, *, detail: str = "", **fields: Any) -> None: +def p_report_start_failure(reason: str, *, detail: str = "", **fields: Any) -> None: """9Router didn't come up. Log it and ship a scrubbed diagnostic so a user's 'every model exits 1' is finally explained from our side instead of a silent warning. The stderr tail can echo an own_key, so it rides the same scrub as @@ -386,16 +386,16 @@ def _report_start_failure(reason: str, *, detail: str = "", **fields: Any) -> No async def ensure_running(): """Start 9Router if not already running. Serialized so concurrent callers (the background auto-start + a dispatch-time ensure) can't double-spawn.""" - global _start_lock - if _start_lock is None: - _start_lock = asyncio.Lock() - async with _start_lock: - await _ensure_running_impl() + global p_start_lock + if p_start_lock is None: + p_start_lock = asyncio.Lock() + async with p_start_lock: + await p_ensure_running_impl() -async def _ensure_running_impl(): +async def p_ensure_running_impl(): """Start 9Router if not already running.""" - global _process + global p_process _is_packaged = os.environ.get("OPENSWARM_PACKAGED") == "1" if is_running(): @@ -421,9 +421,9 @@ async def _ensure_running_impl(): else: logger.info("9Router already running on port %d", NINE_ROUTER_PORT) return - _rotate_request_log() - _9router_dir = _find_9router_dir() - _patch = _gpt5_patch_path() + p_rotate_request_log() + _9router_dir = p_find_9router_dir() + _patch = p_gpt5_patch_path() if _is_packaged: # Packaged: run the pre-built standalone server staged at @@ -431,20 +431,20 @@ async def _ensure_running_impl(): # fall back to the dev npm path here, a user machine has no npm, so that # only ever fails silently; every miss is reported instead. if not _9router_dir: - _report_start_failure("router_not_bundled") + p_report_start_failure("router_not_bundled") return standalone_server = os.path.join(_9router_dir, "server.js") if not os.path.exists(standalone_server): standalone_server = os.path.join(_9router_dir, ".next", "standalone", "server.js") if not os.path.exists(standalone_server): - _report_start_failure("server_missing", router_dir_found=True) + p_report_start_failure("server_missing", router_dir_found=True) return - node = _find_node() + node = p_find_node() if not node: - _report_start_failure("node_not_found", router_dir_found=True, server_found=True) + p_report_start_failure("node_not_found", router_dir_found=True, server_found=True) return logger.info("Starting 9Router (production) on port %d...", NINE_ROUTER_PORT) - cmd = [node, f"--max-old-space-size={_NODE_HEAP_MB}"] + (["--require", _patch] if _patch else []) + [standalone_server] + cmd = [node, f"--max-old-space-size={P_NODE_HEAP_MB}"] + (["--require", _patch] if _patch else []) + [standalone_server] cwd = os.path.dirname(standalone_server) env = {**os.environ, "PORT": str(NINE_ROUTER_PORT), "NODE_ENV": "production"} if node == os.environ.get("OPENSWARM_ELECTRON_PATH"): @@ -453,10 +453,10 @@ async def _ensure_running_impl(): # Dev: install the pinned npm package into a local cache once, then spawn # `node app/server.js` directly (bypasses the package cli.js tray icon # users confusingly quit, its update-check spinner, and the TUI). - cached_server = _ensure_router_cached() + cached_server = p_ensure_router_cached() if not cached_server: return - node = _find_node() + node = p_find_node() if not node: logger.warning("Node.js not found; cannot start 9Router in dev mode.") return @@ -464,7 +464,7 @@ async def _ensure_running_impl(): "Starting 9Router (dev cache, 9router@%s) on port %d...", NINE_ROUTER_NPM_VERSION, NINE_ROUTER_PORT, ) - cmd = [node, f"--max-old-space-size={_NODE_HEAP_MB}"] + (["--require", _patch] if _patch else []) + [cached_server] + cmd = [node, f"--max-old-space-size={P_NODE_HEAP_MB}"] + (["--require", _patch] if _patch else []) + [cached_server] cwd = os.path.dirname(cached_server) env = {**os.environ, "PORT": str(NINE_ROUTER_PORT), "NODE_ENV": "production"} @@ -493,7 +493,7 @@ async def _ensure_running_impl(): _stdout, _stderr = subprocess.DEVNULL, subprocess.DEVNULL try: - _process = subprocess.Popen(cmd, cwd=cwd, stdout=_stdout, stderr=_stderr, env=env) + p_process = subprocess.Popen(cmd, cwd=cwd, stdout=_stdout, stderr=_stderr, env=env) if _cap_file is not None: _cap_file.close() # the child holds its own fd; the parent copy isn't needed timeout = 20 if _is_packaged else 30 @@ -504,10 +504,10 @@ async def _ensure_running_impl(): return # Verify-at-boot: it never answered. Report with the captured tail + the # exit code (non-None = it crashed; None = wedged or just slow). - _report_start_failure( + p_report_start_failure( "not_ready_in_time", - detail=_read_capture_tail(_cap_path) if _is_packaged else "", - returncode=_process.poll(), + detail=p_read_capture_tail(_cap_path) if _is_packaged else "", + returncode=p_process.poll(), timeout_s=timeout, ) except Exception as e: @@ -516,25 +516,25 @@ async def _ensure_running_impl(): _cap_file.close() except OSError: pass - _report_start_failure( + p_report_start_failure( "spawn_exception", - detail=f"{e}\n{_read_capture_tail(_cap_path) if _is_packaged else ''}", + detail=f"{e}\n{p_read_capture_tail(_cap_path) if _is_packaged else ''}", ) def stop(): """Stop the 9Router subprocess.""" - global _process - if _process: + global p_process + if p_process: try: - _process.terminate() - _process.wait(timeout=5) + p_process.terminate() + p_process.wait(timeout=5) except Exception: try: - _process.kill() + p_process.kill() except Exception: pass - _process = None + p_process = None logger.info("9Router stopped") diff --git a/backend/apps/nine_router/sync.py b/backend/apps/nine_router/sync.py index f24053db..a38d829e 100644 --- a/backend/apps/nine_router/sync.py +++ b/backend/apps/nine_router/sync.py @@ -12,7 +12,7 @@ from .process import NINE_ROUTER_API, cli_auth_headers logger = logging.getLogger(__name__) -def _nr(): +def nr(): """The package module. is_running / get_providers / httpx are read off it at call time so tests that patch `backend.apps.nine_router.` still take effect after the split (they used to all live on one module).""" @@ -35,9 +35,9 @@ NINE_ROUTER_CLAUDE_PRO_NAME = "OpenSwarm Pro (OpenSwarm-managed)" NINE_ROUTER_OPENAI_KEYED_PREFIX = "cp-openai" -async def _find_keyed_connection(provider: str, name: str) -> dict | None: +async def find_keyed_connection(provider: str, name: str) -> dict | None: """Return the 9Router connection we manage for this provider, if any.""" - conns = await _nr().get_providers() + conns = await nr().get_providers() if not isinstance(conns, list): return None for c in conns: @@ -51,7 +51,7 @@ async def _find_keyed_connection(provider: str, name: str) -> dict | None: return None -async def _sync_apikey_provider( +async def p_sync_apikey_provider( provider: str, api_key: str | None, name: str, @@ -59,12 +59,12 @@ async def _sync_apikey_provider( label: str, ) -> None: """Create/update/delete an OpenSwarm-managed apikey connection. Silent if 9Router is down.""" - if not _nr().is_running(): + if not nr().is_running(): return - existing = await _find_keyed_connection(provider, name) + existing = await find_keyed_connection(provider, name) try: - async with _nr().httpx.AsyncClient(timeout=5.0, headers=cli_auth_headers()) as client: + async with nr().httpx.AsyncClient(timeout=5.0, headers=cli_auth_headers()) as client: if api_key: payload = { "provider": provider, @@ -100,7 +100,7 @@ async def _sync_apikey_provider( async def sync_gemini_api_key(api_key: str | None) -> None: """Mirror google_api_key into 9Router; bypasses Code Assist's tight quota.""" - await _sync_apikey_provider( + await p_sync_apikey_provider( "gemini", api_key, NINE_ROUTER_KEYED_NAME, label="Gemini" ) @@ -128,12 +128,12 @@ async def sync_openai_api_key(api_key: str | None) -> None: above) so 9Router's translator dispatches to this provider-node instead of the built-in `openai` provider. """ - from .sync_custom import _sync_openai_compat_node - await _sync_openai_compat_node(api_key) + from .sync_custom import sync_openai_compat_node + await sync_openai_compat_node(api_key) async def sync_openrouter_api_key(api_key: str | None) -> None: """Mirror openrouter_api_key into 9Router; supplies bearer for openrouter/ routes.""" - await _sync_apikey_provider( + await p_sync_apikey_provider( "openrouter", api_key, NINE_ROUTER_OPENROUTER_KEYED_NAME, label="OpenRouter" ) diff --git a/backend/apps/nine_router/sync_custom.py b/backend/apps/nine_router/sync_custom.py index d6439f3f..fc9c77d1 100644 --- a/backend/apps/nine_router/sync_custom.py +++ b/backend/apps/nine_router/sync_custom.py @@ -14,8 +14,8 @@ from .process import NINE_ROUTER_API, cli_auth_headers from .sync import ( NINE_ROUTER_CLAUDE_PRO_NAME, NINE_ROUTER_OPENAI_KEYED_PREFIX, - _find_keyed_connection, - _nr, + find_keyed_connection, + nr, ) logger = logging.getLogger(__name__) @@ -25,10 +25,10 @@ logger = logging.getLogger(__name__) NINE_ROUTER_CUSTOM_NAME_SUFFIX = " (OpenSwarm-managed)" -async def _sync_openai_compat_node(api_key: str | None) -> None: +async def sync_openai_compat_node(api_key: str | None) -> None: """Create / update / delete the openai-compatible node + connection pair we use to ferry OpenAI requests through openai-passthrough.""" - if not _nr().is_running(): + if not nr().is_running(): return import os as _os port = _os.environ.get("OPENSWARM_PORT", "8324") @@ -36,7 +36,7 @@ async def _sync_openai_compat_node(api_key: str | None) -> None: managed_name = f"OpenAI{NINE_ROUTER_CUSTOM_NAME_SUFFIX}" try: - async with _nr().httpx.AsyncClient(timeout=5.0, headers=cli_auth_headers()) as client: + async with nr().httpx.AsyncClient(timeout=5.0, headers=cli_auth_headers()) as client: r = await client.get(f"{NINE_ROUTER_API}/provider-nodes") existing_nodes = (r.json().get("nodes") if r.status_code == 200 else []) or [] except Exception as e: @@ -50,7 +50,7 @@ async def _sync_openai_compat_node(api_key: str | None) -> None: if not api_key: if existing_node: try: - async with _nr().httpx.AsyncClient(timeout=5.0, headers=cli_auth_headers()) as client: + async with nr().httpx.AsyncClient(timeout=5.0, headers=cli_auth_headers()) as client: await client.delete(f"{NINE_ROUTER_API}/provider-nodes/{existing_node['id']}") logger.info("9Router: removed OpenAI compat node (key cleared)") except Exception as e: @@ -66,7 +66,7 @@ async def _sync_openai_compat_node(api_key: str | None) -> None: } node_id: str | None = existing_node.get("id") if existing_node else None try: - async with _nr().httpx.AsyncClient(timeout=5.0, headers=cli_auth_headers()) as client: + async with nr().httpx.AsyncClient(timeout=5.0, headers=cli_auth_headers()) as client: if existing_node: await client.put( f"{NINE_ROUTER_API}/provider-nodes/{existing_node['id']}", @@ -92,7 +92,7 @@ async def _sync_openai_compat_node(api_key: str | None) -> None: return try: - existing_conn = await _find_keyed_connection(node_id, managed_name) + existing_conn = await find_keyed_connection(node_id, managed_name) conn_payload = { "provider": node_id, "authType": "apikey", @@ -100,7 +100,7 @@ async def _sync_openai_compat_node(api_key: str | None) -> None: "apiKey": api_key, "priority": 0, } - async with _nr().httpx.AsyncClient(timeout=5.0, headers=cli_auth_headers()) as client: + async with nr().httpx.AsyncClient(timeout=5.0, headers=cli_auth_headers()) as client: if existing_conn: await client.patch( f"{NINE_ROUTER_API}/providers/{existing_conn['id']}", @@ -117,7 +117,7 @@ async def _sync_openai_compat_node(api_key: str | None) -> None: logger.warning(f"9Router OpenAI compat connection sync failed: {e}") -def _custom_provider_slug(name: str) -> str: +def p_custom_provider_slug(name: str) -> str: """Slugify a user-supplied custom-provider name for use as a 9Router prefix. Always returns a non-empty alnum-and-dash string.""" import re @@ -157,11 +157,11 @@ async def sync_custom_providers(providers: list) -> None: no longer in `providers` is deleted (which cascades to its connection). Silent no-op when 9Router isn't running. """ - if not _nr().is_running(): + if not nr().is_running(): return try: - async with _nr().httpx.AsyncClient(timeout=5.0, headers=cli_auth_headers()) as client: + async with nr().httpx.AsyncClient(timeout=5.0, headers=cli_auth_headers()) as client: r = await client.get(f"{NINE_ROUTER_API}/provider-nodes") existing_nodes = (r.json().get("nodes") if r.status_code == 200 else []) or [] except Exception as e: @@ -187,7 +187,7 @@ async def sync_custom_providers(providers: list) -> None: # Bearer header even with auth disabled. Substitute a placeholder; real # auth deployments always have api_key set. api_key = api_key.strip() or "no-auth-required" - slug = _custom_provider_slug(name) + slug = p_custom_provider_slug(name) prefix = f"cp-{slug}" seen_prefixes.add(prefix) managed_name = f"{name.strip()}{NINE_ROUTER_CUSTOM_NAME_SUFFIX}" @@ -201,7 +201,7 @@ async def sync_custom_providers(providers: list) -> None: "type": "openai-compatible", } try: - async with _nr().httpx.AsyncClient(timeout=5.0, headers=cli_auth_headers()) as client: + async with nr().httpx.AsyncClient(timeout=5.0, headers=cli_auth_headers()) as client: if node: await client.put( f"{NINE_ROUTER_API}/provider-nodes/{node['id']}", @@ -228,7 +228,7 @@ async def sync_custom_providers(providers: list) -> None: continue try: - existing_conn = await _find_keyed_connection(node_id, managed_name) + existing_conn = await find_keyed_connection(node_id, managed_name) conn_payload = { "provider": node_id, "authType": "apikey", @@ -236,7 +236,7 @@ async def sync_custom_providers(providers: list) -> None: "apiKey": api_key, "priority": 0, } - async with _nr().httpx.AsyncClient(timeout=5.0, headers=cli_auth_headers()) as client: + async with nr().httpx.AsyncClient(timeout=5.0, headers=cli_auth_headers()) as client: if existing_conn: await client.patch( f"{NINE_ROUTER_API}/providers/{existing_conn['id']}", @@ -259,7 +259,7 @@ async def sync_custom_providers(providers: list) -> None: if prefix in seen_prefixes: continue try: - async with _nr().httpx.AsyncClient(timeout=5.0, headers=cli_auth_headers()) as client: + async with nr().httpx.AsyncClient(timeout=5.0, headers=cli_auth_headers()) as client: await client.delete(f"{NINE_ROUTER_API}/provider-nodes/{node['id']}") logger.info(f"9Router: removed orphaned custom node {prefix}") except Exception as e: @@ -280,15 +280,15 @@ async def sync_openswarm_pro_as_claude(bearer_token: str | None, proxy_url: str the OpenSwarm-Pro-backed Claude connection and routes the search call through our cloud; same quota the user's Pro subscription already covers, no extra cost.""" - if not _nr().is_running(): + if not nr().is_running(): return # 9Router's POST /api/providers only accepts direct-API provider ids # for apikey auth; `claude` is the subscription/IDE id, `anthropic` # is the direct-API id. Use `anthropic`. - existing = await _find_keyed_connection("anthropic", NINE_ROUTER_CLAUDE_PRO_NAME) + existing = await find_keyed_connection("anthropic", NINE_ROUTER_CLAUDE_PRO_NAME) try: - async with _nr().httpx.AsyncClient(timeout=5.0, headers=cli_auth_headers()) as client: + async with nr().httpx.AsyncClient(timeout=5.0, headers=cli_auth_headers()) as client: if bearer_token and proxy_url: payload = { "provider": "anthropic", diff --git a/backend/apps/swarm/closure.py b/backend/apps/swarm/closure.py index becca123..34fb750c 100644 --- a/backend/apps/swarm/closure.py +++ b/backend/apps/swarm/closure.py @@ -31,15 +31,15 @@ from .registry import IMPORT_ORDER, get_exportable from .ziputil import MANIFEST_NAME, BundleError, has_member, is_zip, pack, read_manifest, unpack, verify_checksum -def _now() -> str: +def p_now() -> str: return datetime.now(timezone.utc).isoformat() -def _created_with() -> str: +def p_created_with() -> str: return os.environ.get("OPENSWARM_VERSION") or "OpenSwarm" -class _Ctx: +class p_Ctx: def __init__(self, local_to_bundle: dict[tuple, str]): self._m = local_to_bundle @@ -49,7 +49,7 @@ class _Ctx: # ---------- export ---------- -def _assemble(root_type: EntityType, root_id: str): +def p_assemble(root_type: EntityType, root_id: str): root_cls = get_exportable(root_type) if root_cls is None: raise BundleError(f"can't share a {root_type.value} yet") @@ -79,7 +79,7 @@ def _assemble(root_type: EntityType, root_id: str): queue.append((dep.type, dep.local_id, dinst)) local_to_bundle = {key: uuid4().hex for key in order} - ctx = _Ctx(local_to_bundle) + ctx = p_Ctx(local_to_bundle) payloads: dict[str, dict] = {} files: dict[str, bytes] = {} entities: list[EntityRef] = [] @@ -102,11 +102,11 @@ def _assemble(root_type: EntityType, root_id: str): edges.append(DependencyEdge(from_=bid, to=local_to_bundle[dkey], relation=dep.relation)) requirements.extend(inst.requirements()) - requirements = _dedupe_requirements(requirements) + requirements = p_dedupe_requirements(requirements) root_bid = local_to_bundle[(root_type, root_id)] manifest = Manifest( - created_with=_created_with(), - created_at=_now(), + created_with=p_created_with(), + created_at=p_now(), bundle_id=uuid4().hex, root=EntityRef(type=root_type, bundle_id=root_bid, name=root.name, path=f"entities/{root_bid}"), entities=entities, @@ -123,16 +123,16 @@ def _assemble(root_type: EntityType, root_id: str): def build_manifest(root_type: EntityType, root_id: str) -> Manifest: - return _assemble(root_type, root_id)[0] + return p_assemble(root_type, root_id)[0] def build_bundle(root_type: EntityType, root_id: str) -> tuple[bytes, str]: - manifest, payloads, files = _assemble(root_type, root_id) + manifest, payloads, files = p_assemble(root_type, root_id) raw = pack(manifest.model_dump(by_alias=True, mode="json"), payloads, files) return raw, manifest.root.name -def _dedupe_requirements(reqs: list[Requirement]) -> list[Requirement]: +def p_dedupe_requirements(reqs: list[Requirement]) -> list[Requirement]: out: dict[tuple, Requirement] = {} for r in reqs: k = (r.kind, r.key) @@ -210,24 +210,24 @@ def stage_upload(raw: bytes, filename: str) -> tuple[str, Manifest, list[str]]: shutil.rmtree(sandbox, ignore_errors=True) raise BundleError("this .swarm was made by a newer OpenSwarm; please update") return sandbox, manifest, warnings - return _stage_skill_from_zip(raw, filename, warnings) - return _stage_skill_from_markdown(raw, filename, warnings) + return stage_skill_from_zip(raw, filename, warnings) + return p_stage_skill_from_markdown(raw, filename, warnings) -def _name_from_filename(filename: str) -> str: +def p_name_from_filename(filename: str) -> str: base = os.path.splitext(os.path.basename(filename or "skill"))[0] return base.replace("-", " ").replace("_", " ").strip().title() or "Imported Skill" -def _stage_skill_from_markdown(raw: bytes, filename: str, warnings: list[str]): +def p_stage_skill_from_markdown(raw: bytes, filename: str, warnings: list[str]): try: content = raw.decode("utf-8") except UnicodeDecodeError: raise BundleError("unrecognized file; expected a .swarm or a .md skill") - return _synth_single_skill(content, _name_from_filename(filename), warnings) + return p_synth_single_skill(content, p_name_from_filename(filename), warnings) -def _stage_skill_from_zip(raw: bytes, filename: str, warnings: list[str]): +def stage_skill_from_zip(raw: bytes, filename: str, warnings: list[str]): with zipfile.ZipFile(io.BytesIO(raw)) as zf: mds = [n for n in zf.namelist() if n.lower().endswith(".md") and not n.endswith("/")] target = next((n for n in mds if os.path.basename(n).lower() == "skill.md"), None) @@ -253,10 +253,10 @@ def _stage_skill_from_zip(raw: bytes, filename: str, warnings: list[str]): warnings.append("some oversized/extra supporting files were skipped") continue extra_files[rel] = zf.read(n) - return _synth_single_skill(content, _name_from_filename(filename), warnings, extra_files) + return p_synth_single_skill(content, p_name_from_filename(filename), warnings, extra_files) -def _synth_single_skill(content: str, name: str, warnings: list[str], extra_files: dict[str, bytes] | None = None): +def p_synth_single_skill(content: str, name: str, warnings: list[str], extra_files: dict[str, bytes] | None = None): bid = uuid4().hex sandbox = tempfile.mkdtemp(prefix="swarm-import-") edir = os.path.join(sandbox, "entities", bid) @@ -266,10 +266,10 @@ def _synth_single_skill(content: str, name: str, warnings: list[str], extra_file with open(os.path.join(edir, "payload.json"), "w", encoding="utf-8") as f: json.dump(payload, f) # Supporting files ride the same entities//files/ channel the - # commit reader (_read_files) feeds into import_, so a zip-of-SKILL.md + # commit reader (p_read_files) feeds into import_, so a zip-of-SKILL.md # round-trips as a folder skill instead of getting flattened. for rel, data in (extra_files or {}).items(): - dest = _safe_join(edir, os.path.join("files", rel)) + dest = p_safe_join(edir, os.path.join("files", rel)) os.makedirs(os.path.dirname(dest), exist_ok=True) with open(dest, "wb") as f: f.write(data) @@ -285,7 +285,7 @@ def _synth_single_skill(content: str, name: str, warnings: list[str], extra_file # ---------- import: commit ---------- -def _safe_join(sandbox: str, rel: str) -> str: +def p_safe_join(sandbox: str, rel: str) -> str: dest = os.path.realpath(os.path.join(sandbox, rel)) root = os.path.realpath(sandbox) if dest != root and not dest.startswith(root + os.sep): @@ -293,14 +293,14 @@ def _safe_join(sandbox: str, rel: str) -> str: return dest -def _read_payload(sandbox: str, ref: EntityRef) -> dict: - path = _safe_join(sandbox, os.path.join(ref.path, "payload.json")) +def p_read_payload(sandbox: str, ref: EntityRef) -> dict: + path = p_safe_join(sandbox, os.path.join(ref.path, "payload.json")) with open(path, encoding="utf-8") as f: return json.load(f) -def _read_files(sandbox: str, ref: EntityRef) -> dict[str, bytes]: - base = _safe_join(sandbox, os.path.join(ref.path, "files")) +def p_read_files(sandbox: str, ref: EntityRef) -> dict[str, bytes]: + base = p_safe_join(sandbox, os.path.join(ref.path, "files")) out: dict[str, bytes] = {} if not os.path.isdir(base): return out @@ -326,7 +326,7 @@ def review_bundle(sandbox: str, manifest: Manifest): if e.type != EntityType.app: continue any_app = True - r = scan_app_files(_read_files(sandbox, e)) + r = scan_app_files(p_read_files(sandbox, e)) findings.extend(r.findings) scanned.extend(r.scanned_files) if r.verdict != "clean": @@ -341,13 +341,13 @@ def detect_conflicts(sandbox: str, manifest: Manifest) -> list[IncludeItem]: check = getattr(cls, "conflict", None) if cls else None if not check: continue - msg = check(_read_payload(sandbox, e)) + msg = check(p_read_payload(sandbox, e)) if msg: out.append(IncludeItem(type=e.type, name=e.name, detail=msg)) return out -def _topo_order(manifest: Manifest) -> list[EntityRef]: +def p_topo_order(manifest: Manifest) -> list[EntityRef]: entities = {e.bundle_id: e for e in manifest.entities} deps: dict[str, set[str]] = {bid: set() for bid in entities} for edge in manifest.edges: @@ -372,11 +372,11 @@ def commit(sandbox: str, manifest: Manifest, accept_requirements: list[str]): created: dict[str, list[str]] = {} trail: list[tuple] = [] # (impl_cls, new_local_id) for rollback, newest last try: - for e in _topo_order(manifest): + for e in p_topo_order(manifest): cls = get_exportable(e.type) if cls is None: raise BundleError(f"can't import a {e.type.value} yet") - new_id = cls.import_(_read_payload(sandbox, e), _read_files(sandbox, e), remap) + new_id = cls.import_(p_read_payload(sandbox, e), p_read_files(sandbox, e), remap) remap.assign(e.bundle_id, new_id) created.setdefault(e.type.value, []).append(new_id) trail.append((cls, new_id)) diff --git a/backend/apps/swarm/entities/apps.py b/backend/apps/swarm/entities/apps.py index ca958f90..defa825c 100644 --- a/backend/apps/swarm/entities/apps.py +++ b/backend/apps/swarm/entities/apps.py @@ -18,7 +18,7 @@ from backend.config.paths import OUTPUTS_DIR, OUTPUTS_WORKSPACE_DIR from ..exportable import DepRef, ExportContext, RemapTable from ..models import EntityType, Requirement -_MAX_APP_FILE = 25 * 1024 * 1024 # matches ziputil per-entry cap +P_MAX_APP_FILE = 25 * 1024 * 1024 # matches ziputil per-entry cap class AppExportable: @@ -61,7 +61,7 @@ class AppExportable: if os.path.islink(full): continue try: - if os.path.getsize(full) > _MAX_APP_FILE: + if os.path.getsize(full) > P_MAX_APP_FILE: continue with open(full, "rb") as f: data = f.read() @@ -85,13 +85,13 @@ class AppExportable: for rel, data in files.items(): if not rel.startswith("workspace/"): continue - dest = _safe_join(folder, rel[len("workspace/"):]) + dest = p_safe_join(folder, rel[len("workspace/"):]) os.makedirs(os.path.dirname(dest), exist_ok=True) with open(dest, "wb") as f: f.write(data) wrote_workspace = True if wrote_workspace: - _localize_env(folder) + p_localize_env(folder) o = Output( name=payload.get("name") or "Imported App", @@ -115,7 +115,7 @@ class AppExportable: os.remove(p) -def _safe_join(folder: str, rel: str) -> str: +def p_safe_join(folder: str, rel: str) -> str: dest = os.path.realpath(os.path.join(folder, rel)) root = os.path.realpath(folder) if dest != root and not dest.startswith(root + os.sep): @@ -123,7 +123,7 @@ def _safe_join(folder: str, rel: str) -> str: return dest -def _free_port() -> int: +def p_free_port() -> int: s = socket.socket() try: s.bind(("127.0.0.1", 0)) @@ -132,7 +132,7 @@ def _free_port() -> int: s.close() -def _localize_env(folder: str) -> None: +def p_localize_env(folder: str) -> None: """Regenerate the workspace .env on the importer's machine: a fresh port plus this install's absolute template/debugger paths (the source's were dropped).""" env_path = os.path.join(folder, ".env") @@ -151,7 +151,7 @@ def _localize_env(folder: str) -> None: ) except Exception: return - patch_env_port(env_path, "FRONTEND_PORT", str(_free_port())) + patch_env_port(env_path, "FRONTEND_PORT", str(p_free_port())) patch_env_port(env_path, "OPENSWARM_TEMPLATE_BACKEND_PATH", TEMPLATE_BACKEND_PATH) patch_env_port(env_path, "OPENSWARM_DEBUGGER_PATH", DEBUGGER_PATH) try: diff --git a/backend/apps/swarm/entities/dashboards.py b/backend/apps/swarm/entities/dashboards.py index 65cf9b65..d1544c7f 100644 --- a/backend/apps/swarm/entities/dashboards.py +++ b/backend/apps/swarm/entities/dashboards.py @@ -23,7 +23,7 @@ class DashboardExportable: @classmethod def load(cls, local_id: str) -> "DashboardExportable | None": - data = _read(local_id) + data = p_read(local_id) if data is None: return None return cls(local_id, data.get("name") or "Dashboard", data) @@ -111,21 +111,21 @@ class DashboardExportable: "expanded_session_ids": expanded, }, } - _write(new_did, doc) - _retag_sessions(cards.keys(), new_did) + p_write(new_did, doc) + p_retag_sessions(cards.keys(), new_did) return new_did @classmethod def rollback(cls, local_id: str) -> None: import os - d = _dash_dir() + d = p_dash_dir() if d: p = os.path.join(d, f"{local_id}.json") if os.path.exists(p): os.remove(p) -def _dash_dir() -> str | None: +def p_dash_dir() -> str | None: try: from backend.config.paths import DASHBOARDS_DIR return DASHBOARDS_DIR @@ -133,22 +133,22 @@ def _dash_dir() -> str | None: return None -def _read(did: str) -> dict | None: +def p_read(did: str) -> dict | None: import os from backend.config.json_store import read_json_or_none - d = _dash_dir() + d = p_dash_dir() return read_json_or_none(os.path.join(d, f"{did}.json")) if d else None -def _write(did: str, doc: dict) -> None: +def p_write(did: str, doc: dict) -> None: import os from backend.config.json_store import atomic_write_json - d = _dash_dir() + d = p_dash_dir() if d: atomic_write_json(os.path.join(d, f"{did}.json"), doc) -def _retag_sessions(session_ids, dashboard_id: str) -> None: +def p_retag_sessions(session_ids, dashboard_id: str) -> None: # Best-effort: a hiccup here must not orphan the just-written dashboard. from backend.apps.agents.manager.session.session_store import load_session_data, save_session for sid in session_ids: diff --git a/backend/apps/swarm/entities/modes.py b/backend/apps/swarm/entities/modes.py index 015d3eb9..54ebe22d 100644 --- a/backend/apps/swarm/entities/modes.py +++ b/backend/apps/swarm/entities/modes.py @@ -10,7 +10,7 @@ from ..exportable import DepRef, ExportContext, RemapTable from ..models import EntityType, Requirement # Machine-relative or install-owned fields that must not ride along. -_DROP = {"is_builtin", "default_folder"} +P_DROP = {"is_builtin", "default_folder"} class ModeExportable: @@ -23,7 +23,7 @@ class ModeExportable: @classmethod def load(cls, local_id: str) -> "ModeExportable | None": - store = _store() + store = p_store() if store is None: return None m = store.load_mode(local_id) @@ -33,7 +33,7 @@ class ModeExportable: return cls(local_id, d.get("name") or local_id, d) def serialize(self, ctx: ExportContext) -> dict: - return {k: v for k, v in self._data.items() if k not in _DROP} + return {k: v for k, v in self._data.items() if k not in P_DROP} def files(self) -> dict[str, bytes]: return {} @@ -46,8 +46,8 @@ class ModeExportable: @classmethod def import_(cls, payload: dict, files: dict[str, bytes], remap: RemapTable) -> str: - store = _store() - model = _model() + store = p_store() + model = p_model() if store is None or model is None: from ..ziputil import BundleError raise BundleError("can't import this mode on this build") @@ -63,7 +63,7 @@ class ModeExportable: return mid -def _store(): +def p_store(): try: from backend.apps.modes import modes return modes @@ -71,7 +71,7 @@ def _store(): return None -def _model(): +def p_model(): try: from backend.apps.modes.models import Mode return Mode diff --git a/backend/apps/swarm/entities/sessions.py b/backend/apps/swarm/entities/sessions.py index 4b441dc1..03c421a0 100644 --- a/backend/apps/swarm/entities/sessions.py +++ b/backend/apps/swarm/entities/sessions.py @@ -15,11 +15,11 @@ from uuid import uuid4 from ..exportable import DepRef, ExportContext, RemapTable from ..models import EntityType, Requirement, RequirementKind -_BUILTIN_MODES = {"agent", "ask", "plan", "view-builder", "skill-builder"} +P_BUILTIN_MODES = {"agent", "ask", "plan", "view-builder", "skill-builder"} # Transcript fields ride along so the shared agent keeps its history; ids inside # (message ids, branch ids, their parent/fork refs) are self-consistent within # the one session file, so they carry verbatim with no remap. -_KEEP = ( +P_KEEP = ( "name", "provider", "model", "mode", "system_prompt", "allowed_tools", "max_turns", "thinking_level", "messages", "branches", "active_branch_id", "tool_group_meta", @@ -52,14 +52,14 @@ class SessionExportable: return cls(local_id, d.get("name") or "Agent", d) def serialize(self, ctx: ExportContext) -> dict: - return {k: self._data.get(k) for k in _KEEP if k in self._data} + return {k: self._data.get(k) for k in P_KEEP if k in self._data} def files(self) -> dict[str, bytes]: return {} def dependencies(self) -> list[DepRef]: mode = self._data.get("mode") - if mode and mode not in _BUILTIN_MODES: + if mode and mode not in P_BUILTIN_MODES: return [DepRef(EntityType.mode, mode, "uses_mode")] return [] @@ -71,7 +71,7 @@ class SessionExportable: detail="An agent here uses this action.", )) mode = self._data.get("mode") or "agent" - if mode in _BUILTIN_MODES and mode != "agent": + if mode in P_BUILTIN_MODES and mode != "agent": reqs.append(Requirement( kind=RequirementKind.builtin_mode, key=mode, label=f"{mode} mode", detail="A built-in mode an agent runs in.", diff --git a/backend/apps/swarm/entities/skills.py b/backend/apps/swarm/entities/skills.py index df073f24..69ea854e 100644 --- a/backend/apps/swarm/entities/skills.py +++ b/backend/apps/swarm/entities/skills.py @@ -42,7 +42,7 @@ class SkillExportable: } files: dict[str, bytes] = {} if kind == "folder": - files = _read_supporting_files(os.path.join(store.SKILLS_DIR, local_id)) + files = p_read_supporting_files(os.path.join(store.SKILLS_DIR, local_id)) return cls(local_id, name, payload, files) def serialize(self, ctx: ExportContext) -> dict: @@ -60,14 +60,14 @@ class SkillExportable: @classmethod def conflict(cls, payload: dict) -> str | None: slug = payload.get("slug") or "" - if slug and _slug_taken(slug): + if slug and p_slug_taken(slug): return "already exists; will be added as a copy" return None @classmethod def import_(cls, payload: dict, files: dict[str, bytes], remap: RemapTable) -> str: base = (payload.get("slug") or payload.get("name") or "skill").lower().replace(" ", "-") - slug = _free_slug(base) + slug = p_free_slug(base) meta = { "name": payload.get("name", slug), "description": payload.get("description", ""), @@ -96,7 +96,7 @@ class SkillExportable: store._save_index(index) -def _read_supporting_files(skill_dir: str) -> dict[str, bytes]: +def p_read_supporting_files(skill_dir: str) -> dict[str, bytes]: """Every file in a skill folder except SKILL.md, as {relpath: bytes}.""" out: dict[str, bytes] = {} for root, _dirs, names in os.walk(skill_dir): @@ -113,7 +113,7 @@ def _read_supporting_files(skill_dir: str) -> dict[str, bytes]: return out -def _slug_taken(slug: str) -> bool: +def p_slug_taken(slug: str) -> bool: return ( slug in store._load_index() or os.path.isfile(os.path.join(store.SKILLS_DIR, f"{slug}.md")) @@ -121,14 +121,14 @@ def _slug_taken(slug: str) -> bool: ) -def _free_slug(base: str) -> str: +def p_free_slug(base: str) -> str: base = base or "skill" - if not _slug_taken(base): + if not p_slug_taken(base): return base cand = f"{base}-imported" - if not _slug_taken(cand): + if not p_slug_taken(cand): return cand i = 2 - while _slug_taken(f"{base}-imported-{i}"): + while p_slug_taken(f"{base}-imported-{i}"): i += 1 return f"{base}-imported-{i}" diff --git a/backend/apps/swarm/entities/workflows.py b/backend/apps/swarm/entities/workflows.py index a8b1143c..cfdc7204 100644 --- a/backend/apps/swarm/entities/workflows.py +++ b/backend/apps/swarm/entities/workflows.py @@ -13,18 +13,18 @@ from __future__ import annotations from ..exportable import DepRef, ExportContext, RemapTable from ..models import EntityType, Requirement, RequirementKind -_BUILTIN_MODES = {"agent", "ask", "plan", "view-builder", "skill-builder"} +P_BUILTIN_MODES = {"agent", "ask", "plan", "view-builder", "skill-builder"} # Run-state, machine-linkage, and identifiers that must not ride along. -_DROP_FIELDS = { +P_DROP_FIELDS = { "id", "source_session_id", "dashboard_id", "edit_agent_session_id", "last_run_at", "last_run_status", "last_run_id", "next_run_at", "created_at", "updated_at", "cost_cap_usd_monthly", } -def _sanitize_workflow(data: dict) -> dict: - out = {k: v for k, v in data.items() if k not in _DROP_FIELDS} +def sanitize_workflow(data: dict) -> dict: + out = {k: v for k, v in data.items() if k not in P_DROP_FIELDS} sched = dict(out.get("schedule") or {}) if sched: sched["enabled"] = False @@ -52,7 +52,7 @@ class WorkflowExportable: @classmethod def load(cls, local_id: str) -> "WorkflowExportable | None": - store = _store() + store = p_store() if store is None: return None wf = store.get_workflow(local_id) @@ -62,7 +62,7 @@ class WorkflowExportable: return cls(local_id, data.get("title") or "Untitled workflow", data) def serialize(self, ctx: ExportContext) -> dict: - return _sanitize_workflow(self._data) + return sanitize_workflow(self._data) def files(self) -> dict[str, bytes]: return {} @@ -78,7 +78,7 @@ class WorkflowExportable: detail="This workflow uses this action.", )) mode = self._data.get("mode") or "agent" - if mode in _BUILTIN_MODES and mode != "agent": + if mode in P_BUILTIN_MODES and mode != "agent": reqs.append(Requirement( kind=RequirementKind.builtin_mode, key=mode, label=f"{mode} mode", detail="A built-in mode this workflow runs in.", @@ -92,12 +92,12 @@ class WorkflowExportable: @classmethod def import_(cls, payload: dict, files: dict[str, bytes], remap: RemapTable) -> str: - store = _store() - model = _model() + store = p_store() + model = p_model() if store is None or model is None: from ..ziputil import BundleError raise BundleError("this build doesn't support workflows yet; please update OpenSwarm") - clean = _sanitize_workflow(payload) + clean = sanitize_workflow(payload) clean.pop("id", None) # fresh id via the model's default_factory wf = model(**clean) store.save_workflow(wf) @@ -105,7 +105,7 @@ class WorkflowExportable: @classmethod def rollback(cls, local_id: str) -> None: - store = _store() + store = p_store() if store is not None: try: store.delete_workflow(local_id) @@ -113,7 +113,7 @@ class WorkflowExportable: pass -def _store(): +def p_store(): try: from backend.apps.workflows import storage return storage @@ -121,7 +121,7 @@ def _store(): return None -def _model(): +def p_model(): try: from backend.apps.workflows.models import Workflow return Workflow diff --git a/backend/apps/swarm/redact.py b/backend/apps/swarm/redact.py index 882a128b..89909a3a 100644 --- a/backend/apps/swarm/redact.py +++ b/backend/apps/swarm/redact.py @@ -8,7 +8,7 @@ import re from typing import Any # Substrings that mark a field name as secret (matched case-insensitively). -_DENY_SUBSTRINGS = ( +P_DENY_SUBSTRINGS = ( "api_key", "apikey", "secret", "password", "passwd", "credential", "oauth", "bearer", "subscription_token", "access_token", "refresh_token", "session_token", "auth_token", "private_key", @@ -16,7 +16,7 @@ _DENY_SUBSTRINGS = ( # Exact field names that are sensitive or per-install identity (the substring # pass alone would miss these). -_DENY_EXACT = { +P_DENY_EXACT = { "token", "installation_id", "user_id", "free_trial_token", "free_trial_remaining", "free_trial_runs_limit", "openswarm_bearer_token", "openswarm_usage_cached", "connected_account_email", "oauth_tokens", @@ -35,9 +35,9 @@ from backend.common.secret_scan import ( # noqa: E402 def is_denied_key(key: str) -> bool: k = key.lower() - if k in _DENY_EXACT: + if k in P_DENY_EXACT: return True - return any(sub in k for sub in _DENY_SUBSTRINGS) + return any(sub in k for sub in P_DENY_SUBSTRINGS) def scrub_payload(value: Any) -> Any: diff --git a/backend/apps/swarm/swarm.py b/backend/apps/swarm/swarm.py index 734677fc..6e021ee2 100644 --- a/backend/apps/swarm/swarm.py +++ b/backend/apps/swarm/swarm.py @@ -25,31 +25,31 @@ from .ziputil import MAX_TOTAL_BYTES, BundleError logger = logging.getLogger(__name__) -_STAGING: dict[str, dict] = {} -_STAGING_TTL = 30 * 60 # 30 minutes +P_STAGING: dict[str, dict] = {} +P_STAGING_TTL = 30 * 60 # 30 minutes -def _gc_staging() -> None: +def p_gc_staging() -> None: now = time.time() - for token in list(_STAGING): - if now - _STAGING[token]["created_at"] > _STAGING_TTL: - _discard(token) + for token in list(P_STAGING): + if now - P_STAGING[token]["created_at"] > P_STAGING_TTL: + p_discard(token) -def _discard(token: str) -> None: - entry = _STAGING.pop(token, None) +def p_discard(token: str) -> None: + entry = P_STAGING.pop(token, None) if entry: shutil.rmtree(entry["sandbox"], ignore_errors=True) @asynccontextmanager async def swarm_lifespan(): - _gc_staging() + p_gc_staging() try: yield finally: - for token in list(_STAGING): - _discard(token) + for token in list(P_STAGING): + p_discard(token) swarm = SubApp("swarm", swarm_lifespan) @@ -93,9 +93,9 @@ async def import_preflight(file: UploadFile = File(...)) -> ImportPreflightRespo review = closure.review_bundle(sandbox, manifest) except BundleError as e: raise HTTPException(status_code=400, detail=str(e)) - _gc_staging() + p_gc_staging() token = uuid.uuid4().hex - _STAGING[token] = {"sandbox": sandbox, "manifest": manifest, "created_at": time.time()} + P_STAGING[token] = {"sandbox": sandbox, "manifest": manifest, "created_at": time.time()} return ImportPreflightResponse( summary=closure.summarize(manifest), staging_token=token, @@ -107,7 +107,7 @@ async def import_preflight(file: UploadFile = File(...)) -> ImportPreflightRespo @swarm.router.post("/import/commit") async def import_commit(body: ImportCommitRequest) -> ImportCommitResponse: - entry = _STAGING.get(body.staging_token) + entry = P_STAGING.get(body.staging_token) if not entry: raise HTTPException(status_code=404, detail="import session expired; please re-open the file") try: @@ -117,7 +117,7 @@ async def import_commit(body: ImportCommitRequest) -> ImportCommitResponse: except BundleError as e: raise HTTPException(status_code=400, detail=str(e)) finally: - _discard(body.staging_token) + p_discard(body.staging_token) if root_id is None: raise HTTPException(status_code=400, detail="bundle has no root entity") return ImportCommitResponse( diff --git a/backend/apps/swarm/ziputil.py b/backend/apps/swarm/ziputil.py index bdf1aa5c..0a3d2395 100644 --- a/backend/apps/swarm/ziputil.py +++ b/backend/apps/swarm/ziputil.py @@ -26,7 +26,7 @@ class BundleError(Exception): """Bundle is malformed or unsafe. Message is safe to show the user.""" -def _content_digest(entries: dict[str, bytes]) -> str: +def p_content_digest(entries: dict[str, bytes]) -> str: """Order-independent sha256 over every non-manifest entry (path + bytes).""" h = hashlib.sha256() for path in sorted(entries): @@ -57,7 +57,7 @@ def pack(manifest: dict, payloads: dict[str, dict], files: dict[str, bytes]) -> entries[f"entities/{bid}/payload.json"] = json.dumps(payload, indent=2).encode("utf-8") for path, data in files.items(): entries[path] = data - manifest = {**manifest, "checksum": _content_digest(entries)} + manifest = {**manifest, "checksum": p_content_digest(entries)} buf = io.BytesIO() with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf: zf.writestr(MANIFEST_NAME, json.dumps(manifest, indent=2)) @@ -66,7 +66,7 @@ def pack(manifest: dict, payloads: dict[str, dict], files: dict[str, bytes]) -> return buf.getvalue() -def _sandbox_entries(sandbox: str) -> dict[str, bytes]: +def p_sandbox_entries(sandbox: str) -> dict[str, bytes]: """Every file under the sandbox except the manifest, keyed by forward-slash relpath so it matches the keys pack() hashed (cross-platform).""" out: dict[str, bytes] = {} @@ -88,11 +88,11 @@ def verify_checksum(sandbox: str, manifest: dict) -> None: expected = manifest.get("checksum") if not expected: return - if _content_digest(_sandbox_entries(sandbox)) != expected: + if p_content_digest(p_sandbox_entries(sandbox)) != expected: raise BundleError("this .swarm looks corrupted or was modified") -def _safe_member_path(name: str, sandbox: str) -> str: +def p_safe_member_path(name: str, sandbox: str) -> str: if name.startswith(("/", "\\")) or (len(name) > 1 and name[1] == ":"): raise BundleError("bundle contains an absolute path") dest = os.path.realpath(os.path.join(sandbox, name)) @@ -141,7 +141,7 @@ def unpack(raw: bytes) -> str: for zi in infos: if zi.is_dir(): continue - dest = _safe_member_path(zi.filename, sandbox) + dest = p_safe_member_path(zi.filename, sandbox) os.makedirs(os.path.dirname(dest), exist_ok=True) with zf.open(zi) as src, open(dest, "wb") as out: while True: diff --git a/backend/tests/test_skills_folders.py b/backend/tests/test_skills_folders.py index f7e6ec1f..f6de636e 100644 --- a/backend/tests/test_skills_folders.py +++ b/backend/tests/test_skills_folders.py @@ -209,12 +209,12 @@ async def test_create_writes_folder_and_supersedes_legacy_flat(skills_dir): def test_stage_zip_carries_supporting_files_into_sandbox(): import io as _io, zipfile, os as _os, shutil - from backend.apps.swarm.closure import _stage_skill_from_zip + from backend.apps.swarm.closure import stage_skill_from_zip buf = _io.BytesIO() with zipfile.ZipFile(buf, "w") as zf: zf.writestr("my-skill/SKILL.md", "do it") zf.writestr("my-skill/scripts/run.sh", "echo hi") - sandbox, manifest, warnings = _stage_skill_from_zip(buf.getvalue(), "my-skill.zip", []) + sandbox, manifest, warnings = stage_skill_from_zip(buf.getvalue(), "my-skill.zip", []) try: bid = manifest.entities[0].bundle_id files_dir = _os.path.join(sandbox, "entities", bid, "files") diff --git a/backend/tests/test_swarm_bundle.py b/backend/tests/test_swarm_bundle.py index 55b37a36..4c061556 100644 --- a/backend/tests/test_swarm_bundle.py +++ b/backend/tests/test_swarm_bundle.py @@ -134,7 +134,7 @@ def test_app_export_drops_machine_env(tmp_path, monkeypatch): def test_workflow_sanitize_disables_schedule_and_strips_pii(): - from backend.apps.swarm.entities.workflows import _sanitize_workflow + from backend.apps.swarm.entities.workflows import sanitize_workflow raw = { "id": "wf123", "title": "Daily digest", @@ -147,7 +147,7 @@ def test_workflow_sanitize_disables_schedule_and_strips_pii(): "mode": "agent", "provider": "anthropic", } - out = _sanitize_workflow(raw) + out = sanitize_workflow(raw) # An imported workflow must not auto-run or carry the sharer's identity. assert out["schedule"]["enabled"] is False assert out["schedule"]["runs_count"] == 0 @@ -384,8 +384,8 @@ def test_dashboard_import_remaps_to_fresh_local_ids(monkeypatch): from backend.apps.swarm.exportable import RemapTable written: dict = {} - monkeypatch.setattr(dmod, "_write", lambda did, doc: written.update({did: doc})) - monkeypatch.setattr(dmod, "_retag_sessions", lambda ids, did: None) + monkeypatch.setattr(dmod, "p_write", lambda did, doc: written.update({did: doc})) + monkeypatch.setattr(dmod, "p_retag_sessions", lambda ids, did: None) remap = RemapTable() remap.assign("SBID", "newsess") remap.assign("ABID", "newapp") @@ -422,8 +422,8 @@ def test_dashboard_remap_invariant_generative(monkeypatch): from backend.apps.swarm.models import EntityType written: dict = {} - monkeypatch.setattr(dmod, "_write", lambda did, doc: written.update({did: doc})) - monkeypatch.setattr(dmod, "_retag_sessions", lambda ids, did: None) + monkeypatch.setattr(dmod, "p_write", lambda did, doc: written.update({did: doc})) + monkeypatch.setattr(dmod, "p_retag_sessions", lambda ids, did: None) rng = random.Random(1234) for _ in range(60):