Review topic: Secure-gateway URL wrapping
Why this exists
Air-gapped deployments set SECURE_GATEWAY (legacy LICENSE_SERVER) and can
reach ONLY the gateway host. Every Roboflow-bound request must be rewritten to
{gateway}/proxy?url=<quoted url> via wrap_url()
(inference/core/utils/url_utils.py) on the server side, or via
roboflow_secure_gateway_proxy_url_builder
(inference_models/inference_models/weights_providers/roboflow.py) inside
inference_models. A single unwrapped call site dead-ends behind the gateway —
usually as a hang or a retry loop, discovered only in a customer's air-gapped
environment (see #2658, which fixed three such gaps; #2263 for the offline
loading fallout).
When this applies
Load when the diff shows ANY of these content signals (paths are hints, not the
trigger):
- A new or moved outbound HTTP call:
requests.get/post/put/head/delete,
aiohttp/ClientSession, httpx, urllib.request.urlopen, websockets.
- A URL built from
API_BASE_URL, METRICS_COLLECTOR_BASE_URL,
HOSTED_DETECT_URL / HOSTED_INSTANCE_SEGMENTATION_URL /
HOSTED_CLASSIFICATION_URL / HOSTED_CORE_MODEL_URL /
HOSTED_SEMANTIC_SEGMENTATION_URL, or any literal *.roboflow.com host.
- A new env var, pydantic setting, or config field whose value is an endpoint
URL that will later be requested.
- A new
InferenceHTTPClient(api_url=...) construction in server-side code.
- Any change to
wrap_url, roboflow_secure_gateway_proxy_url_builder,
SECURE_GATEWAY / LICENSE_SERVER parsing, or /proxy?url= handling.
Review checklist
Tag each finding. Fix BLOCK before merge; raise FLAG; NIT is optional.
- BLOCK — A new request to a Roboflow-owned endpoint whose URL does not
pass through
wrap_url() (server code) or the weights-provider
proxy_url_builder (inference_models) before the socket is opened.
Trace the URL from construction to the requests/aiohttp call; "it is
wrapped somewhere upstream" must be verifiable at a specific line. (Rule 1.)
- BLOCK — An endpoint URL sourced from an env var or setting where only
the DEFAULT value is wrapped. Overrides must be wrapped at consumption time
(validator or call site), not at field-definition time — a user-supplied
TELEMETRY_*-style override otherwise bypasses the gateway. (Rule 2.)
- BLOCK — A network call that runs at module import time (version checks,
registry warm-ups) with no timeout, or without honoring
SECURE_GATEWAY / OFFLINE_MODE / its dedicated disable flag. Behind a
packet-dropping firewall an untimed import-time call stalls server startup
for the OS TCP timeout. (Rule 3.)
- BLOCK — A new consumer of hosted inference endpoints (
HOSTED_*_URL,
via InferenceHTTPClient or otherwise) with no SECURE_GATEWAY gate. The
SDK client joins request paths onto api_url, which cannot compose with
/proxy?url=<encoded> — hosted targets are unreachable behind a gateway, so
the path must fail fast or fall back (see the remote+hosted → local fallback
in inference/core/env.py). (Rule 4.)
- FLAG — Hand-rolled wrapping (f-string building
/proxy?url= directly)
instead of calling wrap_url(). The helper is idempotent and handles
scheme-qualified vs bare-host gateways; inline copies drift. (Rule 5.)
- FLAG — A wrapped URL persisted to disk/db (queues, caches) and re-wrapped
on read, or an endpoint stored pre-wrapped where
SECURE_GATEWAY could
differ between writer and reader processes. Persist raw URLs; wrap at send
time. (Rule 6.)
- FLAG — A new third-party integration (Google/Stability/Twilio/Modal/
webhook-style) reachable from a workflow or server path with no timeout or
with errors that propagate as crashes rather than typed failures — behind a
gateway these hosts are unreachable and the block must degrade cleanly.
(Rule 7.)
- NIT — A new outbound integration not mentioned in the
SECURE_GATEWAY
section of docs/quickstart/docker_configuration_options.md when its
gateway behavior is user-visible.
Not blocking
- Calls to localhost / 127.0.0.1 / Unix sockets / LAN devices (PLC, ONVIF,
local event-ingestion) — the gateway constraint is about egress.
- User-supplied media/webhook URLs fetched on behalf of the user (these are
covered by
review-topic-input-boundary-security); flag only if a NEW
Roboflow-owned endpoint hides among them.
- Test files, docstrings, and examples containing URLs.
inference_models package files downloaded from provider-returned URLs —
those arrive pre-wrapped from parse_package_artefacts when the gateway is
configured; do not demand a second wrap.
Standards (one canonical statement per rule)
- Wrap at the socket, verifiably. Every Roboflow-bound URL must flow
through
wrap_url() / proxy_url_builder on a traceable line before the
request executes. The audit that produced this skill found the entire
roboflow_api.py surface wrapped but 42 workflow run_remotely blocks and
the version check unwrapped (#2658) — coverage claims must be per-call-site,
not per-module.
- Defaults are not configuration. Wrapping applied to a default value
(field default, module constant) silently exempts every override. Wrap in
the validator or at the call site so env-provided endpoints route through
the gateway too (
TelemetrySettings fix in #2658).
- Import-time network calls need a bound and a switch. Any call that can
run during
import inference.* must carry an explicit timeout= and be
disabled by SECURE_GATEWAY / OFFLINE_MODE / its own flag. The GitHub
version check ran untimed at import and could stall startup for minutes on
air-gapped networks (#2658).
- Hosted endpoints cannot be proxied — gate them. Until
inference_sdk
understands the gateway /proxy contract, any server-side path that would
contact HOSTED_*_URL under SECURE_GATEWAY must be rerouted or refused
with a clear warning at configuration time, not left to fail per request.
- One wrapper implementation per package. Server code calls
wrap_url();
inference_models code uses the weights-provider builder. New inline
/proxy?url= string construction is drift waiting to happen.
- Wrap late, store raw.
SECURE_GATEWAY is process configuration;
persisted URLs outlive processes. Anything writing URLs to sqlite queues,
caches, or model artifacts stores the raw URL and wraps at send time —
wrap_url idempotence is a safety net, not a design.
- Unreachable third parties must degrade, not crash. Blocks contacting
non-Roboflow services are expected to fail behind a gateway — with a
timeout, a typed/structured error, and no retry storm.
1---2name: review-topic-secure-gateway-url-wrapping3description: Load when a PR adds or modifies an outbound HTTP call (requests.*, aiohttp, httpx, urllib), constructs a URL from API_BASE_URL / METRICS_COLLECTOR_BASE_URL / HOSTED_*_URL / any *.roboflow.com host, adds an env var or setting that holds an endpoint URL, builds an InferenceHTTPClient, or touches wrap_url / roboflow_secure_gateway_proxy_url_builder / SECURE_GATEWAY handling.4---56# Review topic: Secure-gateway URL wrapping78## Why this exists910Air-gapped deployments set `SECURE_GATEWAY` (legacy `LICENSE_SERVER`) and can11reach ONLY the gateway host. Every Roboflow-bound request must be rewritten to12`{gateway}/proxy?url=<quoted url>` via `wrap_url()`13(`inference/core/utils/url_utils.py`) on the server side, or via14`roboflow_secure_gateway_proxy_url_builder`15(`inference_models/inference_models/weights_providers/roboflow.py`) inside16`inference_models`. A single unwrapped call site dead-ends behind the gateway —17usually as a hang or a retry loop, discovered only in a customer's air-gapped18environment (see #2658, which fixed three such gaps; #2263 for the offline19loading fallout).2021## When this applies2223Load when the diff shows ANY of these content signals (paths are hints, not the24trigger):2526- A new or moved outbound HTTP call: `requests.get/post/put/head/delete`,27 `aiohttp`/`ClientSession`, `httpx`, `urllib.request.urlopen`, websockets.28- A URL built from `API_BASE_URL`, `METRICS_COLLECTOR_BASE_URL`,29 `HOSTED_DETECT_URL` / `HOSTED_INSTANCE_SEGMENTATION_URL` /30 `HOSTED_CLASSIFICATION_URL` / `HOSTED_CORE_MODEL_URL` /31 `HOSTED_SEMANTIC_SEGMENTATION_URL`, or any literal `*.roboflow.com` host.32- A new env var, pydantic setting, or config field whose value is an endpoint33 URL that will later be requested.34- A new `InferenceHTTPClient(api_url=...)` construction in server-side code.35- Any change to `wrap_url`, `roboflow_secure_gateway_proxy_url_builder`,36 `SECURE_GATEWAY` / `LICENSE_SERVER` parsing, or `/proxy?url=` handling.3738## Review checklist3940Tag each finding. Fix BLOCK before merge; raise FLAG; NIT is optional.4142- **BLOCK** — A new request to a Roboflow-owned endpoint whose URL does not43 pass through `wrap_url()` (server code) or the weights-provider44 `proxy_url_builder` (`inference_models`) before the socket is opened.45 Trace the URL from construction to the `requests`/`aiohttp` call; "it is46 wrapped somewhere upstream" must be verifiable at a specific line. (Rule 1.)47- **BLOCK** — An endpoint URL sourced from an env var or setting where only48 the DEFAULT value is wrapped. Overrides must be wrapped at consumption time49 (validator or call site), not at field-definition time — a user-supplied50 `TELEMETRY_*`-style override otherwise bypasses the gateway. (Rule 2.)51- **BLOCK** — A network call that runs at module import time (version checks,52 registry warm-ups) with no timeout, or without honoring53 `SECURE_GATEWAY` / `OFFLINE_MODE` / its dedicated disable flag. Behind a54 packet-dropping firewall an untimed import-time call stalls server startup55 for the OS TCP timeout. (Rule 3.)56- **BLOCK** — A new consumer of hosted inference endpoints (`HOSTED_*_URL`,57 via `InferenceHTTPClient` or otherwise) with no `SECURE_GATEWAY` gate. The58 SDK client joins request paths onto `api_url`, which cannot compose with59 `/proxy?url=<encoded>` — hosted targets are unreachable behind a gateway, so60 the path must fail fast or fall back (see the remote+hosted → local fallback61 in `inference/core/env.py`). (Rule 4.)62- **FLAG** — Hand-rolled wrapping (f-string building `/proxy?url=` directly)63 instead of calling `wrap_url()`. The helper is idempotent and handles64 scheme-qualified vs bare-host gateways; inline copies drift. (Rule 5.)65- **FLAG** — A wrapped URL persisted to disk/db (queues, caches) and re-wrapped66 on read, or an endpoint stored pre-wrapped where `SECURE_GATEWAY` could67 differ between writer and reader processes. Persist raw URLs; wrap at send68 time. (Rule 6.)69- **FLAG** — A new third-party integration (Google/Stability/Twilio/Modal/70 webhook-style) reachable from a workflow or server path with no timeout or71 with errors that propagate as crashes rather than typed failures — behind a72 gateway these hosts are unreachable and the block must degrade cleanly.73 (Rule 7.)74- **NIT** — A new outbound integration not mentioned in the `SECURE_GATEWAY`75 section of `docs/quickstart/docker_configuration_options.md` when its76 gateway behavior is user-visible.7778### Not blocking7980- Calls to localhost / 127.0.0.1 / Unix sockets / LAN devices (PLC, ONVIF,81 local event-ingestion) — the gateway constraint is about egress.82- User-supplied media/webhook URLs fetched on behalf of the user (these are83 covered by `review-topic-input-boundary-security`); flag only if a NEW84 Roboflow-owned endpoint hides among them.85- Test files, docstrings, and examples containing URLs.86- `inference_models` package files downloaded from provider-returned URLs —87 those arrive pre-wrapped from `parse_package_artefacts` when the gateway is88 configured; do not demand a second wrap.8990## Standards (one canonical statement per rule)91921. **Wrap at the socket, verifiably.** Every Roboflow-bound URL must flow93 through `wrap_url()` / `proxy_url_builder` on a traceable line before the94 request executes. The audit that produced this skill found the entire95 `roboflow_api.py` surface wrapped but 42 workflow `run_remotely` blocks and96 the version check unwrapped (#2658) — coverage claims must be per-call-site,97 not per-module.982. **Defaults are not configuration.** Wrapping applied to a default value99 (field default, module constant) silently exempts every override. Wrap in100 the validator or at the call site so env-provided endpoints route through101 the gateway too (`TelemetrySettings` fix in #2658).1023. **Import-time network calls need a bound and a switch.** Any call that can103 run during `import inference.*` must carry an explicit `timeout=` and be104 disabled by `SECURE_GATEWAY` / `OFFLINE_MODE` / its own flag. The GitHub105 version check ran untimed at import and could stall startup for minutes on106 air-gapped networks (#2658).1074. **Hosted endpoints cannot be proxied — gate them.** Until `inference_sdk`108 understands the gateway `/proxy` contract, any server-side path that would109 contact `HOSTED_*_URL` under `SECURE_GATEWAY` must be rerouted or refused110 with a clear warning at configuration time, not left to fail per request.1115. **One wrapper implementation per package.** Server code calls `wrap_url()`;112 `inference_models` code uses the weights-provider builder. New inline113 `/proxy?url=` string construction is drift waiting to happen.1146. **Wrap late, store raw.** `SECURE_GATEWAY` is process configuration;115 persisted URLs outlive processes. Anything writing URLs to sqlite queues,116 caches, or model artifacts stores the raw URL and wraps at send time —117 `wrap_url` idempotence is a safety net, not a design.1187. **Unreachable third parties must degrade, not crash.** Blocks contacting119 non-Roboflow services are expected to fail behind a gateway — with a120 timeout, a typed/structured error, and no retry storm.