@miller-tech/uap 1.127.0 → 1.128.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -155,7 +155,16 @@ ANTHROPIC_API_BASE = os.environ.get(
155
155
  ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY", "")
156
156
  ANTHROPIC_PASSTHROUGH_MODELS = os.environ.get("ANTHROPIC_PASSTHROUGH_MODELS", "")
157
157
  PROXY_PORT = int(os.environ.get("PROXY_PORT", "4000"))
158
- PROXY_HOST = os.environ.get("PROXY_HOST", "0.0.0.0")
158
+ # Bind loopback by DEFAULT (security audit: an unauthenticated 0.0.0.0 listener
159
+ # let any LAN host drive the local model and reach cloud passthrough). To run
160
+ # the proxy as a shared LAN service, set PROXY_HOST=0.0.0.0 AND set
161
+ # PROXY_AUTH_TOKEN so the exposure is credential-gated.
162
+ PROXY_HOST = os.environ.get("PROXY_HOST", "127.0.0.1")
163
+ # Optional shared secret. When set, every request to a model route must present
164
+ # it as `Authorization: Bearer <token>` or `X-Uap-Proxy-Token: <token>`. Unset
165
+ # (default) = no check, which is safe only because the default bind is loopback.
166
+ # The health probe stays open so liveness checks don't need the secret.
167
+ PROXY_AUTH_TOKEN = os.environ.get("PROXY_AUTH_TOKEN", "").strip()
159
168
  PROXY_LOG_LEVEL = os.environ.get("PROXY_LOG_LEVEL", "INFO").upper()
160
169
  PROXY_READ_TIMEOUT = float(os.environ.get("PROXY_READ_TIMEOUT", "180"))
161
170
  PROXY_GENERATION_TIMEOUT = float(os.environ.get("PROXY_GENERATION_TIMEOUT", "300"))
@@ -3051,6 +3060,46 @@ app = FastAPI(
3051
3060
  lifespan=lifespan,
3052
3061
  )
3053
3062
 
3063
+
3064
+ # Open paths that never require the shared secret (liveness / discovery), so a
3065
+ # LAN health check or an SDK model-list probe works without the token.
3066
+ _PROXY_AUTH_OPEN_PATHS = frozenset({"/health", "/", "/v1/models"})
3067
+
3068
+
3069
+ @app.middleware("http")
3070
+ async def _shared_secret_auth(request: Request, call_next):
3071
+ """Gate every request behind PROXY_AUTH_TOKEN when it is set.
3072
+
3073
+ No-op when the token is unset (the default; safe only because the default
3074
+ bind is loopback). When set — the intended posture for a shared LAN service
3075
+ (PROXY_HOST=0.0.0.0) — a request must present the token as
3076
+ `Authorization: Bearer <token>` or `X-Uap-Proxy-Token: <token>`; otherwise
3077
+ 401. Uses a constant-time compare to avoid a timing oracle.
3078
+ """
3079
+ if PROXY_AUTH_TOKEN and request.url.path not in _PROXY_AUTH_OPEN_PATHS and request.method != "OPTIONS":
3080
+ provided = request.headers.get("x-uap-proxy-token", "")
3081
+ if not provided:
3082
+ auth = request.headers.get("authorization", "")
3083
+ if auth.lower().startswith("bearer "):
3084
+ provided = auth[7:].strip()
3085
+ import hmac as _hmac
3086
+
3087
+ if not (provided and _hmac.compare_digest(provided, PROXY_AUTH_TOKEN)):
3088
+ return Response(
3089
+ content=json.dumps(
3090
+ {
3091
+ "type": "error",
3092
+ "error": {
3093
+ "type": "authentication_error",
3094
+ "message": "missing or invalid proxy token (set X-Uap-Proxy-Token or Authorization: Bearer)",
3095
+ },
3096
+ }
3097
+ ),
3098
+ status_code=401,
3099
+ media_type="application/json",
3100
+ )
3101
+ return await call_next(request)
3102
+
3054
3103
  # NOTE: Concurrency control is enforced by _acquire_upstream_slot() inside
3055
3104
  # _post_with_retry (the single point where we hit llama.cpp). An earlier
3056
3105
  # implementation also added an HTTP middleware that acquired the same