From c86165e735fb263a4b50bce7cb059dff5905d662 Mon Sep 17 00:00:00 2001 From: Leo Borcherding Date: Fri, 26 Jun 2026 02:56:23 -0500 Subject: [PATCH] Studio Colab: opt-in shareable Cloudflare tunnel link (#6684) * Studio Colab: add opt-in shareable Cloudflare tunnel link colab.start(cloudflare=True) opts in to a free Cloudflare quick tunnel and shows a trycloudflare.com link above the proxy iframe, reachable from any device. Default OFF: bare start() keeps the in-tab Colab-proxy behavior. run_server suppresses the tunnel on Colab by design, so colab.py starts it directly via cloudflare_tunnel.start_studio_tunnel(); failures degrade to the Colab proxy only. * Studio Colab notebook: surface opt-in cloudflare=True in start cell * Studio Colab: reskin shareable Cloudflare link to match the proxy banner Retrofit _shareable_link_html to reuse the original Colab proxy banner skin from show_link (white card, black border, Unsloth gem, black Open button) instead of the plain dark box, so the shareable Cloudflare link gets the same prominent 'Ready!' treatment. * Studio Colab: address review feedback on Cloudflare tunnel - try/finally around tunnel start + embed + keepalive so a KeyboardInterrupt while the tunnel is starting or the iframe is rendering tears it down instead of orphaning the cloudflared process (Gemini review). - Publish the directly-started tunnel URL onto app.state.cloudflare_url via a new _publish_cloudflare_url helper so /api/health advertises it; otherwise the frontend's API examples fall back to the unreachable raw server_url (Codex P2). _stop_cloudflare_tunnel now also clears it so health stops showing a dead tunnel. - Notebook: make cloudflare=True a replacement for start(), not an addition, since start() blocks and the second call would never run if both are left in (Codex P2). * Studio Colab: gate Cloudflare tunnel on auth + honor opt-out in run_server - Refuse to open the Cloudflare tunnel while the admin still holds its seeded bootstrap password. While requires_password_change is true the server injects that password into same-origin index GETs, and a public tunnel request counts as same-origin, so sharing the link would leak admin access. New _bootstrap_password_pending() gate (fails safe) blocks the tunnel and tells the user to change the password first, then re-run start(cloudflare=True) (P1). - Pass cloudflare=False into run_server so the opt-out holds even when Colab detection fails; this helper is now the sole owner of the tunnel decision, preventing run_server from opening a tunnel on the 0.0.0.0 bind by default (P2). * [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci * Studio Colab: drop duplicate tunnel link log and simplify start cell guidance * Studio Colab: validate /api/health identity before reusing or tunneling a port * Studio Colab: condense verbose docstrings and comments --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Daniel Han --- studio/Unsloth_Studio_Colab.ipynb | 2 +- studio/backend/colab.py | 176 ++++++++++++++++++++++++++---- 2 files changed, 156 insertions(+), 22 deletions(-) diff --git a/studio/Unsloth_Studio_Colab.ipynb b/studio/Unsloth_Studio_Colab.ipynb index 00eecfe51..619395bd6 100644 --- a/studio/Unsloth_Studio_Colab.ipynb +++ b/studio/Unsloth_Studio_Colab.ipynb @@ -84,7 +84,7 @@ "id": "277e431e" }, "outputs": [], - "source": "import sys\nsys.path.insert(0, \"/content/unsloth/studio/backend\")\nfrom colab import start\nstart()" + "source": "import sys\nsys.path.insert(0, \"/content/unsloth/studio/backend\")\nfrom colab import start\n\n# Default: in-tab iframe only. start() blocks to keep the kernel alive.\nstart()\n\n# For a shareable Cloudflare link, replace start() above with:\n# start(cloudflare=True)" }, { "cell_type": "markdown", diff --git a/studio/backend/colab.py b/studio/backend/colab.py index ba46c52a6..dd274399b 100644 --- a/studio/backend/colab.py +++ b/studio/backend/colab.py @@ -103,24 +103,132 @@ def show_link(port: int = 8888, *, _url: "str | None" = None): display(HTML(html)) -def _is_studio_healthy(port: int, timeout: float = 2.0) -> bool: - """Return True if a Studio backend is already answering health checks on *port*.""" - import urllib.request +def _bootstrap_password_pending() -> bool: + """True while the default admin still owes a bootstrap-password change. + + While pending, main.py injects that password into same-origin GETs, and a public + tunnel GET (no Origin) reads as same-origin, so sharing the link would leak admin + access. Fails safe to pending if the state cannot be read. + """ try: - with urllib.request.urlopen(f"http://localhost:{port}/api/health", timeout = timeout): - return True + from auth.storage import requires_password_change, DEFAULT_ADMIN_USERNAME + return bool(requires_password_change(DEFAULT_ADMIN_USERNAME)) + except Exception as e: + logger.info(f"Could not check admin password state ({e}); refusing tunnel to be safe.") + return True + + +def start_cloudflare_tunnel(port: int) -> "str | None": + """Open a shareable Cloudflare quick tunnel to localhost:*port*, or None. + + run_server suppresses the tunnel on Colab by design, so we start it directly. + Refused while the bootstrap password is pending; any failure collapses to None + and the Colab proxy still works. + """ + if _bootstrap_password_pending(): + logger.warning( + "Cloudflare link not started: the admin account still has its temporary " + "bootstrap password, which is exposed to anyone who can load the page. " + "Open Studio in this tab, log in and change the admin password, then re-run " + "start(cloudflare=True) to get the shareable link." + ) + return None + try: + from cloudflare_tunnel import start_studio_tunnel + except Exception as e: + logger.info(f"Cloudflare tunnel unavailable ({e}); using Colab proxy only.") + return None + try: + url = start_studio_tunnel(port) + except Exception as e: + logger.info(f"Cloudflare tunnel failed to start ({e}); using Colab proxy only.") + return None + # Success is logged by _show_and_embed; note only misses here. + if not url: + logger.info("Cloudflare tunnel did not produce a URL; using Colab proxy only.") + return url + + +def _publish_cloudflare_url(cloudflare_url: "str | None") -> None: + """Publish a directly-started tunnel URL onto app.state so /api/health advertises it. + + run_server only sets this when it opens the tunnel itself, which it skips on Colab, + so we set it here. Otherwise the frontend's API examples fall back to an + unreachable server_url. Best-effort. + """ + if not cloudflare_url: + return + try: + from main import app as _studio_app + _studio_app.state.cloudflare_url = cloudflare_url + except Exception as e: + logger.info(f"Could not publish Cloudflare URL to /api/health ({e}).") + + +def _stop_cloudflare_tunnel() -> None: + """Best-effort teardown of the Cloudflare tunnel started by start_cloudflare_tunnel.""" + try: + from cloudflare_tunnel import stop_studio_tunnel + stop_studio_tunnel() + except Exception: + pass + # Stop /api/health advertising a dead tunnel. + try: + from main import app as _studio_app + _studio_app.state.cloudflare_url = None + except Exception: + pass + + +def _is_studio_healthy(port: int, timeout: float = 2.0) -> bool: + """True only if Unsloth Studio (not some other app) answers /api/health on *port*. + + The service-marker check stops the reuse path reusing or tunneling a foreign + process that merely serves /api/health. + """ + import json, urllib.request + try: + with urllib.request.urlopen(f"http://localhost:{port}/api/health", timeout = timeout) as r: + return json.loads(r.read()).get("service") == "Unsloth UI Backend" except Exception: return False -def _show_and_embed(port: int): - """Embed the Studio inline for *port* with a branded header bar. - - Fetches the proxy URL once (registering the port), then renders header bar + - iframe. Falls back to serve_kernel_port_as_iframe if IPython HTML is unavailable. +def _shareable_link_html(cloudflare_url: str) -> str: + """Branded card for the shareable Cloudflare link, styled like the show_link banner.""" + return f""" +
+

+ + Shareable Studio Link is Ready! +

+ + + Open Unsloth Studio + +

+ This Cloudflare HTTPS link works from any device — share it with anyone. The Colab view below only works in this tab. +

+

+ 🔗 {cloudflare_url} +

+
""" + + +def _show_and_embed(port: int, *, cloudflare_url: "str | None" = None): + """Render the Studio header + iframe for *port*, with a shareable-link card above + when *cloudflare_url* is set. Falls back to serve_kernel_port_as_iframe.""" url = get_colab_url(port) logger.info(f"🌐 Unsloth Studio URL: {url}") + if cloudflare_url: + logger.info(f"🔗 Shareable Cloudflare link: {cloudflare_url}") try: from IPython.display import HTML, display @@ -136,6 +244,9 @@ def _show_and_embed(port: int): except (ValueError, IndexError): short_url = url + if cloudflare_url: + display(HTML(_shareable_link_html(cloudflare_url))) + display( HTML(f"""