From 5bef023c402035959f8d90058b3ee13a7e75d1ab Mon Sep 17 00:00:00 2001 From: eddiebergman Date: Thu, 1 Oct 2026 14:10:05 +0200 Subject: [PATCH 1/6] docs(auth): say no data is shared when accepting the license (ENG-1020) Co-Authored-By: Claude Opus 5.5 --- README.md | 2 ++ src/tabpfn/browser_auth.py | 12 +++++++++++- tests/test_browser_auth.py | 11 +++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e7122ea85..181bc1d3d 100644 --- a/README.md +++ b/README.md @@ -250,6 +250,8 @@ TabPFN requires **Python 3.10+** due to newer language features. Compatible vers On first use, TabPFN will automatically open a browser window where you can log in via [PriorLabs](https://ux.priorlabs.ai) and accept the license terms. Your authentication token is cached locally so you only need to do this once. +Accepting the license does not share your data with Prior Labs. The login only links the license to your account so the weights can be downloaded. Training and prediction run entirely on your machine, and once the weights are downloaded TabPFN works fully offline (see the offline usage question below). + **For headless / CI environments** where a browser is not available, visit [https://ux.priorlabs.ai](https://ux.priorlabs.ai), go to the **License** tab to accept the license, and then set the `TABPFN_TOKEN` environment variable with a token obtained from your account. If access via the browser-based flow is not an option for you, please contact us at [`sales@priorlabs.ai`](mailto:sales@priorlabs.ai). diff --git a/src/tabpfn/browser_auth.py b/src/tabpfn/browser_auth.py index d2d624668..31e811327 100644 --- a/src/tabpfn/browser_auth.py +++ b/src/tabpfn/browser_auth.py @@ -101,6 +101,14 @@ def _has_display() -> bool: return bool(os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY")) +# Users hesitate to accept when they think it shares their datasets with us. Only +# the API key and license version are sent; data never leaves the machine. +_DATA_STAYS_LOCAL_NOTE = ( + "Your datasets are never sent to Prior Labs. Once the weights are\n" + "downloaded, TabPFN runs entirely on this machine and works offline." +) + + # --------------------------------------------------------------------------- # Token cache helpers # --------------------------------------------------------------------------- @@ -405,6 +413,7 @@ def _headless_interactive_login(gui_url: str, *, hf_repo_id: str) -> str | None: print( # noqa: T201 "\nTabPFN requires a one-time license acceptance to download" " model weights for local inference.\n" + f"{_DATA_STAYS_LOCAL_NOTE}\n" "\nNo display detected. Open this URL in a browser on another device:\n" f"\n {login_url}\n" f"\nAfter logging in, accept the license on the Licenses tab,\n" @@ -598,7 +607,8 @@ def try_browser_login( assert_never(step) print( # noqa: T201 "\nTabPFN requires a one-time license acceptance to download" - " model weights for local inference." + " model weights for local inference.\n" + f"{_DATA_STAYS_LOCAL_NOTE}\n" f"{instructions}" ) diff --git a/tests/test_browser_auth.py b/tests/test_browser_auth.py index 08de93313..313435ea8 100644 --- a/tests/test_browser_auth.py +++ b/tests/test_browser_auth.py @@ -701,6 +701,17 @@ def test_login_url_includes_hf_repo_id(self, capsys: pytest.CaptureFixture[str]) captured = capsys.readouterr() assert "hf_repo_id=tabpfn_2_6" in captured.out + def test_prompt_says_data_stays_local(self, capsys: pytest.CaptureFixture[str]): + from tabpfn.browser_auth import _DATA_STAYS_LOCAL_NOTE # noqa: PLC0415 + + headless_login = self._import_headless() + with ( + patch("tabpfn.browser_auth._headless_cbreak_loop", return_value=None), + patch("tabpfn.browser_auth._headless_readline_loop", return_value=None), + ): + headless_login("https://ux.priorlabs.ai") + assert _DATA_STAYS_LOCAL_NOTE in capsys.readouterr().out + # --------------------------------------------------------------------------- # try_browser_login routing From a7f96dfbb369e64f949ca232ddcd433ddf0ebd36 Mon Sep 17 00:00:00 2001 From: eddiebergman Date: Thu, 1 Oct 2026 14:10:45 +0200 Subject: [PATCH 2/6] docs(changelog): add entry for license data note Co-Authored-By: Claude Opus 5.5 --- changelog/1339.changed.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/1339.changed.md diff --git a/changelog/1339.changed.md b/changelog/1339.changed.md new file mode 100644 index 000000000..875d527c2 --- /dev/null +++ b/changelog/1339.changed.md @@ -0,0 +1 @@ +The license acceptance prompt and README now state that accepting the license does not send your data to Prior Labs. From 4fa70fcdcf125e880ce9db9567a09861a235de6c Mon Sep 17 00:00:00 2001 From: eddiebergman Date: Thu, 1 Oct 2026 14:22:54 +0200 Subject: [PATCH 3/6] refactor(auth): share one license header across prompts and errors Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- changelog/1339.changed.md | 2 +- src/tabpfn/browser_auth.py | 27 +++++++-------------------- src/tabpfn/errors.py | 14 +++++++++++--- tests/test_browser_auth.py | 8 +++++--- 5 files changed, 25 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 181bc1d3d..b2c7cb863 100644 --- a/README.md +++ b/README.md @@ -250,7 +250,7 @@ TabPFN requires **Python 3.10+** due to newer language features. Compatible vers On first use, TabPFN will automatically open a browser window where you can log in via [PriorLabs](https://ux.priorlabs.ai) and accept the license terms. Your authentication token is cached locally so you only need to do this once. -Accepting the license does not share your data with Prior Labs. The login only links the license to your account so the weights can be downloaded. Training and prediction run entirely on your machine, and once the weights are downloaded TabPFN works fully offline (see the offline usage question below). +Accepting the license does not share your data with Prior Labs. The local `tabpfn` package runs entirely on your machine and works offline once the weights are downloaded (see the offline usage question below). **For headless / CI environments** where a browser is not available, visit [https://ux.priorlabs.ai](https://ux.priorlabs.ai), go to the **License** tab to accept the license, and then set the `TABPFN_TOKEN` environment variable with a token obtained from your account. diff --git a/changelog/1339.changed.md b/changelog/1339.changed.md index 875d527c2..dd0d6b59b 100644 --- a/changelog/1339.changed.md +++ b/changelog/1339.changed.md @@ -1 +1 @@ -The license acceptance prompt and README now state that accepting the license does not send your data to Prior Labs. +The license acceptance prompts, license errors and README now state that accepting the license does not share your data with Prior Labs. diff --git a/src/tabpfn/browser_auth.py b/src/tabpfn/browser_auth.py index 31e811327..cfecfad65 100644 --- a/src/tabpfn/browser_auth.py +++ b/src/tabpfn/browser_auth.py @@ -28,6 +28,7 @@ from typing_extensions import assert_never from tabpfn.errors import ( + LICENSE_ACCEPTANCE_HEADER, TabPFNError, TabPFNHuggingFaceGatedRepoError, TabPFNLicenseError, @@ -101,14 +102,6 @@ def _has_display() -> bool: return bool(os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY")) -# Users hesitate to accept when they think it shares their datasets with us. Only -# the API key and license version are sent; data never leaves the machine. -_DATA_STAYS_LOCAL_NOTE = ( - "Your datasets are never sent to Prior Labs. Once the weights are\n" - "downloaded, TabPFN runs entirely on this machine and works offline." -) - - # --------------------------------------------------------------------------- # Token cache helpers # --------------------------------------------------------------------------- @@ -411,9 +404,7 @@ def _headless_interactive_login(gui_url: str, *, hf_repo_id: str) -> str | None: login_url = _browser_url(gui_url, _AuthStep.LOGIN, hf_repo_id=hf_repo_id) print( # noqa: T201 - "\nTabPFN requires a one-time license acceptance to download" - " model weights for local inference.\n" - f"{_DATA_STAYS_LOCAL_NOTE}\n" + f"\n{LICENSE_ACCEPTANCE_HEADER}" "\nNo display detected. Open this URL in a browser on another device:\n" f"\n {login_url}\n" f"\nAfter logging in, accept the license on the Licenses tab,\n" @@ -606,9 +597,7 @@ def try_browser_login( case _: assert_never(step) print( # noqa: T201 - "\nTabPFN requires a one-time license acceptance to download" - " model weights for local inference.\n" - f"{_DATA_STAYS_LOCAL_NOTE}\n" + f"\n{LICENSE_ACCEPTANCE_HEADER}" f"{instructions}" ) @@ -698,17 +687,15 @@ def ensure_license_accepted(hf_repo_id: str) -> Literal[True]: # noqa: C901, PL raise _license_not_accepted_error(gui_url, hf_repo_id) case _AuthStep.LOGIN, _NoBrowserToken.DISABLED: raise TabPFNLicenseError( - "TabPFN requires a one-time license acceptance to download\n" - "model weights for local inference, but browser login is\n" - "disabled (TABPFN_NO_BROWSER is set).\n\n" + f"{LICENSE_ACCEPTANCE_HEADER}\n" + "Browser login is disabled (TABPFN_NO_BROWSER is set).\n\n" "Set the TABPFN_TOKEN environment variable with a valid API key\n" "obtained from https://ux.priorlabs.ai" ) case _AuthStep.LOGIN, _NoBrowserToken.UNAVAILABLE: raise TabPFNLicenseError( - "TabPFN requires a one-time license acceptance to download\n" - "model weights for local inference, but no interactive terminal\n" - "is available.\n\n" + f"{LICENSE_ACCEPTANCE_HEADER}\n" + "There is no interactive terminal available.\n\n" "To authenticate in a non-interactive environment:\n" f" 1. Open {gui_url} in a browser and log in (or register)\n" f" 2. Accept the license on the Licenses tab\n" diff --git a/src/tabpfn/errors.py b/src/tabpfn/errors.py index 3e72cb605..3d4cf0f85 100644 --- a/src/tabpfn/errors.py +++ b/src/tabpfn/errors.py @@ -28,6 +28,16 @@ class TabPFNValidationError(ValueError, TabPFNUserError): """User provided invalid data (shape, NaNs, categories, etc.).""" +# Users hesitate to accept when they think it shares their datasets with us. Only +# the API key and license version are sent; data never leaves the machine. +LICENSE_ACCEPTANCE_HEADER = ( + "TabPFN requires a one-time license acceptance to download model weights\n" + "for local inference. Accepting the license does not share your data with\n" + "Prior Labs. The local tabpfn package runs entirely on your machine and\n" + "works offline once the weights are downloaded.\n" +) + + class TabPFNLicenseError(TabPFNError): """Error raised when the user has not accepted the TabPFN license.""" @@ -35,9 +45,7 @@ def __init__(self, message: str | None = None): if message is None: gui_url = settings.tabpfn.auth_gui_url message = ( - "TabPFN requires a one-time license acceptance" - " to download model weights for local" - " inference.\n\n" + f"{LICENSE_ACCEPTANCE_HEADER}\n" "To authenticate in a non-interactive" " environment:\n" f" 1. Open {gui_url} in a browser" diff --git a/tests/test_browser_auth.py b/tests/test_browser_auth.py index 313435ea8..52ee34f55 100644 --- a/tests/test_browser_auth.py +++ b/tests/test_browser_auth.py @@ -702,7 +702,7 @@ def test_login_url_includes_hf_repo_id(self, capsys: pytest.CaptureFixture[str]) assert "hf_repo_id=tabpfn_2_6" in captured.out def test_prompt_says_data_stays_local(self, capsys: pytest.CaptureFixture[str]): - from tabpfn.browser_auth import _DATA_STAYS_LOCAL_NOTE # noqa: PLC0415 + from tabpfn.errors import LICENSE_ACCEPTANCE_HEADER # noqa: PLC0415 headless_login = self._import_headless() with ( @@ -710,7 +710,7 @@ def test_prompt_says_data_stays_local(self, capsys: pytest.CaptureFixture[str]): patch("tabpfn.browser_auth._headless_readline_loop", return_value=None), ): headless_login("https://ux.priorlabs.ai") - assert _DATA_STAYS_LOCAL_NOTE in capsys.readouterr().out + assert LICENSE_ACCEPTANCE_HEADER in capsys.readouterr().out # --------------------------------------------------------------------------- @@ -797,8 +797,9 @@ def test_graphical_accept_step_opens_acceptance_page(self): "&callback=http://localhost:" ) - def test_graphical_opens_browser(self): + def test_graphical_opens_browser(self, capsys: pytest.CaptureFixture[str]): """TTY + display → opens browser (existing flow).""" + from tabpfn.errors import LICENSE_ACCEPTANCE_HEADER # noqa: PLC0415 from tabpfn.browser_auth import _AuthStep # noqa: PLC0415 try_login = self._import_try_login() @@ -815,3 +816,4 @@ def test_graphical_opens_browser(self): assert result == "browser-jwt" mock_browser.assert_called_once() + assert LICENSE_ACCEPTANCE_HEADER in capsys.readouterr().out From bf70de4d4f0c3661365281faa0276901ee1c8e21 Mon Sep 17 00:00:00 2001 From: eddiebergman Date: Thu, 1 Oct 2026 15:23:25 +0200 Subject: [PATCH 4/6] docs(auth): say datasets are never sent to Prior Labs Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- changelog/1339.changed.md | 2 +- src/tabpfn/errors.py | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index b2c7cb863..7948df05a 100644 --- a/README.md +++ b/README.md @@ -250,7 +250,7 @@ TabPFN requires **Python 3.10+** due to newer language features. Compatible vers On first use, TabPFN will automatically open a browser window where you can log in via [PriorLabs](https://ux.priorlabs.ai) and accept the license terms. Your authentication token is cached locally so you only need to do this once. -Accepting the license does not share your data with Prior Labs. The local `tabpfn` package runs entirely on your machine and works offline once the weights are downloaded (see the offline usage question below). +Accepting the license unlocks the model weights download. Neither your datasets nor anything about them is ever sent to Prior Labs: the local `tabpfn` package runs entirely on your machine and works offline once the weights are downloaded (see the offline usage question below). **For headless / CI environments** where a browser is not available, visit [https://ux.priorlabs.ai](https://ux.priorlabs.ai), go to the **License** tab to accept the license, and then set the `TABPFN_TOKEN` environment variable with a token obtained from your account. diff --git a/changelog/1339.changed.md b/changelog/1339.changed.md index dd0d6b59b..7511cb80b 100644 --- a/changelog/1339.changed.md +++ b/changelog/1339.changed.md @@ -1 +1 @@ -The license acceptance prompts, license errors and README now state that accepting the license does not share your data with Prior Labs. +The license acceptance prompts, license errors and README now state that neither your datasets nor anything about them is sent to Prior Labs. diff --git a/src/tabpfn/errors.py b/src/tabpfn/errors.py index 3d4cf0f85..db3159cd9 100644 --- a/src/tabpfn/errors.py +++ b/src/tabpfn/errors.py @@ -32,9 +32,9 @@ class TabPFNValidationError(ValueError, TabPFNUserError): # the API key and license version are sent; data never leaves the machine. LICENSE_ACCEPTANCE_HEADER = ( "TabPFN requires a one-time license acceptance to download model weights\n" - "for local inference. Accepting the license does not share your data with\n" - "Prior Labs. The local tabpfn package runs entirely on your machine and\n" - "works offline once the weights are downloaded.\n" + "for local inference. Neither your datasets nor anything about them is ever\n" + "sent to Prior Labs: the local tabpfn package runs entirely on your machine\n" + "and works offline once the weights are downloaded.\n" ) From e93f13b6da6281aaded3319458577d024b0b54d3 Mon Sep 17 00:00:00 2001 From: eddiebergman Date: Thu, 1 Oct 2026 15:31:39 +0200 Subject: [PATCH 5/6] docs(auth): shorten the datasets note Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- changelog/1339.changed.md | 2 +- src/tabpfn/errors.py | 5 ++--- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 7948df05a..a8e1e5ca0 100644 --- a/README.md +++ b/README.md @@ -250,7 +250,7 @@ TabPFN requires **Python 3.10+** due to newer language features. Compatible vers On first use, TabPFN will automatically open a browser window where you can log in via [PriorLabs](https://ux.priorlabs.ai) and accept the license terms. Your authentication token is cached locally so you only need to do this once. -Accepting the license unlocks the model weights download. Neither your datasets nor anything about them is ever sent to Prior Labs: the local `tabpfn` package runs entirely on your machine and works offline once the weights are downloaded (see the offline usage question below). +Accepting the license unlocks the model weights download. Your datasets stay local and are never sent to Prior Labs, and TabPFN works offline once the weights are downloaded (see the offline usage question below). **For headless / CI environments** where a browser is not available, visit [https://ux.priorlabs.ai](https://ux.priorlabs.ai), go to the **License** tab to accept the license, and then set the `TABPFN_TOKEN` environment variable with a token obtained from your account. diff --git a/changelog/1339.changed.md b/changelog/1339.changed.md index 7511cb80b..534db213d 100644 --- a/changelog/1339.changed.md +++ b/changelog/1339.changed.md @@ -1 +1 @@ -The license acceptance prompts, license errors and README now state that neither your datasets nor anything about them is sent to Prior Labs. +The license acceptance prompts, license errors and README now state that your datasets stay local and are never sent to Prior Labs. diff --git a/src/tabpfn/errors.py b/src/tabpfn/errors.py index db3159cd9..3d14371cb 100644 --- a/src/tabpfn/errors.py +++ b/src/tabpfn/errors.py @@ -32,9 +32,8 @@ class TabPFNValidationError(ValueError, TabPFNUserError): # the API key and license version are sent; data never leaves the machine. LICENSE_ACCEPTANCE_HEADER = ( "TabPFN requires a one-time license acceptance to download model weights\n" - "for local inference. Neither your datasets nor anything about them is ever\n" - "sent to Prior Labs: the local tabpfn package runs entirely on your machine\n" - "and works offline once the weights are downloaded.\n" + "for local inference. Your datasets stay local and are never sent to Prior\n" + "Labs, and TabPFN works offline once the weights are downloaded.\n" ) From f45cea94abcf2dcadaee1a4b4ddf96993d9c35dd Mon Sep 17 00:00:00 2001 From: eddiebergman Date: Mon, 19 Oct 2026 17:12:47 +0200 Subject: [PATCH 6/6] test(auth): adapt privacy prompt test to required repository --- src/tabpfn/browser_auth.py | 3 +-- tests/test_browser_auth.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/tabpfn/browser_auth.py b/src/tabpfn/browser_auth.py index cfecfad65..c47f26e96 100644 --- a/src/tabpfn/browser_auth.py +++ b/src/tabpfn/browser_auth.py @@ -597,8 +597,7 @@ def try_browser_login( case _: assert_never(step) print( # noqa: T201 - f"\n{LICENSE_ACCEPTANCE_HEADER}" - f"{instructions}" + f"\n{LICENSE_ACCEPTANCE_HEADER}{instructions}" ) # --- main thread: poll stdin while waiting for callback --- diff --git a/tests/test_browser_auth.py b/tests/test_browser_auth.py index 52ee34f55..f3da30e32 100644 --- a/tests/test_browser_auth.py +++ b/tests/test_browser_auth.py @@ -709,7 +709,7 @@ def test_prompt_says_data_stays_local(self, capsys: pytest.CaptureFixture[str]): patch("tabpfn.browser_auth._headless_cbreak_loop", return_value=None), patch("tabpfn.browser_auth._headless_readline_loop", return_value=None), ): - headless_login("https://ux.priorlabs.ai") + headless_login("https://ux.priorlabs.ai", hf_repo_id="tabpfn_2_6") assert LICENSE_ACCEPTANCE_HEADER in capsys.readouterr().out @@ -799,8 +799,8 @@ def test_graphical_accept_step_opens_acceptance_page(self): def test_graphical_opens_browser(self, capsys: pytest.CaptureFixture[str]): """TTY + display → opens browser (existing flow).""" - from tabpfn.errors import LICENSE_ACCEPTANCE_HEADER # noqa: PLC0415 from tabpfn.browser_auth import _AuthStep # noqa: PLC0415 + from tabpfn.errors import LICENSE_ACCEPTANCE_HEADER # noqa: PLC0415 try_login = self._import_try_login() with (