{"record":{"id":"11c0393fecad6469","repo":"bytedance/deer-flow","slug":"browser-automation-is-not-enabled","errorCode":null,"errorMessage":"Browser automation is not enabled","messagePattern":"Browser automation is not enabled","errorType":"http","errorClass":"HTTPException","httpStatus":404,"severity":"error","filePath":"backend/app/gateway/routers/browser.py","lineNumber":88,"sourceCode":"    record = await thread_store.get(thread_id, user_id=user_id)\n    return record is not None and record.get(\"user_id\") == user_id\n\n\n@router.post(\n    \"/threads/{thread_id}/browser/navigate\",\n    response_model=BrowserNavigateResponse,\n    summary=\"Navigate The Live Browser Session\",\n    description=\"Steer the thread's live browser session to a URL from the UI and capture a screenshot.\",\n)\n@require_permission(\"threads\", \"write\", owner_check=True, require_existing=True)\nasync def navigate_browser(thread_id: ThreadId, body: BrowserNavigateRequest, request: Request) -> BrowserNavigateResponse:\n    user_id = str(request.state.auth.user.id)\n    thread_store = getattr(request.app.state, \"thread_store\", None)\n    if thread_store is None or not await _browser_thread_owned_by(thread_store, thread_id, user_id):\n        raise HTTPException(status_code=404, detail=f\"Thread {thread_id} not found\")\n\n    if not _browser_tools_enabled():\n        raise HTTPException(status_code=404, detail=\"Browser automation is not enabled\")\n\n    try:\n        from deerflow.community.browser_automation import navigate_and_capture, redact_browser_url\n    except ImportError as exc:  # Playwright is an optional dependency.\n        raise HTTPException(status_code=501, detail=\"Browser automation is not available\") from exc\n\n    url = body.url.strip()\n    if not url:\n        raise HTTPException(status_code=400, detail=\"URL is required\")\n\n    outputs_path = get_paths().sandbox_outputs_dir(thread_id, user_id=get_effective_user_id())\n    try:\n        result = await navigate_and_capture(thread_id=thread_id, url=url, outputs_path=outputs_path)\n    except ValueError as exc:\n        # SSRF / URL validation failure.\n        raise HTTPException(status_code=400, detail=str(exc)) from exc\n    except Exception as exc:\n        logger.error(","sourceCodeStart":70,"sourceCodeEnd":106,"githubUrl":"https://github.com/bytedance/deer-flow/blob/1dd6ba1acb03700589994b0366c5d1c7d05e2eff/backend/app/gateway/routers/browser.py#L70-L106","documentation":"404 from the browser navigate route (browser.py:88) when _browser_tools_enabled() returns false. That helper (browser.py:43) checks whether the 'browser_navigate' tool is enabled in config.yaml; the live browser HTTP/WS endpoints are an opt-in surface — having Playwright importable is explicitly NOT sufficient, otherwise server-side browser control would be exposed without operator consent.","triggerScenarios":"POST /api/threads/{id}/browser/navigate on a deployment where the browser_navigate tool is not enabled in config.yaml (default off), including fresh installs that never opted in.","commonSituations":"New install without enabling browser tooling; config.yaml regenerated from the example (which leaves the tool off) after previously enabling it; feature flagged off in production but UI from a dev build still shows browser controls.","solutions":["Enable the browser_navigate tool in config.yaml (tools section) and restart the Gateway","Verify via the feature-availability endpoint the frontend uses (GET /api/features reports browser_control gating) and gate the UI on it so the control is not shown when disabled","Do not try to bypass the check — it is an intentional operator opt-in security boundary"],"exampleFix":"# config.yaml (before) — browser tool not enabled\ntools:\n  enabled:\n    - web_search\n\n# after\ntools:\n  enabled:\n    - web_search\n    - browser_navigate","handlingStrategy":"validation","validationCode":"# Gate the UI control on the feature flag before calling\nconst features = await fetch(\"/api/features\").then(r => r.json());\nif (features.browser_control?.enabled) showBrowserControls();","typeGuard":"const browserEnabled = (f: Features): boolean => Boolean(f?.browser_control?.enabled);","tryCatchPattern":"try { await navigateBrowser(threadId, url) } catch (e) { if (e.status === 404 && e.detail === \"Browser automation is not enabled\") hideBrowserControls(); else throw e; }","preventionTips":["Consume GET /api/features for browser_control gating rather than assuming availability","Treat 'browser_navigate not in config' and 'Playwright missing' as distinct failure layers (404 vs 501) when triaging","Never enable the endpoint in response to user pressure without operator consent — it is an intentional opt-in boundary"],"tags":["browser","config","feature-flag","http-404"],"backgroundTag":null,"analyzedSha":"1dd6ba1acb03700589994b0366c5d1c7d05e2eff","analyzedAt":"2026-08-14T21:20:34.804Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}