diff --git a/backend/apps/agents/agent_manager.py b/backend/apps/agents/agent_manager.py index 88b09456..c7426fe4 100644 --- a/backend/apps/agents/agent_manager.py +++ b/backend/apps/agents/agent_manager.py @@ -79,8 +79,7 @@ class AgentManager(SessionLifecycle, SessionPersistence, Messaging, SessionContr builtin_perms = load_builtin_permissions() - # Builtins default to always_allow (frictionless); path_gate still force-prompts on catastrophic patterns (rm -rf), OS-scheduling, and sensitive paths, so poisoned-email -> destructive-command is still caught. Flip Bash to "ask" in the UI for a prompt on every command. - # Bind turn + stderr first: build_agent_options can raise early (no provider) and the except hands both to handle_run_error. + # Builtins default to always_allow (frictionless); path_gate still force-prompts on catastrophic patterns (rm -rf), OS-scheduling, and sensitive paths, so poisoned-email -> destructive-command is still caught. Flip Bash to "ask" in the UI for a prompt on every command. Bind turn + stderr first: build_agent_options can raise early (no provider) and the except hands both to handle_run_error. turn = TurnState() p_stderr_buffer: List[str] = [] try: diff --git a/backend/apps/agents/manager/session/SessionPersistence.py b/backend/apps/agents/manager/session/SessionPersistence.py index 0930ad24..7d44baac 100644 --- a/backend/apps/agents/manager/session/SessionPersistence.py +++ b/backend/apps/agents/manager/session/SessionPersistence.py @@ -80,10 +80,7 @@ class SessionPersistence(AgentManagerProtocol): if session.closed_at is not None: continue if session.status in ("running", "waiting_approval"): - # The app died mid-turn. If the last message in the active branch is already an - # assistant reply, the turn finished streaming and only the status finalize was lost - # (-> completed, no spurious "Resume" button); otherwise the agent was genuinely cut - # off owing a response (-> stopped, resumable). + # The app died mid-turn. If the last message in the active branch is already an assistant reply, the turn finished streaming and only the status finalize was lost (-> completed, no spurious "Resume" button); otherwise the agent was genuinely cut off owing a response (-> stopped, resumable). branch = session.active_branch_id or "main" p_branch_msgs = [m for m in session.messages if (m.branch_id or "main") == branch] p_last = p_branch_msgs[-1] if p_branch_msgs else None diff --git a/backend/auth.py b/backend/auth.py index 95dc61bb..4269dd7e 100644 --- a/backend/auth.py +++ b/backend/auth.py @@ -179,15 +179,9 @@ P_AUTH_EXEMPT_EXACT = { "/api/subscription/activate", "/api/auth/signin-activate", "/api/version", - # Local Google OAuth token-endpoint proxy: hit by the - # google-workspace-mcp subprocess we spawn. It doesn't (and can't - # easily) carry the install bearer in google-auth's refresh post. - # Localhost binding is the gate, and the route does nothing the - # public api.openswarm.com/api/oauth/google/refresh doesn't already - # do for any internet caller, so no new attack surface. + # Local Google OAuth token-endpoint proxy: hit by the google-workspace-mcp subprocess we spawn. It doesn't (and can't easily) carry the install bearer in google-auth's refresh post. Localhost binding is the gate, and the route does nothing the public api.openswarm.com/api/oauth/google/refresh doesn't already do for any internet caller, so no new attack surface. "/api/tools/google-oauth-token", - # Dev-only token handoff for the split-port frontend (no Electron preload - # to read the token from). The route itself 404s in packaged builds. + # Dev-only token handoff for the split-port frontend (no Electron preload to read the token from). The route itself 404s in packaged builds. "/api/dev/token", } diff --git a/backend/config/Apps.py b/backend/config/Apps.py index f94c3e34..1b739864 100644 --- a/backend/config/Apps.py +++ b/backend/config/Apps.py @@ -30,11 +30,7 @@ class MainApp: @asynccontextmanager async def lifespan(app: FastAPI): async with AsyncExitStack() as stack: - # [perf] per-lifespan boot timing. debug() is a no-op in the - # packaged build, so without this the packaged backend.log has no - # per-SubApp markers and a cold-start stall can only be guessed at. - # One perf_counter + flushed print per app pins exactly which - # lifespan (or the cold first-touch I/O entering it) dominates. + # [perf] per-lifespan boot timing. debug() is a no-op in the packaged build, so without this the packaged backend.log has no per-SubApp markers and a cold-start stall can only be guessed at. One perf_counter + flushed print per app pins exactly which lifespan (or the cold first-touch I/O entering it) dominates. p_boot_t0 = time.perf_counter() for sub_app in sub_apps: debug(sub_app.name) diff --git a/backend/main.py b/backend/main.py index 130c0819..5c356b1e 100644 --- a/backend/main.py +++ b/backend/main.py @@ -4,10 +4,7 @@ import logging import os from uuid import uuid4 -# App-level INFO logs (fast-path gates, skill recording, replay decisions) were -# invisible because nothing configured the 'backend' logger; every debugging -# session re-paid that blindness. Idempotent so uvicorn reloads don't stack -# handlers; uvicorn's own access logs are untouched. +# App-level INFO logs (fast-path gates, skill recording, replay decisions) were invisible because nothing configured the 'backend' logger; every debugging session re-paid that blindness. Idempotent so uvicorn reloads don't stack handlers; uvicorn's own access logs are untouched. p_backend_logger = logging.getLogger("backend") if not p_backend_logger.handlers: p_h = logging.StreamHandler() @@ -54,8 +51,7 @@ import json main_app = MainApp([health, agents, skills, tools_lib, modes, settings, mcp_registry, skill_registry, outputs, output_versions, dashboards, swarm, service, subscription, auth, web, anthropic_proxy, workflows]) app = main_app.app -# Generate per-install auth token BEFORE we bind the HTTP port. By the -# time any request lands, the token file exists. See backend/auth.py. +# Generate per-install auth token BEFORE we bind the HTTP port. By the time any request lands, the token file exists. See backend/auth.py. from backend.auth import ( init_auth_token, install_token_scrubber, @@ -64,19 +60,10 @@ from backend.auth import ( is_origin_allowed, ) init_auth_token() -# Install the log scrubber AFTER the token exists so any log line that -# accidentally embeds it (subprocess env dumps, urllib retry traces, -# proxied-request error bodies) gets redacted before hitting handlers. +# Install the log scrubber AFTER the token exists so any log line that accidentally embeds it (subprocess env dumps, urllib retry traces, proxied-request error bodies) gets redacted before hitting handlers. install_token_scrubber() -# Generate the per-install id (installation_id) at the same pre-bind moment -# as the auth token. It is otherwise created lazily on the first analytics -# submission, so on a clean install the sign-in window can render and build -# its Google/email OAuth URL (which embeds install_id) before that -# submission fires, producing an empty install_id that the cloud rejects. -# Generating here guarantees the very first GET /api/settings already -# carries it. Platform-agnostic; wrapped so a settings hiccup never blocks -# startup, and the lazy path stays as a fallback. +# Generate the per-install id (installation_id) at the same pre-bind moment as the auth token. It is otherwise created lazily on the first analytics submission, so on a clean install the sign-in window can render and build its Google/email OAuth URL (which embeds install_id) before that submission fires, producing an empty install_id that the cloud rejects. Generating here guarantees the very first GET /api/settings already carries it. Platform-agnostic; wrapped so a settings hiccup never blocks startup, and the lazy path stays as a fallback. try: import uuid as p_uuid from backend.apps.settings.store import load_settings as p_load_boot_settings, save_settings as p_save_boot_settings @@ -88,12 +75,7 @@ except Exception: pass -# CORS: previously wide open (`allow_origins=["*"]`), which combined with -# `allow_credentials=True` was a security footgun, any external origin -# could CORS-preflight us. Now restricted to Electron renderer origins + -# localhost dev servers. The token middleware below provides the -# *primary* defense; CORS is defense-in-depth so a misconfigured page -# can't even reach us. +# CORS: previously wide open (`allow_origins=["*"]`), which combined with `allow_credentials=True` was a security footgun, any external origin could CORS-preflight us. Now restricted to Electron renderer origins + localhost dev servers. The token middleware below provides the *primary* defense; CORS is defense-in-depth so a misconfigured page can't even reach us. app.add_middleware( CORSMiddleware, allow_origins=[ @@ -106,13 +88,7 @@ app.add_middleware( allow_credentials=True, allow_methods=["*"], allow_headers=["*"], - # Every cross-origin POST from the Electron renderer (file:// → http://localhost:8324) - # carries Authorization: Bearer, which CORS classifies as non-simple and - # forces a preflight OPTIONS before EACH POST. With no max_age the browser - # re-preflights on a tight schedule (~5 s in Chromium); under heavy - # interaction we observed a 1:1 OPTIONS-to-POST ratio in the dev log, - # doubling roundtrip count for no reason. Caching the preflight result - # for 10 minutes drops that to one OPTIONS per ~600 POSTs. + # Every cross-origin POST from the Electron renderer (file:// → http://localhost:8324) carries Authorization: Bearer, which CORS classifies as non-simple and forces a preflight OPTIONS before EACH POST. With no max_age the browser re-preflights on a tight schedule (~5 s in Chromium); under heavy interaction we observed a 1:1 OPTIONS-to-POST ratio in the dev log, doubling roundtrip count for no reason. Caching the preflight result for 10 minutes drops that to one OPTIONS per ~600 POSTs. max_age=600, ) @@ -142,13 +118,10 @@ async def p_auth_middleware(request: Request, call_next): elif is_path_exempt(request.url.path): response = await call_next(request) else: - # Accept Authorization Bearer, x-openswarm-token, OR x-api-key - # (CLI path, CLI sends x-api-key with our token as value). + # Accept Authorization Bearer, x-openswarm-token, OR x-api-key (CLI path, CLI sends x-api-key with our token as value). headers = dict(request.headers) x_api_key = headers.get("x-api-key") or headers.get("X-API-Key") - # Accept `?token=` query param too. Required for browser-driven - # GETs that can't set headers, notably the App Builder iframe loading - # /api/outputs/.../serve/index.html via