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):