mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-10-14 05:20:46 +02:00
Pin Web oracle proxy sessions per batch
This commit is contained in:
7 files changed
+97
-18
No files matched your search
@@ -47,6 +47,10 @@ real browser. Proxy credentials are read only through a slot's named environment
|
||||
and never enter a manifest or log. Dotenv interpolation may derive routed proxy URLs from
|
||||
one secret key without mutating the process environment. A selected proxy context may
|
||||
accept that proxy's TLS interception certificate; direct contexts keep normal TLS checks.
|
||||
ThorData rotating endpoints are pinned to one manifest-derived sticky session for the
|
||||
life of a Web batch so one browser context does not change exit identity between page
|
||||
requests. This is route continuity, not account or IP failover; a refusal still ends on
|
||||
the selected slot and never selects another route automatically.
|
||||
Upload readiness is surface-specific: OpenAI reaches it at page load but keeps background
|
||||
requests alive, while Microsoft and Meta require network idle. Preserve exact provider
|
||||
errors as indeterminate evidence rather than retrying or interpreting them as clean.
|
||||
|
||||
@@ -88,6 +88,14 @@ OpenAI, Microsoft, and Meta Web slots may each name one of them through
|
||||
verify egress before relying on it, and never automatically change route after
|
||||
a refusal. Bound a batch before starting.
|
||||
|
||||
ThorData user-and-password endpoints without `sessid` rotate the exit on each
|
||||
request, which breaks a browser page whose resources must share one network
|
||||
identity. The runner therefore derives one sticky `sessid` from the immutable
|
||||
manifest hash and keeps it for that batch. A new prepared batch gets a new
|
||||
session; rerunning or resuming the same batch keeps its session while the proxy
|
||||
retains it. An explicit `sessid` in the configured route is preserved. This is
|
||||
session continuity, not permission to retry a refused batch on another route.
|
||||
|
||||
### OpenAI API
|
||||
|
||||
Use `check-openai` with one explicit upload acknowledgement. Keep named API keys
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
|
||||
# ThorData residential proxies (optional; only for explicitly selected Web oracle routes)
|
||||
# Generate these URLs in the ThorData endpoint generator; do not commit credentials.
|
||||
# The Web runner pins a rotating user-and-password route to one sticky session per manifest.
|
||||
# THORDATA_ROUTE_US=http://td-customer-USERNAME-country-US:PASSWORD@HOST.pr.thordata.net:9999
|
||||
# THORDATA_ROUTE_DE=http://td-customer-USERNAME-country-DE:PASSWORD@HOST.pr.thordata.net:9999
|
||||
# THORDATA_ROUTE_GB=http://td-customer-USERNAME-country-GB:PASSWORD@HOST.pr.thordata.net:9999
|
||||
|
||||
@@ -386,6 +386,16 @@ navigation before each upload, does not retry, and stores the settled page text.
|
||||
Each completed verdict is persisted immediately with its own observation
|
||||
timestamp. A later navigation or upload failure preserves those completed
|
||||
rows and leaves unobserved rows unresolved.
|
||||
ThorData [user-and-password endpoints without a `sessid` rotate the exit on each
|
||||
request](https://doc.thordata.com/doc/proxies/residential-proxies/session-control).
|
||||
That is unsuitable inside one browser context: page resources can
|
||||
arrive through different exits and trigger the provider's bot checks. For a
|
||||
ThorData hostname, the runner derives a non-secret sticky session id from the
|
||||
immutable manifest hash and appends a 90-minute session window when the
|
||||
configured username has no explicit session. The same batch therefore keeps
|
||||
one route while the provider retains it, a new prepared batch gets a distinct
|
||||
route, and an operator-supplied `sessid` remains unchanged. This pins a selected
|
||||
route; it does not retry or fail over after a refusal.
|
||||
For a selected proxy route it enables certificate handling in that isolated
|
||||
Chromium launch and context; direct launches keep Playwright's normal TLS
|
||||
verification. A 2026-09-07 preflight sent one read-only request through each
|
||||
@@ -423,6 +433,17 @@ for the same processed source hash. Keep each route in its own immutable batch;
|
||||
do not splice an isolated-runner failure and a first-party browser or API verdict into
|
||||
one response.
|
||||
|
||||
On 2026-09-16, a rotating ThorData browser batch demonstrated the failure mode:
|
||||
one upload reached OpenAI's generic error, four encountered Cloudflare, and ten
|
||||
became unreachable after the browser context closed. Five fresh connections to
|
||||
the same rotating endpoint had five distinct exits, while five connections with
|
||||
one sticky session had one exit. After session pinning, one direct route and
|
||||
five independently prepared country routes all reached the upload handler with
|
||||
the same prepared test image, but every route returned OpenAI's exact generic
|
||||
error. Record those responses as `indeterminate`. The result distinguishes the
|
||||
fixed session-continuity defect from the remaining anonymous-Web availability
|
||||
problem; further route changes are not a substitute for an API verdict.
|
||||
|
||||
## OpenAI API
|
||||
|
||||
The one automated path calls the hardened metadata-independent adapter already
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, datetime
|
||||
from time import monotonic
|
||||
@@ -125,8 +126,10 @@ def parse_provider_result(surface: str, page_text: str) -> WebVerdict:
|
||||
def proxy_settings(
|
||||
slot: oracles.ExecutionSlot,
|
||||
environ: Mapping[str, str] = os.environ,
|
||||
*,
|
||||
sticky_session_id: str | None = None,
|
||||
) -> ProxySettings | None:
|
||||
"""Resolve a secret proxy URL without putting it in a manifest or log."""
|
||||
"""Resolve a secret proxy URL and pin rotating ThorData routes per batch."""
|
||||
variable = slot.get("proxy_url_env")
|
||||
if variable is None:
|
||||
return None
|
||||
@@ -142,7 +145,13 @@ def proxy_settings(
|
||||
server = f"{server}:{parsed.port}"
|
||||
settings: ProxySettings = {"server": server}
|
||||
if parsed.username is not None:
|
||||
settings["username"] = unquote(parsed.username)
|
||||
username = unquote(parsed.username)
|
||||
is_thordata = parsed.hostname.endswith((".thordata.net", ".thordata.online"))
|
||||
if is_thordata and sticky_session_id is not None and "-sessid-" not in username.casefold():
|
||||
if re.fullmatch(r"[A-Za-z0-9]{1,32}", sticky_session_id) is None:
|
||||
raise ValueError("sticky_session_id must be 1-32 ASCII letters or digits")
|
||||
username = f"{username}-sessid-{sticky_session_id}-sesstime-90"
|
||||
settings["username"] = username
|
||||
if parsed.password is not None:
|
||||
settings["password"] = unquote(parsed.password)
|
||||
return settings
|
||||
@@ -287,7 +296,11 @@ def run_web_batch(
|
||||
if slot is None:
|
||||
raise ValueError("Playwright batches require an explicit execution slot")
|
||||
secret_values = dict(environ) if env_file is None else oracles.secret_environment(env_file, environ=environ)
|
||||
proxy = proxy_settings(slot, secret_values)
|
||||
proxy = proxy_settings(
|
||||
slot,
|
||||
secret_values,
|
||||
sticky_session_id=f"oracle{results['manifest_sha256'][:16]}",
|
||||
)
|
||||
pending = [
|
||||
(manifest_row, result_row)
|
||||
for manifest_row, result_row in zip(manifest["rows"], results["rows"], strict=True)
|
||||
|
||||
@@ -80,11 +80,14 @@ def test_web_results_survive_later_browser_failure(monkeypatch, tmp_path):
|
||||
"slot": {},
|
||||
"rows": [{"artifact_id": "first", "upload_path": "one"}, {"artifact_id": "second", "upload_path": "two"}],
|
||||
}
|
||||
results = {"rows": [{"watermark_result": None}, {"watermark_result": None}]}
|
||||
results = {
|
||||
"manifest_sha256": "0" * 64,
|
||||
"rows": [{"watermark_result": None}, {"watermark_result": None}],
|
||||
}
|
||||
saved = []
|
||||
monkeypatch.setattr(web.oracles, "verify_batch", lambda *a, **k: {})
|
||||
monkeypatch.setattr(web.oracles, "load_batch", lambda *a: (manifest, results))
|
||||
monkeypatch.setattr(web, "proxy_settings", lambda *a: None)
|
||||
monkeypatch.setattr(web, "proxy_settings", lambda *a, **k: None)
|
||||
monkeypatch.setattr(web.oracles, "record_result", lambda *a, **k: saved.append(k))
|
||||
|
||||
def submit(*args, **kwargs):
|
||||
|
||||
@@ -13,6 +13,18 @@ if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def _thordata_slot() -> oracles.ExecutionSlot:
|
||||
return {
|
||||
"name": "openai-thordata-us",
|
||||
"surface": "openai-web",
|
||||
"account_label": None,
|
||||
"browser_profile": None,
|
||||
"google_account_index": None,
|
||||
"network_label": "thordata-us-residential",
|
||||
"proxy_url_env": "THORDATA_ROUTE_US",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("surface", "page_text", "watermark", "provenance"),
|
||||
[
|
||||
@@ -171,6 +183,31 @@ def test_proxy_settings_are_loaded_from_an_environment_reference() -> None:
|
||||
}
|
||||
|
||||
|
||||
def test_thordata_proxy_settings_pin_one_exit_for_the_browser_session() -> None:
|
||||
proxy = web.proxy_settings(
|
||||
_thordata_slot(),
|
||||
{"THORDATA_ROUTE_US": "http://route:secret@gateway.pr.thordata.net:9999"},
|
||||
sticky_session_id="oracle0123456789abcdef",
|
||||
)
|
||||
|
||||
assert proxy == {
|
||||
"server": "http://gateway.pr.thordata.net:9999",
|
||||
"username": "route-sessid-oracle0123456789abcdef-sesstime-90",
|
||||
"password": "secret",
|
||||
}
|
||||
|
||||
|
||||
def test_thordata_proxy_settings_preserve_an_explicit_session() -> None:
|
||||
proxy = web.proxy_settings(
|
||||
_thordata_slot(),
|
||||
{"THORDATA_ROUTE_US": ("http://route-sessid-manual-sesstime-15:secret@gateway.pr.thordata.net:9999")},
|
||||
sticky_session_id="oracle0123456789abcdef",
|
||||
)
|
||||
|
||||
assert proxy is not None
|
||||
assert proxy["username"] == "route-sessid-manual-sesstime-15"
|
||||
|
||||
|
||||
def test_proxy_settings_do_not_treat_a_missing_secret_as_direct_access() -> None:
|
||||
slot: oracles.ExecutionSlot = {
|
||||
"name": "openai-west",
|
||||
@@ -290,27 +327,18 @@ def test_run_web_batch_loads_the_selected_proxy_from_dotenv(
|
||||
tmp_path: Path,
|
||||
tmp_clean_png: Path,
|
||||
) -> None:
|
||||
slot: oracles.ExecutionSlot = {
|
||||
"name": "openai-thordata-us",
|
||||
"surface": "openai-web",
|
||||
"account_label": None,
|
||||
"browser_profile": None,
|
||||
"google_account_index": None,
|
||||
"network_label": "thordata-us-residential",
|
||||
"proxy_url_env": "THORDATA_ROUTE_US",
|
||||
}
|
||||
manifest_path = oracles.prepare_batch(
|
||||
"openai-web",
|
||||
[tmp_clean_png],
|
||||
output_dir=tmp_path / "oracle-batch",
|
||||
repository_root=oracles.REPOSITORY_ROOT,
|
||||
slot=slot,
|
||||
slot=_thordata_slot(),
|
||||
)
|
||||
env_path = tmp_path / ".env"
|
||||
env_path.write_text(
|
||||
"THORDATA_PASSWORD=test-thordata-password\n"
|
||||
"THORDATA_ROUTE_US=http://td-customer-user-country-US:${THORDATA_PASSWORD}"
|
||||
"@proxy.thordata.test:9999\n",
|
||||
"@proxy.pr.thordata.net:9999\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
proxies: list[dict[str, str] | None] = []
|
||||
@@ -339,10 +367,11 @@ def test_run_web_batch_loads_the_selected_proxy_from_dotenv(
|
||||
)
|
||||
|
||||
assert report["watermark_results"] == {"not_detected": 1}
|
||||
manifest_sha256 = json.loads((manifest_path.parent / "results.json").read_text(encoding="utf-8"))["manifest_sha256"]
|
||||
assert proxies == [
|
||||
{
|
||||
"server": "http://proxy.thordata.test:9999",
|
||||
"username": "td-customer-user-country-US",
|
||||
"server": "http://proxy.pr.thordata.net:9999",
|
||||
"username": (f"td-customer-user-country-US-sessid-oracle{manifest_sha256[:16]}-sesstime-90"),
|
||||
"password": "test-thordata-password",
|
||||
}
|
||||
]
|
||||
|
||||
Reference in new issue
Block a user