Spaces:
Running
Running
Upload 34 files
Browse files- README.md +100 -7
- _utils/_shared_logic.py +6 -0
- app.py +47 -14
README.md
CHANGED
|
@@ -246,10 +246,95 @@ pages/WAL remnants, backups, CDN/provider logs, and infrastructure snapshots are
|
|
| 246 |
|
| 247 |
## Configuration
|
| 248 |
|
| 249 |
-
Configure
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 250 |
|
| 251 |
-
|
| 252 |
-
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
|
| 254 |
See [DATASET_COLLECTION_GUIDANCE.md](./DATASET_COLLECTION_GUIDANCE.md) for the beginner-to-operator storage guide, including exact HF-only, GitHub-only, Primary+Mirror, migration, testing, and deduplication recipes.
|
| 255 |
|
|
@@ -317,7 +402,7 @@ production.
|
|
| 317 |
| `BACKEND_URL` | No | `""` | **Path 1** β Explicit upstream URL. All requests forwarded here when set. Use for Docker Model Runner, Ollama, or any custom backend. |
|
| 318 |
| `HF_SPACES_MODEL_URL` | No | β | **Path 2** β Custom ZeroGPU Space URL (e.g. `https://scikit-plots-ai-model.hf.space/v1/chat/completions`). Receives requests whose `model` matches `HF_SPACES_MODEL_NAMESPACES`. |
|
| 319 |
| `HF_SPACES_MODEL_NAMESPACES` | No | `scikit-plots` | Comma-separated model owner prefixes routed to Path 2 (e.g. `scikit-plots,my-org`). |
|
| 320 |
-
| `ALLOWED_MODELS` | No |
|
| 321 |
| `HF_BASE` | No | `https://router.huggingface.co` | **Path 3** β HF Serverless API base URL. Only used when Path 1 and Path 2 do not match. |
|
| 322 |
| `DEFAULT_MODEL` | No | `scikit-plots/Qwen2.5-Coder-7B-Instruct` | Fallback model when the request body omits `"model"`. |
|
| 323 |
|
|
@@ -386,7 +471,8 @@ All values are in seconds. Non-integer values silently fall back to the default
|
|
| 386 |
|
| 387 |
| Variable | Default | Description |
|
| 388 |
|---|---|---|
|
| 389 |
-
| `ALLOWED_ORIGINS` | empty | Comma-separated
|
|
|
|
| 390 |
| `DEPLOYMENT_PROFILE` | `compat` | `compat` preserves explicit legacy deployment choices. The hardened Dockerfile sets `strict`, which fails startup as root, rejects wildcard origins and opaque-origin **writes**, and requires verified TLS for all configured Redis authorities. Read-only opaque Share compatibility remains a separate explicit opt-in because local `file://` viewers may need it. |
|
| 391 |
| `REDIS_REQUIRE_TLS` | `false` (`true` in strict) | Requires `rediss://`; URL query parameters are rejected so callers cannot disable certificate verification through redis-py URL options. |
|
| 392 |
| `REQUIRE_NON_ROOT` | `false` (`true` in strict) | Fails application startup when the process is UID 0 on POSIX. |
|
|
@@ -694,8 +780,15 @@ HF_DATASET_TOKEN = hf_<fine-grained-dataset-token>
|
|
| 694 |
# For Primary + Mirrors, use RECORD_STORAGE_TARGETS instead; see DATASET_COLLECTION_GUIDANCE.md.
|
| 695 |
|
| 696 |
# Security
|
| 697 |
-
#
|
|
|
|
|
|
|
|
|
|
| 698 |
ALLOWED_ORIGINS =
|
|
|
|
|
|
|
|
|
|
|
|
|
| 699 |
# Optional read-only local-file (Origin:null) Share compatibility.
|
| 700 |
# Keep false unless you explicitly need Global Share viewing from file:// pages.
|
| 701 |
SHARE_ALLOW_OPAQUE_ORIGIN = false
|
|
@@ -712,7 +805,7 @@ BASE=https://scikit-plots-ai.hf.space
|
|
| 712 |
|
| 713 |
# 1. Liveness probe
|
| 714 |
curl $BASE/health
|
| 715 |
-
# {"status":"ok","version":"
|
| 716 |
|
| 717 |
# Optional deterministic stub rig status
|
| 718 |
curl -s $BASE/health | python3 -m json.tool
|
|
|
|
| 246 |
|
| 247 |
## Configuration
|
| 248 |
|
| 249 |
+
Configure the Space from **Settings β Variables and secrets**. Hugging Face exposes
|
| 250 |
+
Variables as non-sensitive deployment configuration and keeps Secrets private. See
|
| 251 |
+
[Managing Spaces variables and secrets](https://huggingface.co/docs/hub/spaces-overview#managing-secrets).
|
| 252 |
+
|
| 253 |
+
### Variables and secrets
|
| 254 |
+
|
| 255 |
+
Use one simple rule: **configuration goes in Variables; credentials and capabilities go in Secrets**.
|
| 256 |
+
Both surfaces become environment variables inside the container, so putting a non-secret value
|
| 257 |
+
in Secrets still works technically, but it makes maintenance harder and hides which settings are
|
| 258 |
+
actually sensitive.
|
| 259 |
+
|
| 260 |
+
#### Variables β public / non-sensitive
|
| 261 |
+
|
| 262 |
+
| Name | Required? | Typical value | Notes |
|
| 263 |
+
|---|---:|---|---|
|
| 264 |
+
| `RECORD_STORAGE_TARGETS` | Multi-store deployments | JSON array | Provider-neutral Primary + Mirrors topology. May contain repo IDs and `token_env` **names**, never token values. If repository topology itself is confidential, it is acceptable to store this JSON as a Secret instead. |
|
| 265 |
+
| `TRAINING_DATASET_REPO` | Legacy HF-only mode | `scikit-plots/ai-assistant-contributions` | Repository identifier, not a credential. If this is currently stored as a Secret, it can be moved to Variables unless you intentionally treat the repo identity as confidential. Ignored as the active topology when `RECORD_STORAGE_TARGETS` is set. |
|
| 266 |
+
| `ALLOWED_MODELS` | No | comma-separated model IDs | Exact Path-3 model allow-list. Do not use `*`; an explicit model list prevents the proxy from becoming a general-purpose inference relay. |
|
| 267 |
+
| `HF_SPACES_MODEL_NAMESPACES` | No | `scikit-plots` | Model owner prefixes routed through the configured Path-2 model Space. |
|
| 268 |
+
| `ALLOWED_ORIGINS` | Custom sites only | comma-separated origins | Exact browser origins such as `https://docs.example.org`. Origins contain only scheme + host (+ optional port): no path, query, fragment, or trailing page URL. |
|
| 269 |
+
| `ALLOWED_ORIGINS_MODE` | No | `additive` | `additive` keeps the bundled Scikit-plots origins and adds `ALLOWED_ORIGINS`; `replace` trusts only `ALLOWED_ORIGINS` and is the recommended mode for forks/downstream sites that want their own CORS boundary. |
|
| 270 |
+
| `CONTRIBUTION_REVIEW_MODE` | No | `provider-pr` or `ledger` | Native provider PR/MR review or historical ledger review. |
|
| 271 |
+
| `HF_TOKEN_TYPE` | Recommended | `fine-grained` / `read` / `write` | Non-secret classification label for `HF_TOKEN`; avoids `unknown` startup diagnostics. |
|
| 272 |
+
| `HF_DATASET_TOKEN_TYPE` | Legacy HF persistence | `fine-grained` | Non-secret classification label for the dataset-persistence token. |
|
| 273 |
+
|
| 274 |
+
The proxy has two built-in browser origins because this extension is currently deployed on both
|
| 275 |
+
Scikit-plots documentation sites:
|
| 276 |
|
| 277 |
+
```text
|
| 278 |
+
https://scikit-plots.github.io
|
| 279 |
+
https://scikit-plots-learn.readthedocs.io
|
| 280 |
+
```
|
| 281 |
+
|
| 282 |
+
With the default `ALLOWED_ORIGINS_MODE=additive`, both remain trusted and any
|
| 283 |
+
`ALLOWED_ORIGINS` entries are appended. A downstream/open-source deployment can take complete
|
| 284 |
+
control without editing `app.py`:
|
| 285 |
+
|
| 286 |
+
```text
|
| 287 |
+
# Custom/fork deployment: trust only these sites
|
| 288 |
+
ALLOWED_ORIGINS=https://docs.example.org,https://learn.example.org
|
| 289 |
+
ALLOWED_ORIGINS_MODE=replace
|
| 290 |
+
```
|
| 291 |
+
|
| 292 |
+
To add a site while retaining the two Scikit-plots defaults:
|
| 293 |
+
|
| 294 |
+
```text
|
| 295 |
+
ALLOWED_ORIGINS=https://preview.example.org
|
| 296 |
+
ALLOWED_ORIGINS_MODE=additive
|
| 297 |
+
```
|
| 298 |
+
|
| 299 |
+
Never use `ALLOWED_ORIGINS=*` in production. CORS is a browser abuse boundary, not authentication;
|
| 300 |
+
server-to-server clients without an `Origin` header still rely on their own token/capability controls.
|
| 301 |
+
|
| 302 |
+
#### Secrets β private / server-only
|
| 303 |
+
|
| 304 |
+
| Name | Required? | Purpose |
|
| 305 |
+
|---|---:|---|
|
| 306 |
+
| `HF_TOKEN` | Path 3 inference | Hugging Face inference credential. Prefer least privilege; it should not also be your broad repository-write token. |
|
| 307 |
+
| `AI_RECORD_STORAGE_TOKEN_HF_PRIMARY` | When referenced by `RECORD_STORAGE_TARGETS` | Example HF Primary write credential. The exact name is configurable through each target's `token_env`. |
|
| 308 |
+
| `AI_RECORD_STORAGE_TOKEN_GITHUB_MIRROR` | When referenced by `RECORD_STORAGE_TARGETS` | Example GitHub Mirror write credential. Use an independent least-privilege token. |
|
| 309 |
+
| `AI_RECORD_STORAGE_TOKEN_*` | Per configured target | Provider-specific HF/GitHub/GitLab/Bitbucket write credential. Only names with this prefix are accepted by storage target configuration. |
|
| 310 |
+
| `HF_DATASET_TOKEN` | Legacy HF-only persistence | Preferred legacy dataset token; fine-grained to the target dataset repository. |
|
| 311 |
+
| `CONTRIBUTION_REVIEW_TOKEN` | Optional | API-driven review/promotion capability. Not required when maintainers review entirely through provider-native PR/MR UI. |
|
| 312 |
+
| `RATE_LIMIT_IDENTITY_SECRET` | Redis rate limiting | HMAC key used to pseudonymize shared rate-limit identities. |
|
| 313 |
+
| `CONTRIBUTION_LEDGER_KEY_SECRET` | Redis contribution ledger | HMAC key used to pseudonymize receipt identifiers. |
|
| 314 |
+
| Redis URLs containing credentials | When Redis is used | Keep `RATE_LIMIT_REDIS_URL`, `SHARE_STORE_REDIS_URL`, and `CONTRIBUTION_LEDGER_REDIS_URL` private when they contain usernames/passwords/tokens. |
|
| 315 |
+
|
| 316 |
+
A practical Scikit-plots Space layout is therefore:
|
| 317 |
+
|
| 318 |
+
```text
|
| 319 |
+
# Variables
|
| 320 |
+
RECORD_STORAGE_TARGETS=<provider-neutral JSON topology>
|
| 321 |
+
ALLOWED_MODELS=openai/gpt-oss-20b,Qwen/Qwen2.5-Coder-7B-Instruct,Qwen/Qwen2.5-Coder-32B-Instruct,scikit-plots/gpt-oss-20b,scikit-plots/Qwen2.5-Coder-7B-Instruct,scikit-plots/Qwen2.5-Coder-32B-Instruct
|
| 322 |
+
HF_SPACES_MODEL_NAMESPACES=scikit-plots
|
| 323 |
+
ALLOWED_ORIGINS_MODE=additive
|
| 324 |
+
# ALLOWED_ORIGINS may remain empty because both current Scikit-plots sites are built in.
|
| 325 |
+
|
| 326 |
+
# Legacy-only variable (not needed as the active topology when RECORD_STORAGE_TARGETS is used)
|
| 327 |
+
TRAINING_DATASET_REPO=scikit-plots/ai-assistant-contributions
|
| 328 |
+
|
| 329 |
+
# Secrets
|
| 330 |
+
HF_TOKEN=<inference-token>
|
| 331 |
+
AI_RECORD_STORAGE_TOKEN_HF_PRIMARY=<repo-scoped-write-token>
|
| 332 |
+
AI_RECORD_STORAGE_TOKEN_GITHUB_MIRROR=<repo-scoped-write-token>
|
| 333 |
+
```
|
| 334 |
+
|
| 335 |
+
The proxy never reads provider token values from `RECORD_STORAGE_TARGETS`; it reads only the
|
| 336 |
+
configured `token_env` name and then resolves that environment variable server-side. Keep token
|
| 337 |
+
values out of `conf.py`, generated Sphinx HTML, JavaScript, logs, repository URLs, and commit metadata.
|
| 338 |
|
| 339 |
See [DATASET_COLLECTION_GUIDANCE.md](./DATASET_COLLECTION_GUIDANCE.md) for the beginner-to-operator storage guide, including exact HF-only, GitHub-only, Primary+Mirror, migration, testing, and deduplication recipes.
|
| 340 |
|
|
|
|
| 402 |
| `BACKEND_URL` | No | `""` | **Path 1** β Explicit upstream URL. All requests forwarded here when set. Use for Docker Model Runner, Ollama, or any custom backend. |
|
| 403 |
| `HF_SPACES_MODEL_URL` | No | β | **Path 2** β Custom ZeroGPU Space URL (e.g. `https://scikit-plots-ai-model.hf.space/v1/chat/completions`). Receives requests whose `model` matches `HF_SPACES_MODEL_NAMESPACES`. |
|
| 404 |
| `HF_SPACES_MODEL_NAMESPACES` | No | `scikit-plots` | Comma-separated model owner prefixes routed to Path 2 (e.g. `scikit-plots,my-org`). |
|
| 405 |
+
| `ALLOWED_MODELS` | No | `DEFAULT_MODEL` + bundled provider models | Exact model IDs accepted by the strict chat contract before routing. The bundled defaults include the configured `DEFAULT_MODEL` plus `Qwen/Qwen2.5-Coder-7B-Instruct`, `Qwen/Qwen2.5-Coder-32B-Instruct`, and `openai/gpt-oss-20b`; Path-2 namespaces remain an additional explicit admission rule. |
|
| 406 |
| `HF_BASE` | No | `https://router.huggingface.co` | **Path 3** β HF Serverless API base URL. Only used when Path 1 and Path 2 do not match. |
|
| 407 |
| `DEFAULT_MODEL` | No | `scikit-plots/Qwen2.5-Coder-7B-Instruct` | Fallback model when the request body omits `"model"`. |
|
| 408 |
|
|
|
|
| 471 |
|
| 472 |
| Variable | Default | Description |
|
| 473 |
|---|---|---|
|
| 474 |
+
| `ALLOWED_ORIGINS` | empty | Comma-separated exact browser origins. In `additive` mode they are appended to the two bundled defaults (`https://scikit-plots.github.io`, `https://scikit-plots-learn.readthedocs.io`). In `replace` mode they are the complete browser allow-list. Missing `Origin` remains valid for server-to-server clients. `*` is an explicit insecure compatibility escape hatch and is not authentication. |
|
| 475 |
+
| `ALLOWED_ORIGINS_MODE` | `additive` | `additive` retains the bundled project origins; `replace` starts from an empty allow-list and trusts only valid entries from `ALLOWED_ORIGINS`. Invalid values fail safely back to `additive`. |
|
| 476 |
| `DEPLOYMENT_PROFILE` | `compat` | `compat` preserves explicit legacy deployment choices. The hardened Dockerfile sets `strict`, which fails startup as root, rejects wildcard origins and opaque-origin **writes**, and requires verified TLS for all configured Redis authorities. Read-only opaque Share compatibility remains a separate explicit opt-in because local `file://` viewers may need it. |
|
| 477 |
| `REDIS_REQUIRE_TLS` | `false` (`true` in strict) | Requires `rediss://`; URL query parameters are rejected so callers cannot disable certificate verification through redis-py URL options. |
|
| 478 |
| `REQUIRE_NON_ROOT` | `false` (`true` in strict) | Fails application startup when the process is UID 0 on POSIX. |
|
|
|
|
| 780 |
# For Primary + Mirrors, use RECORD_STORAGE_TARGETS instead; see DATASET_COLLECTION_GUIDANCE.md.
|
| 781 |
|
| 782 |
# Security
|
| 783 |
+
# Both current Scikit-plots origins are built in:
|
| 784 |
+
# https://scikit-plots.github.io
|
| 785 |
+
# https://scikit-plots-learn.readthedocs.io
|
| 786 |
+
# Leave empty to use only those defaults, or add exact custom origins.
|
| 787 |
ALLOWED_ORIGINS =
|
| 788 |
+
ALLOWED_ORIGINS_MODE = additive
|
| 789 |
+
# Fork/downstream alternative:
|
| 790 |
+
# ALLOWED_ORIGINS=https://docs.example.org,https://learn.example.org
|
| 791 |
+
# ALLOWED_ORIGINS_MODE=replace
|
| 792 |
# Optional read-only local-file (Origin:null) Share compatibility.
|
| 793 |
# Keep false unless you explicitly need Global Share viewing from file:// pages.
|
| 794 |
SHARE_ALLOW_OPAQUE_ORIGIN = false
|
|
|
|
| 805 |
|
| 806 |
# 1. Liveness probe
|
| 807 |
curl $BASE/health
|
| 808 |
+
# {"status":"ok","version":"7.3.0"}
|
| 809 |
|
| 810 |
# Optional deterministic stub rig status
|
| 811 |
curl -s $BASE/health | python3 -m json.tool
|
_utils/_shared_logic.py
CHANGED
|
@@ -1413,6 +1413,8 @@ def load_proxy_env() -> dict[str, Any]:
|
|
| 1413 |
Path 3 read timeout (env ``PATH3_TIMEOUT``).
|
| 1414 |
``max_body_bytes`` : int
|
| 1415 |
``allowed_origins`` : str
|
|
|
|
|
|
|
| 1416 |
``hf_token_type`` : str
|
| 1417 |
Classified token type for *hf_token* (env ``HF_TOKEN_TYPE``).
|
| 1418 |
One of ``"fine-grained"``, ``"read"``, ``"write"``, ``"unknown"``.
|
|
@@ -1504,4 +1506,8 @@ def load_proxy_env() -> dict[str, Any]:
|
|
| 1504 |
DEFAULT_MAX_BODY_BYTES,
|
| 1505 |
),
|
| 1506 |
"allowed_origins": os.environ.get("ALLOWED_ORIGINS", "").strip(),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1507 |
}
|
|
|
|
| 1413 |
Path 3 read timeout (env ``PATH3_TIMEOUT``).
|
| 1414 |
``max_body_bytes`` : int
|
| 1415 |
``allowed_origins`` : str
|
| 1416 |
+
``allowed_origins_mode`` : str
|
| 1417 |
+
Raw deployment composition mode (``additive`` or ``replace``).
|
| 1418 |
``hf_token_type`` : str
|
| 1419 |
Classified token type for *hf_token* (env ``HF_TOKEN_TYPE``).
|
| 1420 |
One of ``"fine-grained"``, ``"read"``, ``"write"``, ``"unknown"``.
|
|
|
|
| 1506 |
DEFAULT_MAX_BODY_BYTES,
|
| 1507 |
),
|
| 1508 |
"allowed_origins": os.environ.get("ALLOWED_ORIGINS", "").strip(),
|
| 1509 |
+
"allowed_origins_mode": (
|
| 1510 |
+
os.environ.get("ALLOWED_ORIGINS_MODE", "additive").strip().lower()
|
| 1511 |
+
or "additive"
|
| 1512 |
+
),
|
| 1513 |
}
|
app.py
CHANGED
|
@@ -87,7 +87,8 @@
|
|
| 87 |
# persistence; fine-grained repo-write is preferred.
|
| 88 |
# HF_WRITE_TOKEN_TYPE Legacy type declaration for HF_WRITE_TOKEN.
|
| 89 |
# Accepted values: fine-grained | read | write.
|
| 90 |
-
# ALLOWED_ORIGINS Comma-separated
|
|
|
|
| 91 |
# RECORD_STORAGE_TARGETS Optional JSON array defining one primary record
|
| 92 |
# store plus mirrors (huggingface/github/gitlab/bitbucket).
|
| 93 |
# Credentials are referenced only through env names with
|
|
@@ -993,8 +994,11 @@ _protocol_retries: int = max(
|
|
| 993 |
# CORS
|
| 994 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 995 |
|
| 996 |
-
_DEFAULT_ALLOWED_ORIGINS
|
| 997 |
-
|
|
|
|
|
|
|
|
|
|
| 998 |
|
| 999 |
|
| 1000 |
def _normalise_browser_origin(value: str) -> str:
|
|
@@ -1017,18 +1021,32 @@ def _normalise_browser_origin(value: str) -> str:
|
|
| 1017 |
return f"{parsed.scheme.lower()}://{parsed.netloc.lower()}"
|
| 1018 |
|
| 1019 |
|
| 1020 |
-
def
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1021 |
"""Build the exact browser-origin allowlist.
|
| 1022 |
|
| 1023 |
-
|
| 1024 |
-
|
| 1025 |
-
|
| 1026 |
-
an explicit ``*`` selects wildcard compatibility mode.
|
| 1027 |
"""
|
| 1028 |
text = str(raw or "").strip()
|
| 1029 |
if text == "*":
|
| 1030 |
return ["*"]
|
| 1031 |
-
|
|
|
|
|
|
|
|
|
|
| 1032 |
for item in text.split(","):
|
| 1033 |
item = item.strip() # ruff: ignore[redefined-loop-name]
|
| 1034 |
if not item:
|
|
@@ -1045,7 +1063,12 @@ def _build_allowed_origins(raw: str) -> list[str]:
|
|
| 1045 |
|
| 1046 |
|
| 1047 |
_raw_origins: str = os.environ.get("ALLOWED_ORIGINS", "").strip()
|
| 1048 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1049 |
|
| 1050 |
#: Optional Share-only compatibility for browser opaque origins such as
|
| 1051 |
#: ``file://`` documents, which browsers serialize as ``Origin: null``. This is
|
|
@@ -1076,9 +1099,13 @@ if _allowed_origins == ["*"]:
|
|
| 1076 |
"AI proxy [CORS_WILDCARD]: ALLOWED_ORIGINS=* explicitly enables every browser origin; "
|
| 1077 |
"use an exact comma-separated origin list in production."
|
| 1078 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1079 |
elif _raw_origins:
|
| 1080 |
logger.info(
|
| 1081 |
-
"AI proxy CORS:
|
| 1082 |
)
|
| 1083 |
|
| 1084 |
#: Trust ``X-Forwarded-For`` only when a known ingress proxy strips/overwrites
|
|
@@ -2706,14 +2733,20 @@ def _canonical_payload_digest(value: Any) -> str:
|
|
| 2706 |
def _cors_public_status() -> dict[str, Any]:
|
| 2707 |
"""Return public CORS diagnostics without exposing custom/private origins."""
|
| 2708 |
wildcard = _allowed_origins == ["*"]
|
|
|
|
| 2709 |
return {
|
| 2710 |
-
|
|
|
|
| 2711 |
"official_docs_origin_allowed": (
|
| 2712 |
-
wildcard or
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2713 |
),
|
| 2714 |
"wildcard": wildcard,
|
| 2715 |
"allowed_origin_count": None if wildcard else len(_allowed_origins),
|
| 2716 |
-
"env_semantics":
|
| 2717 |
"share_opaque_origin_allowed": SHARE_ALLOW_OPAQUE_ORIGIN,
|
| 2718 |
"share_opaque_origin_write_allowed": bool(
|
| 2719 |
SHARE_ALLOW_OPAQUE_ORIGIN and SHARE_ALLOW_OPAQUE_ORIGIN_WRITE
|
|
|
|
| 87 |
# persistence; fine-grained repo-write is preferred.
|
| 88 |
# HF_WRITE_TOKEN_TYPE Legacy type declaration for HF_WRITE_TOKEN.
|
| 89 |
# Accepted values: fine-grained | read | write.
|
| 90 |
+
# ALLOWED_ORIGINS Comma-separated exact CORS origins. Additive by default; use ALLOWED_ORIGINS_MODE=replace for downstream sites.
|
| 91 |
+
# ALLOWED_ORIGINS_MODE additive (default) keeps built-in Scikit-plots origins; replace trusts only ALLOWED_ORIGINS.
|
| 92 |
# RECORD_STORAGE_TARGETS Optional JSON array defining one primary record
|
| 93 |
# store plus mirrors (huggingface/github/gitlab/bitbucket).
|
| 94 |
# Credentials are referenced only through env names with
|
|
|
|
| 994 |
# CORS
|
| 995 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 996 |
|
| 997 |
+
_DEFAULT_ALLOWED_ORIGINS: tuple[str, ...] = (
|
| 998 |
+
"https://scikit-plots.github.io",
|
| 999 |
+
"https://scikit-plots-learn.readthedocs.io",
|
| 1000 |
+
)
|
| 1001 |
+
_ALLOWED_ORIGIN_MODES: frozenset[str] = frozenset({"additive", "replace"})
|
| 1002 |
|
| 1003 |
|
| 1004 |
def _normalise_browser_origin(value: str) -> str:
|
|
|
|
| 1021 |
return f"{parsed.scheme.lower()}://{parsed.netloc.lower()}"
|
| 1022 |
|
| 1023 |
|
| 1024 |
+
def _normalise_allowed_origins_mode(value: str) -> str:
|
| 1025 |
+
"""Return the supported CORS composition mode without echoing bad input."""
|
| 1026 |
+
mode = str(value or "additive").strip().lower() or "additive"
|
| 1027 |
+
if mode not in _ALLOWED_ORIGIN_MODES:
|
| 1028 |
+
logger.warning(
|
| 1029 |
+
"AI proxy [CORS_MODE_INVALID]: invalid ALLOWED_ORIGINS_MODE; using additive defaults."
|
| 1030 |
+
)
|
| 1031 |
+
return "additive"
|
| 1032 |
+
return mode
|
| 1033 |
+
|
| 1034 |
+
|
| 1035 |
+
def _build_allowed_origins(raw: str, *, mode: str = "additive") -> list[str]:
|
| 1036 |
"""Build the exact browser-origin allowlist.
|
| 1037 |
|
| 1038 |
+
``additive`` retains the package defaults and appends deployment-specific
|
| 1039 |
+
exact origins. ``replace`` starts empty so downstream/open-source deployments
|
| 1040 |
+
can own their complete browser-origin trust boundary without editing source.
|
| 1041 |
+
Only an explicit ``*`` selects wildcard compatibility mode.
|
| 1042 |
"""
|
| 1043 |
text = str(raw or "").strip()
|
| 1044 |
if text == "*":
|
| 1045 |
return ["*"]
|
| 1046 |
+
selected_mode = _normalise_allowed_origins_mode(mode)
|
| 1047 |
+
merged: list[str] = (
|
| 1048 |
+
list(_DEFAULT_ALLOWED_ORIGINS) if selected_mode == "additive" else []
|
| 1049 |
+
)
|
| 1050 |
for item in text.split(","):
|
| 1051 |
item = item.strip() # ruff: ignore[redefined-loop-name]
|
| 1052 |
if not item:
|
|
|
|
| 1063 |
|
| 1064 |
|
| 1065 |
_raw_origins: str = os.environ.get("ALLOWED_ORIGINS", "").strip()
|
| 1066 |
+
ALLOWED_ORIGINS_MODE: str = _normalise_allowed_origins_mode(
|
| 1067 |
+
os.environ.get("ALLOWED_ORIGINS_MODE", "additive")
|
| 1068 |
+
)
|
| 1069 |
+
_allowed_origins: list[str] = _build_allowed_origins(
|
| 1070 |
+
_raw_origins, mode=ALLOWED_ORIGINS_MODE
|
| 1071 |
+
)
|
| 1072 |
|
| 1073 |
#: Optional Share-only compatibility for browser opaque origins such as
|
| 1074 |
#: ``file://`` documents, which browsers serialize as ``Origin: null``. This is
|
|
|
|
| 1099 |
"AI proxy [CORS_WILDCARD]: ALLOWED_ORIGINS=* explicitly enables every browser origin; "
|
| 1100 |
"use an exact comma-separated origin list in production."
|
| 1101 |
)
|
| 1102 |
+
elif ALLOWED_ORIGINS_MODE == "replace":
|
| 1103 |
+
logger.info(
|
| 1104 |
+
"AI proxy CORS: replacement mode active; only exact ALLOWED_ORIGINS entries are trusted."
|
| 1105 |
+
)
|
| 1106 |
elif _raw_origins:
|
| 1107 |
logger.info(
|
| 1108 |
+
"AI proxy CORS: built-in documentation origins retained; ALLOWED_ORIGINS contributes additional exact origins."
|
| 1109 |
)
|
| 1110 |
|
| 1111 |
#: Trust ``X-Forwarded-For`` only when a known ingress proxy strips/overwrites
|
|
|
|
| 2733 |
def _cors_public_status() -> dict[str, Any]:
|
| 2734 |
"""Return public CORS diagnostics without exposing custom/private origins."""
|
| 2735 |
wildcard = _allowed_origins == ["*"]
|
| 2736 |
+
primary_default = _DEFAULT_ALLOWED_ORIGINS[0]
|
| 2737 |
return {
|
| 2738 |
+
# Backward-compatible singular fields refer to the primary project origin.
|
| 2739 |
+
"official_docs_origin": primary_default,
|
| 2740 |
"official_docs_origin_allowed": (
|
| 2741 |
+
wildcard or primary_default in _allowed_origins
|
| 2742 |
+
),
|
| 2743 |
+
"default_allowed_origin_count": len(_DEFAULT_ALLOWED_ORIGINS),
|
| 2744 |
+
"default_allowed_origins_allowed": (
|
| 2745 |
+
wildcard or all(origin in _allowed_origins for origin in _DEFAULT_ALLOWED_ORIGINS)
|
| 2746 |
),
|
| 2747 |
"wildcard": wildcard,
|
| 2748 |
"allowed_origin_count": None if wildcard else len(_allowed_origins),
|
| 2749 |
+
"env_semantics": ALLOWED_ORIGINS_MODE,
|
| 2750 |
"share_opaque_origin_allowed": SHARE_ALLOW_OPAQUE_ORIGIN,
|
| 2751 |
"share_opaque_origin_write_allowed": bool(
|
| 2752 |
SHARE_ALLOW_OPAQUE_ORIGIN and SHARE_ALLOW_OPAQUE_ORIGIN_WRITE
|